oft-agt 0.5.7 → 0.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli.mjs CHANGED
@@ -157,7 +157,7 @@ async function start(config) {
157
157
  try { const response = await fetch('https://discord.com/api/v10/users/@me', { headers: { Authorization: 'Bot ' + secret.token }, signal: AbortSignal.timeout(15000) }); if (response.ok) { const user = await response.json(); profile = { id: user.id, username: user.username, avatar: user.avatar }; } } finally { secret.token = ''; }
158
158
  } catch {}
159
159
  }
160
- send({ type: 'hello', agentVersion: AGENT_VERSION, updatesVersion: 1, officialUpdatesVersion: 1, protocol: 2, errorBranch: true, diagnosticsVersion: 2, optionsVersion: 1, conditionsVersion: 1, complementaryBlocksVersion: 1, discordActionsVersion: 1, messageButtonsVersion: 2, variablesVersion: Number(process.versions.node.split('.')[0]) > 22 || (Number(process.versions.node.split('.')[0]) === 22 && Number(process.versions.node.split('.')[1]) >= 5) ? 1 : 0, metricsVersion: usageReady ? 1 : 0, profile, running: Boolean(child && child.exitCode === null && !child.signalCode), hasToken: await exists(secretPath), deploymentId: active });
160
+ send({ type: 'hello', agentVersion: AGENT_VERSION, updatesVersion: 1, officialUpdatesVersion: 1, protocol: 2, errorBranch: true, diagnosticsVersion: 2, optionsVersion: 1, conditionsVersion: 1, eventsVersion: 1, complementaryBlocksVersion: 1, discordActionsVersion: 1, messageButtonsVersion: 2, variablesVersion: Number(process.versions.node.split('.')[0]) > 22 || (Number(process.versions.node.split('.')[0]) === 22 && Number(process.versions.node.split('.')[1]) >= 5) ? 1 : 0, metricsVersion: usageReady ? 1 : 0, profile, running: Boolean(child && child.exitCode === null && !child.signalCode), hasToken: await exists(secretPath), deploymentId: active });
161
161
  if (process.connected) process.send?.({ type: 'agent-ready' }, () => {});
162
162
  await usage.flush(send).catch(() => console.error('Statistiques locales indisponibles.'));
163
163
  usageTimer = setInterval(() => usage.flush(send).catch(() => console.error('Statistiques locales indisponibles.')), 60000);
package/compile-plan.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { validateEventGraph } from './runtime/events.mjs';
1
2
  import { isVariableBlock, validateVariableBlock, validateVariableDefinitions } from './runtime/custom-variables.mjs';
2
3
  import { validateButton } from './runtime/message-buttons.mjs';
3
4
  import { isDiscordBlock, validateDiscordBlock } from './runtime/discord-blocks.mjs';
@@ -43,6 +44,7 @@ export function compile(input) {
43
44
 
44
45
  if (!plain(command)) fail(`Commande ${commandIndex + 1} : configuration absente.`);
45
46
 
47
+ validateEventGraph(command);
46
48
  const name = text(command.name ?? entry.name, 'Nom de commande', 32, true);
47
49
 
48
50
  if (!/^[a-z0-9_-]{1,32}$/.test(name)) fail(`/${name} : utilisez des minuscules, chiffres, tirets ou underscores.`);
@@ -291,7 +293,7 @@ export function compile(input) {
291
293
 
292
294
  if (!calls.length || (errorCalls && !errorCalls.length)) fail(`/${name} : ajoutez au moins une action à chaque racine.`);
293
295
 
294
- return { name, description, ...(options.length ? { options } : {}), calls, ...(buttonBranches.length?{buttonBranches}:{}), ...(errorCalls ? { errorCalls } : {}) };
296
+ return { name, description, ...(command.trigger ? {trigger:command.trigger} : {}), ...(options.length ? { options } : {}), calls, ...(buttonBranches.length?{buttonBranches}:{}), ...(errorCalls ? { errorCalls } : {}) };
295
297
 
296
298
  });
