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 +1 -1
- package/compile-plan.mjs +3 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/runtime/context-variables.mjs +15 -1
- package/runtime/discord-actions.mjs +128 -64
- package/runtime/event-runtime.mjs +36 -0
- package/runtime/events.mjs +30 -0
- package/runtime/execute.mjs +3 -3
- package/runtime/functions.mjs +12 -1
- package/runtime/message-config.mjs +2 -2
- package/templates/bot.mjs +9 -4
- package/version.mjs +1 -1
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
|
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oft-agt",
|
|
3
|
-
"version": "0.5.
|
|
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.
|
|
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,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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
if (!
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if(kind
|
|
24
|
-
|
|
25
|
-
if(kind==='
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if(!
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
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
|
+
}
|
package/runtime/execute.mjs
CHANGED
|
@@ -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
|
|
package/runtime/functions.mjs
CHANGED
|
@@ -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_
|
|
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_
|
|
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
|
|
26
|
-
const
|
|
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.
|
|
1
|
+
export const AGENT_VERSION = '0.5.8';
|