297
299
 
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "oft-agt",
3
- "version": "0.5.7",
3
+ "version": "0.5.8",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "oft-agt",
9
- "version": "0.5.7",
9
+ "version": "0.5.8",
10
10
  "dependencies": {
11
11
  "discord.js": "^14.14.0",
12
12
  "ws": "^8.18.0"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oft-agt",
3
- "version": "0.5.7",
3
+ "version": "0.5.8",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -1,14 +1,28 @@
1
1
  export const CONTEXT_VARIABLES = {
2
2
  user_id:'ID de l’auteur',user_name:'Nom utilisateur',user_display_name:'Nom affiché',user_mention:'Mention de l’auteur',user_avatar:'URL de l’avatar',user_created_at:'Création du compte (ISO)',user_created_timestamp:'Création du compte (Unix)',user_bot:'Compte robot',user_locale:'Langue de l’auteur',
3
3
  member_nickname:'Surnom sur le serveur',member_joined_at:'Arrivée sur le serveur (ISO)',member_joined_timestamp:'Arrivée sur le serveur (Unix)',member_roles:'Mentions des rôles',member_role_ids:'IDs des rôles',member_boost_since:'Début du boost (ISO)',
4
+ first_channel:'ID du premier salon textuel du serveur (ordre Discord)',
4
5
  channel_id:'ID du salon courant',channel_name:'Nom du salon',channel_mention:'Mention du salon',channel_topic:'Sujet du salon',channel_type:'Type du salon',channel_parent_id:'ID de la catégorie',
5
6
  guild_id:'ID du serveur',guild_name:'Nom du serveur',guild_icon:'Icône du serveur',guild_owner_id:'Propriétaire du serveur',guild_member_count:'Nombre de membres',guild_locale:'Langue du serveur',
6
7
  command_name:'Nom de la commande',interaction_id:'ID de l’interaction',bot_id:'ID du bot',bot_name:'Nom du bot',timestamp:'Horodatage Unix',date_iso:'Date actuelle (ISO)',
7
8
  };
9
+ // Only guild text/announcement channels can be direct message destinations.
10
+ // Keep IDs as strings: Discord snowflakes exceed JavaScript's safe integer range.
11
+ export function firstChannelId(guild) {
12
+ const channels = [...(guild?.channels?.cache?.values?.() || [])]
13
+ .filter(channel => channel && [0, 5].includes(channel.type) && typeof channel.id === 'string' && (!channel.guildId || channel.guildId === guild.id));
14
+ channels.sort((a,b) => {
15
+ const position = (a.position ?? a.rawPosition ?? 0) - (b.position ?? b.rawPosition ?? 0);
16
+ if (position) return position;
17
+ const left = BigInt(a.id), right = BigInt(b.id);
18
+ return left < right ? -1 : left > right ? 1 : 0;
19
+ });
20
+ return channels[0]?.id || '';
21
+ }
8
22
  export function contextVariables(interaction, client) {
9
23
  const u=interaction.user||{},m=interaction.member||{},c=interaction.channel||{},g=interaction.guild||{};
10
24
  const iso = value => value ? new Date(value).toISOString() : '';
11
25
  const unix = value => value ? String(Math.floor(value/1000)) : '';
12
26
  const roles=m.roles?.cache ? [...m.roles.cache.values()].filter(r=>r.id!==g.id) : [];
13
- return {user_id:u.id||'',user_name:u.username||'',user_display_name:m.displayName||m.nick||u.globalName||u.username||'',user_mention:u.id?`<@${u.id}>`:'',user_avatar:u.displayAvatarURL?.()||'',user_created_at:iso(u.createdTimestamp),user_created_timestamp:unix(u.createdTimestamp),user_bot:String(Boolean(u.bot)),user_locale:interaction.locale||'',member_nickname:m.nickname||m.nick||'',member_joined_at:iso(m.joinedTimestamp),member_joined_timestamp:unix(m.joinedTimestamp),member_roles:roles.map(r=>`<@&${r.id}>`).join(' '),member_role_ids:roles.map(r=>r.id).join(','),member_boost_since:iso(m.premiumSinceTimestamp),channel_id:interaction.channelId||'',channel_name:c.name||'',channel_mention:interaction.channelId?`<#${interaction.channelId}>`:'',channel_topic:c.topic||'',channel_type:String(c.type??''),channel_parent_id:c.parentId||'',guild_id:interaction.guildId||'',guild_name:g.name||'',guild_icon:g.iconURL?.()||'',guild_owner_id:g.ownerId||'',guild_member_count:String(g.memberCount??''),guild_locale:g.preferredLocale||'',command_name:interaction.commandName||'',interaction_id:interaction.id||'',bot_id:client.user?.id||'',bot_name:client.user?.username||'',timestamp:unix(Date.now()),date_iso:new Date().toISOString()};
27
+ return {first_channel:firstChannelId(interaction.guild),user_id:u.id||'',user_name:u.username||'',user_display_name:m.displayName||m.nick||u.globalName||u.username||'',user_mention:u.id?`<@${u.id}>`:'',user_avatar:u.displayAvatarURL?.()||'',user_created_at:iso(u.createdTimestamp),user_created_timestamp:unix(u.createdTimestamp),user_bot:String(Boolean(u.bot)),user_locale:interaction.locale||'',member_nickname:m.nickname||m.nick||'',member_joined_at:iso(m.joinedTimestamp),member_joined_timestamp:unix(m.joinedTimestamp),member_roles:roles.map(r=>`<@&${r.id}>`).join(' '),member_role_ids:roles.map(r=>r.id).join(','),member_boost_since:iso(m.premiumSinceTimestamp),channel_id:interaction.channelId||'',channel_name:c.name||'',channel_mention:interaction.channelId?`<#${interaction.channelId}>`:'',channel_topic:c.topic||'',channel_type:String(c.type??''),channel_parent_id:c.parentId||'',guild_id:interaction.guildId||'',guild_name:g.name||'',guild_icon:g.iconURL?.()||'',guild_owner_id:g.ownerId||'',guild_member_count:String(g.memberCount??''),guild_locale:g.preferredLocale||'',command_name:interaction.commandName||'',interaction_id:interaction.id||'',bot_id:client.user?.id||'',bot_name:client.user?.username||'',timestamp:unix(Date.now()),date_iso:new Date().toISOString()};
14
28
  }
@@ -1,64 +1,128 @@
1
- import { DISCORD_BLOCKS, validateDiscordBlock } from './discord-blocks.mjs';
2
-
3
- async function execute(kind, context, input) {
4
- const args=validateDiscordBlock(kind,input,false), {interaction}=context, guild=interaction.guild;
5
- if (!guild || !interaction.guildId) throw new Error('Cette action nécessite un serveur Discord.');
6
- const actor=await guild.members.fetch({user:interaction.user.id,force:true});
7
- const bot=await guild.members.fetchMe({force:true});
8
- const permission=DISCORD_BLOCKS[kind].permission;
9
- if (!actor.permissions.has(permission) || !bot.permissions.has(permission)) throw new Error(`Permission requise : ${permission}.`);
10
- const outranks = (member, target) => member.id===guild.ownerId || member.roles.highest.comparePositionTo(target.roles.highest)>0;
11
- let target;
12
- if (args.memberId && kind!=='unbanMember') {
13
- target=await guild.members.fetch({user:args.memberId,force:true}).catch(error => { if(kind==='banMember' && error.code===10007) return null; throw error; });
14
- if (!['addRole','removeRole'].includes(kind) && (args.memberId===guild.ownerId || args.memberId===actor.id || args.memberId===bot.id || (target && (!outranks(actor,target)||!outranks(bot,target))))) throw new Error('La hiérarchie des membres interdit cette action.');
15
- }
16
- let role;
17
- if(args.roleId) {
18
- role=await guild.roles.fetch(args.roleId);
19
- if(!role || role.managed || role.id===guild.id || bot.roles.highest.comparePositionTo(role)<=0 || (actor.id!==guild.ownerId && actor.roles.highest.comparePositionTo(role)<=0)) throw new Error('Rôle inaccessible ou supérieur à votre rôle.');
20
- }
21
- if(kind==='banMember') return guild.members.ban(args.memberId,{reason:args.reason,deleteMessageSeconds:args.deleteMessageSeconds});
22
- if(kind==='unbanMember') return guild.members.unban(args.memberId,args.reason);
23
- if(kind==='kickMember') return target.kick(args.reason);
24
- if(kind==='timeoutMember'||kind==='removeTimeout') return target.timeout(kind==='removeTimeout'?null:args.seconds*1000,args.reason);
25
- if(kind==='nicknameMember') return target.setNickname(args.nickname||null,args.reason);
26
- if(kind==='addRole'||kind==='removeRole') return target.roles[kind==='addRole'?'add':'remove'](role,args.reason);
27
- if(kind==='deleteRole') return role.delete(args.reason);
28
- if(kind==='createRole') {
29
- if(!actor.permissions.has(args.permissions)||!bot.permissions.has(args.permissions)) throw new Error('Vous ne pouvez pas accorder ces permissions.');
30
- const {icon,unicodeEmoji,color,colorStyle,secondaryColor,...rest}=args;
31
- const colors=colorStyle==='holographic'?{primaryColor:11127295,secondaryColor:16759788,tertiaryColor:16761760}:{primaryColor:color||0,...(colorStyle==='gradient'?{secondaryColor}: {})};
32
- return guild.roles.create({...rest,colors,...(icon?{icon}:{}),...(unicodeEmoji?{unicodeEmoji}:{})});
33
- }
34
- if(kind==='createChannel') {
35
- const {type,parent,rtcRegion,availableTags,defaultReactionEmoji,defaultSortOrder,defaultForumLayout,defaultAutoArchiveDuration,defaultThreadRateLimitPerUser,rateLimitPerUser,bitrate,userLimit,videoQualityMode,topic,permissionOverwrites,...rest}=args;
36
- const channelType=Number(type);
37
- // Custom overwrites require ManageRoles as well; never grant permissions the caller lacks.
38
- if(permissionOverwrites.length && (!actor.permissions.has('ManageRoles')||!bot.permissions.has('ManageRoles'))) throw new Error('ManageRoles est requis pour les permissions du salon.');
39
- for(const overwrite of permissionOverwrites) {
40
- if(!/^\d{17,20}$/.test(overwrite.id||'') || ![0,1].includes(overwrite.type) || !Array.isArray(overwrite.allow)||!Array.isArray(overwrite.deny)||!actor.permissions.has(overwrite.allow)||!bot.permissions.has(overwrite.allow)) throw new Error('Permissions de salon invalides ou non autorisées.');
41
- }
42
- if(parent) { const category=await guild.channels.fetch(parent); if(!category || category.type!==4 || !category.permissionsFor(actor)?.has(['ViewChannel','ManageChannels'])) throw new Error('Catégorie inaccessible.'); }
43
- return guild.channels.create({...rest,type:channelType,permissionOverwrites,...([0,2,5,13,15,16].includes(channelType)?{rateLimitPerUser}:{}),...([0,5,15,16].includes(channelType)?{defaultAutoArchiveDuration:Number(defaultAutoArchiveDuration),defaultThreadRateLimitPerUser}:{}),...(parent?{parent}:{}),...([0,5,15,16].includes(channelType)?{topic}:{}),...([2,13].includes(channelType)?{bitrate,userLimit,videoQualityMode:Number(videoQualityMode),rtcRegion:rtcRegion||null}:{}),...([15,16].includes(channelType)?{availableTags,defaultReactionEmoji,defaultSortOrder:Number(defaultSortOrder),defaultForumLayout:Number(defaultForumLayout)}:{})});
44
- }
45
- const destination=await guild.channels.fetch(args.channelId);
46
- if(!destination || !destination.permissionsFor(actor)?.has(['ViewChannel',permission]) || !destination.permissionsFor(bot)?.has(permission)) throw new Error('Salon inaccessible ou permissions insuffisantes.');
47
- if(['addReaction','removeReaction','editMessage'].includes(kind)) {
48
- if(!destination.isTextBased?.() || !destination.messages || !destination.permissionsFor(actor)?.has('ReadMessageHistory') || !destination.permissionsFor(bot)?.has(['ViewChannel','ReadMessageHistory'])) throw new Error('Message inaccessible.');
49
- const message=await destination.messages.fetch(args.messageId);
50
- if(kind==='editMessage') {
51
- if(message.author.id!==bot.id) throw new Error('Seuls les messages de ce bot peuvent être modifiés.');
52
- return message.edit({content:args.content,allowedMentions:{parse:[],repliedUser:false}});
53
- }
54
- const emoji=args.emoji.replace(/^<a?:[^:]+:(\d{17,20})>$/, '$1');
55
- if(kind==='addReaction') return message.react(emoji);
56
- const reaction=message.reactions.resolve(emoji);
57
- if(!reaction) throw new Error('Réaction introuvable.');
58
- return reaction.users.remove(args.userId || bot.id);
59
- }
60
- if(kind==='deleteChannel') return destination.delete(args.reason);
61
- if(kind==='createInvite') return destination.createInvite(args);
62
- throw new Error('Action inconnue.');
63
- }
64
- export const DISCORD_ACTIONS = Object.fromEntries(Object.keys(DISCORD_BLOCKS).map(kind=>[kind,(context,args)=>execute(kind,context,args)]));
1
+ import { DISCORD_BLOCKS, validateDiscordBlock } from './discord-blocks.mjs';
2
+
3
+
4
+
5
+ async function execute(kind, context, input) {
6
+
7
+ const args=validateDiscordBlock(kind,input,false), {interaction}=context, guild=interaction.guild;
8
+
9
+ if (!guild || !interaction.guildId) throw new Error('Cette action nécessite un serveur Discord.');
10
+
11
+ const actor=interaction.eventTrigger?await guild.members.fetchMe({force:true}):await guild.members.fetch({user:interaction.user.id,force:true});
12
+
13
+ const bot=await guild.members.fetchMe({force:true});
14
+
15
+ const permission=DISCORD_BLOCKS[kind].permission;
16
+
17
+ if (!actor.permissions.has(permission) || !bot.permissions.has(permission)) throw new Error(`Permission requise : ${permission}.`);
18
+
19
+ const outranks = (member, target) => member.id===guild.ownerId || member.roles.highest.comparePositionTo(target.roles.highest)>0;
20
+
21
+ let target;
22
+
23
+ if (args.memberId && kind!=='unbanMember') {
24
+
25
+ target=await guild.members.fetch({user:args.memberId,force:true}).catch(error => { if(kind==='banMember' && error.code===10007) return null; throw error; });
26
+
27
+ if (!['addRole','removeRole'].includes(kind) && (args.memberId===guild.ownerId || args.memberId===actor.id || args.memberId===bot.id || (target && (!outranks(actor,target)||!outranks(bot,target))))) throw new Error('La hiérarchie des membres interdit cette action.');
28
+
29
+ }
30
+
31
+ let role;
32
+
33
+ if(args.roleId) {
34
+
35
+ role=await guild.roles.fetch(args.roleId);
36
+
37
+ if(!role || role.managed || role.id===guild.id || bot.roles.highest.comparePositionTo(role)<=0 || (actor.id!==guild.ownerId && actor.roles.highest.comparePositionTo(role)<=0)) throw new Error('Rôle inaccessible ou supérieur à votre rôle.');
38
+
39
+ }
40
+
41
+ if(kind==='banMember') return guild.members.ban(args.memberId,{reason:args.reason,deleteMessageSeconds:args.deleteMessageSeconds});
42
+
43
+ if(kind==='unbanMember') return guild.members.unban(args.memberId,args.reason);
44
+
45
+ if(kind==='kickMember') return target.kick(args.reason);
46
+
47
+ if(kind==='timeoutMember'||kind==='removeTimeout') return target.timeout(kind==='removeTimeout'?null:args.seconds*1000,args.reason);
48
+
49
+ if(kind==='nicknameMember') return target.setNickname(args.nickname||null,args.reason);
50
+
51
+ if(kind==='addRole'||kind==='removeRole') return target.roles[kind==='addRole'?'add':'remove'](role,args.reason);
52
+
53
+ if(kind==='deleteRole') return role.delete(args.reason);
54
+
55
+ if(kind==='createRole') {
56
+
57
+ if(!actor.permissions.has(args.permissions)||!bot.permissions.has(args.permissions)) throw new Error('Vous ne pouvez pas accorder ces permissions.');
58
+
59
+ const {icon,unicodeEmoji,color,colorStyle,secondaryColor,...rest}=args;
60
+
61
+ const colors=colorStyle==='holographic'?{primaryColor:11127295,secondaryColor:16759788,tertiaryColor:16761760}:{primaryColor:color||0,...(colorStyle==='gradient'?{secondaryColor}: {})};
62
+
63
+ return guild.roles.create({...rest,colors,...(icon?{icon}:{}),...(unicodeEmoji?{unicodeEmoji}:{})});
64
+
65
+ }
66
+
67
+ if(kind==='createChannel') {
68
+
69
+ const {type,parent,rtcRegion,availableTags,defaultReactionEmoji,defaultSortOrder,defaultForumLayout,defaultAutoArchiveDuration,defaultThreadRateLimitPerUser,rateLimitPerUser,bitrate,userLimit,videoQualityMode,topic,permissionOverwrites,...rest}=args;
70
+
71
+ const channelType=Number(type);
72
+
73
+ // Custom overwrites require ManageRoles as well; never grant permissions the caller lacks.
74
+
75
+ if(permissionOverwrites.length && (!actor.permissions.has('ManageRoles')||!bot.permissions.has('ManageRoles'))) throw new Error('ManageRoles est requis pour les permissions du salon.');
76
+
77
+ for(const overwrite of permissionOverwrites) {
78
+
79
+ if(!/^\d{17,20}$/.test(overwrite.id||'') || ![0,1].includes(overwrite.type) || !Array.isArray(overwrite.allow)||!Array.isArray(overwrite.deny)||!actor.permissions.has(overwrite.allow)||!bot.permissions.has(overwrite.allow)) throw new Error('Permissions de salon invalides ou non autorisées.');
80
+
81
+ }
82
+
83
+ if(parent) { const category=await guild.channels.fetch(parent); if(!category || category.type!==4 || !category.permissionsFor(actor)?.has(['ViewChannel','ManageChannels'])) throw new Error('Catégorie inaccessible.'); }
84
+
85
+ return guild.channels.create({...rest,type:channelType,permissionOverwrites,...([0,2,5,13,15,16].includes(channelType)?{rateLimitPerUser}:{}),...([0,5,15,16].includes(channelType)?{defaultAutoArchiveDuration:Number(defaultAutoArchiveDuration),defaultThreadRateLimitPerUser}:{}),...(parent?{parent}:{}),...([0,5,15,16].includes(channelType)?{topic}:{}),...([2,13].includes(channelType)?{bitrate,userLimit,videoQualityMode:Number(videoQualityMode),rtcRegion:rtcRegion||null}:{}),...([15,16].includes(channelType)?{availableTags,defaultReactionEmoji,defaultSortOrder:Number(defaultSortOrder),defaultForumLayout:Number(defaultForumLayout)}:{})});
86
+
87
+ }
88
+
89
+ const destination=await guild.channels.fetch(args.channelId);
90
+
91
+ if(!destination || !destination.permissionsFor(actor)?.has(['ViewChannel',permission]) || !destination.permissionsFor(bot)?.has(permission)) throw new Error('Salon inaccessible ou permissions insuffisantes.');
92
+
93
+ if(['addReaction','removeReaction','editMessage'].includes(kind)) {
94
+
95
+ if(!destination.isTextBased?.() || !destination.messages || !destination.permissionsFor(actor)?.has('ReadMessageHistory') || !destination.permissionsFor(bot)?.has(['ViewChannel','ReadMessageHistory'])) throw new Error('Message inaccessible.');
96
+
97
+ const message=await destination.messages.fetch(args.messageId);
98
+
99
+ if(kind==='editMessage') {
100
+
101
+ if(message.author.id!==bot.id) throw new Error('Seuls les messages de ce bot peuvent être modifiés.');
102
+
103
+ return message.edit({content:args.content,allowedMentions:{parse:[],repliedUser:false}});
104
+
105
+ }
106
+
107
+ const emoji=args.emoji.replace(/^<a?:[^:]+:(\d{17,20})>$/, '$1');
108
+
109
+ if(kind==='addReaction') return message.react(emoji);
110
+
111
+ const reaction=message.reactions.resolve(emoji);
112
+
113
+ if(!reaction) throw new Error('Réaction introuvable.');
114
+
115
+ return reaction.users.remove(args.userId || bot.id);
116
+
117
+ }
118
+
119
+ if(kind==='deleteChannel') return destination.delete(args.reason);
120
+
121
+ if(kind==='createInvite') return destination.createInvite(args);
122
+
123
+ throw new Error('Action inconnue.');
124
+
125
+ }
126
+
127
+ export const DISCORD_ACTIONS = Object.fromEntries(Object.keys(DISCORD_BLOCKS).map(kind=>[kind,(context,args)=>execute(kind,context,args)]));
128
+
@@ -0,0 +1,36 @@
1
+ import { EVENTS } from './events.mjs';
2
+ import { executeCommand } from './execute.mjs';
3
+ export function eventContext(name,args,client){
4
+ const current=args[name.endsWith('Update')?1:0]||args[0],previous=args[0];
5
+ const reaction=name.startsWith('messageReaction')?args[0]:null;
6
+ const message=reaction?.message||(name.startsWith('message')?current:null);
7
+ const member=name.startsWith('guildMember')?current:message?.member;
8
+ const channel=message?.channel||(name.startsWith('channel')&&name!=='channelDelete'?current:null);
9
+ const guild=message?.guild||member?.guild||current?.guild||(name==='guildCreate'||name==='guildDelete'?current:null);
10
+ const user=reaction?args[1]:message?.author||member?.user||(name==='userUpdate'?current:null);
11
+ return {eventTrigger:name,eventMessage:message,user:user||client.user,member,channel,channelId:channel?.id,guild,guildId:guild?.id,createdTimestamp:Date.now(),
12
+ eventVariables:{event_name:name,message_id:message?.id||'',message_content:message?.content||'',previous_message_content:name==='messageUpdate'?previous?.content||'':'',reaction_emoji:reaction?.emoji?.id||reaction?.emoji?.name||'',previous_channel_name:name==='channelUpdate'?previous?.name||'':''}};
13
+ }
14
+ export function registerEvents(client,graphs,options={}){
15
+ const registrations=[], budgets=new Map();
16
+ for(const name of new Set(graphs.map(graph=>graph.trigger.event))){
17
+ if(!EVENTS[name])throw Error('Événement inconnu.');
18
+ const listener=async(...args)=>{
19
+ for(const graph of graphs.filter(graph=>graph.trigger.event===name)){
20
+ try{
21
+ const context=eventContext(name,args,client);
22
+ if(graph.trigger.ignoreBots!==false&&context.user?.bot&&context.user!==client.user)continue;
23
+ // Never recursively trigger an automation on a message/reaction produced by this bot.
24
+ if((name.startsWith('message'))&&context.user?.id===client.user?.id)continue;
25
+ if(name==='guildDelete')context.guild=null; // The bot no longer has access.
26
+ const now=Date.now();let budget=budgets.get(graph);if(!budget||now-budget.since>=10000){budget={since:now,count:0,active:0};budgets.set(graph,budget);}
27
+ if(budget.count>=20||budget.active>=4){options.onEventError?.(name,new Error('EVENT_RATE_LIMIT'));continue;}
28
+ budget.count++;budget.active++;
29
+ try{await executeCommand(graph,context,client,{...options,contextVariables:context.eventVariables});}finally{budget.active--;}
30
+ }catch(error){options.onEventError?.(name,error);console.error(`Échec événement ${name} (${error.name}).`);}
31
+ }
32
+ };
33
+ client.on(name,listener);registrations.push([name,listener]);
34
+ }
35
+ return ()=>{for(const [name,listener]of registrations)client.off(name,listener);};
36
+ }
@@ -0,0 +1,30 @@
1
+ // Event graphs share the native command graph format; triggers stay explicit.
2
+ const e=(label,group,channel=false,reply=false,intents=[])=>({label,group,channel,reply,intents});
3
+ export const EVENTS={
4
+ messageCreate:e('Message envoyé','Messages',true,true,['GuildMessages','MessageContent']),
5
+ messageUpdate:e('Message modifié','Messages',true,false,['GuildMessages','MessageContent']),
6
+ messageDelete:e('Message supprimé','Messages',true,false,['GuildMessages']),
7
+ messageReactionAdd:e('Réaction ajoutée','Réactions',true,false,['GuildMessageReactions']),
8
+ messageReactionRemove:e('Réaction retirée','Réactions',true,false,['GuildMessageReactions']),
9
+ channelCreate:e('Salon créé','Salons',true),channelUpdate:e('Salon modifié','Salons',true),channelDelete:e('Salon supprimé','Salons'),
10
+ guildMemberAdd:e('Membre arrivé','Membres',false,false,['GuildMembers']),guildMemberRemove:e('Membre parti','Membres',false,false,['GuildMembers']),guildMemberUpdate:e('Membre modifié','Membres',false,false,['GuildMembers']),
11
+ userUpdate:e('Utilisateur modifié','Utilisateurs'),
12
+ clientReady:e('Bot prêt / reconnecté','Bot'),guildCreate:e('Bot ajouté à un serveur','Bot'),guildDelete:e('Bot retiré d’un serveur','Bot'),
13
+ };
14
+ export const EVENT_VARIABLES={event_name:'Nom de l’événement',message_id:'ID du message',message_content:'Contenu du message',previous_message_content:'Ancien contenu du message',reaction_emoji:'Emoji de la réaction',previous_channel_name:'Ancien nom du salon'};
15
+ export function validateEventGraph(graph){
16
+ if(!graph?.trigger)return;
17
+ const trigger=graph.trigger,cap=EVENTS[trigger.event];
18
+ if(trigger.kind!=='event'||!cap)throw Error('Déclencheur événement inconnu.');
19
+ if(trigger.ignoreBots!==undefined&&typeof trigger.ignoreBots!=='boolean')throw Error('Filtre des bots invalide.');
20
+ for(const block of graph.blocks||[]){
21
+ if(['textOption','userOption','channelOption'].includes(block.type))throw Error('Les options slash ne sont pas disponibles dans les événements.');
22
+ if(block.type==='sendMessage'){
23
+ const c=block.config||{};
24
+ if(c.ephemeral)throw Error('Les réponses éphémères sont réservées aux commandes et boutons.');
25
+ if((c.sendMode||'reply')==='reply'&&(c.replyMode||'command')==='command'&&!cap.reply)throw Error('Cet événement ne permet pas de répondre au message déclencheur. Choisissez un salon, un MP ou un message par ID.');
26
+ if(c.sendMode==='channel'&&(c.channelMode||'current')==='current'&&!cap.channel)throw Error('Cet événement ne fournit pas de salon courant. Choisissez un salon par ID.');
27
+ if(c.sendMode==='dm'&&(c.dmMode||'author')==='author'&&['clientReady','guildCreate','guildDelete','channelCreate','channelUpdate','channelDelete'].includes(trigger.event))throw Error('Cet événement ne fournit pas de destinataire. Choisissez un utilisateur par ID.');
28
+ }
29
+ }
30
+ }
@@ -81,7 +81,7 @@ async function executeAdmitted(command, interaction, client, options) {
81
81
 
82
82
  validate(command.calls); validate(errorCalls);
83
83
 
84
- const optionValues = { ...(options.buttonSnapshot?.variables || resolveOptions(command.options, interaction)), ...contextVariables(interaction, client) };
84
+ const optionValues = { ...(options.buttonSnapshot?.variables || resolveOptions(command.options, interaction)), ...contextVariables(interaction, client), ...(options.contextVariables || {}) };
85
85
 
86
86
  optionValues.command_name=command.name;
87
87
  const usesCustomVariables = Boolean(JSON.stringify(command).includes('randomCondition') || referencedVariables(command).length || JSON.stringify(command).includes('"fn":"setVariable"') || JSON.stringify(command).includes('"fn":"deleteVariable"'));
@@ -90,7 +90,7 @@ async function executeAdmitted(command, interaction, client, options) {
90
90
  const firstReply = flatten(command.calls).find(call => call.fn === 'sendMessage' && call.args.sendMode === 'reply' && call.args.replyMode === 'command');
91
91
  return !firstReply || Boolean(firstReply.args.ephemeral);
92
92
  })();
93
- await interaction.deferReply(privateAck ? { flags: 64 } : {});
93
+ if (!interaction.eventTrigger) await interaction.deferReply(privateAck ? { flags: 64 } : {});
94
94
  if (options.signal.aborted) throw options.signal.reason;
95
95
  const context = { interaction, client, messages: new Map(options.buttonSnapshot?.messages), channels: new Map(options.buttonSnapshot?.channels), hasCommandReply: false, signal: options.signal || AbortSignal.timeout(870000), variableStore: options.variableStore, variableReplyMode: usesCustomVariables };
96
96
  const definitions = options.variableDefinitions || [];
@@ -156,7 +156,7 @@ async function executeAdmitted(command, interaction, client, options) {
156
156
 
157
157
  // For commands that only send externally, remove the private loading acknowledgement.
158
158
 
159
- if (!context.hasCommandReply || context.removeDeferredReply) await interaction.deleteReply();
159
+ if (!interaction.eventTrigger && (!context.hasCommandReply || context.removeDeferredReply)) await interaction.deleteReply();
160
160
 
161
161
  return context.messages;
162
162
 
@@ -13,13 +13,19 @@ import { runtimeError, validateMessage } from './errors.mjs';
13
13
 
14
14
  async function authorizeChannel(interaction, channel) {
15
15
 
16
+ if (interaction.eventTrigger && ['clientReady','userUpdate'].includes(interaction.eventTrigger)) {
17
+ if(!channel?.guild) throw new Error('Salon de serveur indisponible.');
18
+ const bot=await channel.guild.members.fetchMe({force:true});
19
+ if(!channel.permissionsFor(bot)?.has(['ViewChannel',channel.isThread?.()?'SendMessagesInThreads':'SendMessages']))throw new Error('Permissions du bot insuffisantes.');
20
+ return;
21
+ }
16
22
  if (!interaction.guildId || channel?.guildId !== interaction.guildId || !interaction.user?.id || !interaction.guild) {
17
23
 
18
24
  throw new Error('Destination Discord non autorisée.');
19
25
 
20
26
  }
21
27
 
22
- const member = await interaction.guild.members.fetch({ user: interaction.user.id, force: true });
28
+ const member = interaction.eventTrigger ? await interaction.guild.members.fetchMe({force:true}) : await interaction.guild.members.fetch({ user: interaction.user.id, force: true });
23
29
 
24
30
  const permissions = channel.permissionsFor(member);
25
31
 
@@ -55,6 +61,11 @@ async function sendMessage(context, parameters) {
55
61
 
56
62
  const { interaction, client, messages } = context;
57
63
 
64
+ if (interaction.eventTrigger && config.ephemeral) throw runtimeError('MESSAGE_INVALID');
65
+ if (interaction.eventTrigger && config.sendMode === 'reply' && config.replyMode === 'command') {
66
+ if (interaction.eventTrigger !== 'messageCreate' || !interaction.eventMessage?.reply) throw runtimeError('MESSAGE_NOT_FOUND');
67
+ return interaction.eventMessage.reply(payload);
68
+ }
58
69
  if (config.sendMode === 'channel') {
59
70
 
60
71
  const channel = config.channelMode === 'created' ? context.channels.get(config.createdChannelBlockId) : config.channelMode === 'id' ? await client.channels.fetch(config.channelId) : interaction.channel;
@@ -80,7 +80,7 @@ export function messageConfigError(raw, nodes, allowVariables = true) {
80
80
 
81
81
  for (const url of [embed.url, embed.author?.url, embed.author?.icon_url, embed.footer?.icon_url, embed.image?.url, embed.thumbnail?.url]) {
82
82
 
83
- const previewUrl = allowVariables && typeof url === 'string' ? url.replace(/\{[a-zA-Z0-9_-]+\}/g, 'https://example.com') : url;
83
+ const previewUrl = allowVariables && typeof url === 'string' ? url.replace(/\{[a-zA-Z0-9_.-]+\}/g, 'https://example.com') : url;
84
84
 
85
85
  if (url && !safeMessageUrl(previewUrl)) return `Utilisez une URL http ou https valide dans l’embed ${index + 1}.`;
86
86
 
@@ -102,7 +102,7 @@ export function messageConfigError(raw, nodes, allowVariables = true) {
102
102
 
103
103
  const needsId = config.sendMode === 'reply' && config.replyMode === 'message' ? config.messageId : config.sendMode === 'channel' && config.channelMode === 'id' ? config.channelId : config.sendMode === 'dm' && config.dmMode === 'id' ? config.userId : null;
104
104
 
105
- if (needsId !== null && !/^\d{17,20}$/.test(needsId) && !(allowVariables && /^\{[a-zA-Z0-9_-]+\}$/.test(needsId))) return 'Renseignez un ID Discord valide (17 à 20 chiffres).';
105
+ if (needsId !== null && !/^\d{17,20}$/.test(needsId) && !(allowVariables && /^\{[a-zA-Z0-9_.-]+\}$/.test(needsId))) return 'Renseignez un ID Discord valide (17 à 20 chiffres).';
106
106
 
107
107
  return '';
108
108
 
package/templates/bot.mjs CHANGED
@@ -1,7 +1,9 @@
1
+ import { EVENTS } from './runtime/events.mjs';
2
+ import { registerEvents } from './runtime/event-runtime.mjs';
1
3
  import { fileURLToPath } from 'node:url';
2
4
  import { VariableStore } from './runtime/variable-store.mjs';
3
5
  import { readFile } from 'node:fs/promises';
4
- import { Client, GatewayIntentBits, REST, Routes } from 'discord.js';
6
+ import { Client, GatewayIntentBits, Partials, REST, Routes } from 'discord.js';
5
7
  import { executeCommand, executeButtonInteraction } from './runtime/execute.mjs';
6
8
 
7
9
  const plan = JSON.parse(await readFile(new URL('./plan.json', import.meta.url), 'utf8'));
@@ -16,14 +18,17 @@ if (process.argv.includes('--deploy')) {
16
18
  if (guildId && !/^\d{17,20}$/.test(guildId)) throw new Error('DISCORD_GUILD_ID invalide.');
17
19
  const rest = new REST({ version: '10' }).setToken(token);
18
20
  const route = guildId ? Routes.applicationGuildCommands(applicationId, guildId) : Routes.applicationCommands(applicationId);
19
- await rest.put(route, { body: plan.commands.map(command => ({ name: command.name, description: command.description, type: 1, options: (command.options || []).map(({ type, name, description, required }) => ({ type, name, description, required, ...(type === 3 ? { max_length: 6000 } : {}) })) })) });
21
+ await rest.put(route, { body: plan.commands.filter(command=>!command.trigger).map(command => ({ name: command.name, description: command.description, type: 1, options: (command.options || []).map(({ type, name, description, required }) => ({ type, name, description, required, ...(type === 3 ? { max_length: 6000 } : {}) })) })) });
20
22
  console.log(`${plan.commands.length} commande(s) enregistrée(s).`);
21
23
  } else {
22
24
  const variableStore = plan.variableDefinitions?.length || plan.variableDeletions?.length ? new VariableStore({ file: process.env.OFTONE_VARIABLES_PATH || fileURLToPath(new URL('./data/variables.sqlite', import.meta.url)), definitions: plan.variableDefinitions || [], deletions: plan.variableDeletions || [] }) : null;
23
25
  if (variableStore) await variableStore.ready;
24
26
  const variableOptions = { variableStore, variableDefinitions: plan.variableDefinitions || [] };
25
- const client = new Client({ intents: [GatewayIntentBits.Guilds] });
26
- const commands = new Map(plan.commands.map(command => [command.name, command]));
27
+ const eventGraphs=plan.commands.filter(command=>command.trigger?.kind==='event');
28
+ const intentNames=new Set(['Guilds',...eventGraphs.flatMap(graph=>EVENTS[graph.trigger.event].intents)]);
29
+ const client = new Client({ intents:[...intentNames].map(name=>GatewayIntentBits[name]), partials:[Partials.Message,Partials.Channel,Partials.Reaction,Partials.User,Partials.GuildMember] });
30
+ registerEvents(client,eventGraphs,variableOptions);
31
+ const commands = new Map(plan.commands.filter(command=>!command.trigger).map(command => [command.name, command]));
27
32
  client.once('ready', () => console.log(`Bot connecté : ${client.user.tag}`));
28
33
  client.on('interactionCreate', async interaction => {
29
34
  if(interaction.isButton?.()) {
package/version.mjs CHANGED
@@ -1 +1 @@
1
- export const AGENT_VERSION = '0.5.7';
1
+ export const AGENT_VERSION = '0.5.8';