fbp-language 1.1.3 → 1.1.6

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/README.md CHANGED
@@ -23,7 +23,7 @@ Puis, dans le `package.json` de ton projet :
23
23
  "start": "fbp run lancer"
24
24
  },
25
25
  "dependencies": {
26
- "fbp-language": "^1.1.3"
26
+ "fbp-language": "^1.1.6"
27
27
  }
28
28
  }
29
29
  ```
@@ -127,3 +127,76 @@ npm test
127
127
  ```
128
128
 
129
129
  Les fichiers d'essai internes se trouvent dans `tests/fixtures`. Ils ne font pas partie du paquet publié.
130
+
131
+
132
+ ## Informations Discord dynamiques (v1.1.6)
133
+
134
+ Les champs d'un embed peuvent récupérer les informations accessibles au moment où la commande est utilisée. Exemple :
135
+
136
+ ```fbp
137
+ discordConnexion()
138
+ discordCommandeEmbed("serveur-info", "Informations du serveur", "Données actuelles", "#5865F2")
139
+ discordChampDynamique("serveur-info", "Nom", "discordServeur.nom")
140
+ discordChampDynamique("serveur-info", "Identifiant", "discordServeur.id")
141
+ discordChampDynamique("serveur-info", "Création", "discordServeur.creation")
142
+ discordChampDynamique("serveur-info", "Membres", "discordMembres.nombre")
143
+ discordChampDynamique("serveur-info", "Rôles", "discordRoles.nombre")
144
+ discordChampDynamique("serveur-info", "Salons", "discordSalons.nombre")
145
+ discordChampDynamique("serveur-info", "Latence", "discordLatence")
146
+ discordOptionUtilisateur("serveur-info", "membre", "Membre à afficher")
147
+ discordChampDynamique("serveur-info", "Profil", "discordProfil.nom")
148
+ discordChampDynamique("serveur-info", "Avatar", "discordAvatar")
149
+ ```
150
+
151
+ Sources disponibles : `discordServeur.nom`, `discordServeur.id`, `discordServeur.creation`, `discordMembres.nombre`, `discordProfil.nom`, `discordProfil.id`, `discordProfil.avatar`, `discordRoles.nombre`, `discordSalons.nombre`, `discordStatistiques.membres`, `discordStatistiques.roles`, `discordStatistiques.salons`, `discordLatence` et `discordAvatar`. `discordOptionUtilisateur(nomCommande, nomOption, description)` ajoute un sélecteur facultatif de membre à une commande embed. Les données accessibles dépendent des permissions et du cache Discord du bot.
152
+
153
+
154
+ ## Options des commandes slash (v1.1.6)
155
+
156
+ Les commandes slash peuvent maintenant recevoir des options texte, nombre, entier ou booléen. Le dernier argument indique si l'option est obligatoire ; il est facultatif et vaut `false` par défaut.
157
+
158
+ ```fbp
159
+ discordConnexion()
160
+ discordCommande("bonjour", "Salut {{prenom}} !")
161
+ discordOptionTexte("bonjour", "prenom", "Ton prénom", true)
162
+ discordOptionEntier("bonjour", "age", "Ton âge")
163
+ discordOptionNombre("bonjour", "note", "Une valeur décimale")
164
+ discordOptionBooleen("bonjour", "confirme", "Confirmer ?")
165
+ ```
166
+
167
+ Dans une réponse texte, `{{prenom}}` est remplacé par la valeur saisie. Pour les embeds, les champs dynamiques peuvent lire une option avec `discordChampDynamique("commande", "Champ", "option.prenom")` (ou `discordOption.prenom`). Les options absentes ne sont pas remplacées et une option obligatoire inaccessible produit une réponse d'erreur.
168
+
169
+ Fonctions disponibles :
170
+
171
+ - `discordOptionTexte(commande, nom, description, obligatoire)`
172
+ - `discordOptionNombre(commande, nom, description, obligatoire)`
173
+ - `discordOptionEntier(commande, nom, description, obligatoire)`
174
+ - `discordOptionBooleen(commande, nom, description, obligatoire)`
175
+ - `discordOptionUtilisateur(commande, nom, description)` pour le sélecteur de membre existant des commandes embed.
176
+
177
+ Les noms d'options doivent respecter les règles Discord (minuscules, chiffres, tirets et underscores, 1 à 32 caractères). Une commande peut avoir jusqu'à 25 options. Les types d'options sont déclarés lors de l'enregistrement des commandes ; redémarre le bot après avoir modifié leur définition. Le bot a besoin du scope `applications.commands` et de l'accès au serveur concerné. Aucun intent privilégié supplémentaire n'est requis pour ces options.
178
+
179
+
180
+ ## Choix prédéfinis des options (v1.1.6)
181
+
182
+ Pour limiter les réponses possibles d'une option, ajoute des choix après avoir déclaré l'option :
183
+
184
+ ```fbp
185
+ discordConnexion()
186
+ discordCommande("couleur", "Couleur choisie : {{couleur}}")
187
+ discordOptionTexte("couleur", "couleur", "Choisis une couleur", true)
188
+ discordChoixTexte("couleur", "couleur", "Bleu", "bleu")
189
+ discordChoixTexte("couleur", "couleur", "Rouge", "rouge")
190
+
191
+ discordCommande("difficulte", "Niveau choisi : {{niveau}}")
192
+ discordOptionEntier("difficulte", "niveau", "Choisis un niveau", true)
193
+ discordChoixEntier("difficulte", "niveau", "Facile", 1)
194
+ discordChoixEntier("difficulte", "niveau", "Normal", 2)
195
+ discordChoixEntier("difficulte", "niveau", "Difficile", 3)
196
+ ```
197
+
198
+ - `discordChoixTexte(commande, option, nomChoix, valeur)` ajoute un choix à une option texte.
199
+ - `discordChoixEntier(commande, option, nomChoix, valeur)` ajoute un choix à une option entière.
200
+ - Déclare toujours l'option avant ses choix.
201
+ - Une option peut avoir jusqu'à 25 choix. Les noms de choix sont limités à 100 caractères.
202
+ - Les choix sont présentés par Discord sous forme de liste et la valeur choisie est récupérable comme les autres options.
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
9
  const compiler_1 = require("./compiler");
10
10
  const errors_1 = require("./errors");
11
- const VERSION = "1.1.3";
11
+ const VERSION = "1.1.6";
12
12
  function help() {
13
13
  console.log(`FBP — French Bot Programming
14
14
 
package/dist/compiler.js CHANGED
@@ -112,6 +112,29 @@ class FbpCompiler {
112
112
  constructor(options = {}) {
113
113
  this.output = options.onOutput ?? (value => console.log(value));
114
114
  }
115
+ resolveDiscordValue(source, interaction, selectedUser, selectedMember, optionValues) {
116
+ const guild = interaction.guild;
117
+ const values = {
118
+ "discordServeur.nom": guild?.name ?? "Serveur indisponible",
119
+ "discordServeur.id": guild?.id ?? "Indisponible",
120
+ "discordServeur.creation": guild ? "<t:" + Math.floor(guild.createdTimestamp / 1000) + ":D>" : "Indisponible",
121
+ "discordMembres.nombre": guild ? String(guild.memberCount) : "Indisponible",
122
+ "discordProfil.nom": selectedMember?.displayName ?? selectedUser?.username ?? "Sélectionne un membre",
123
+ "discordProfil.id": selectedUser?.id ?? "Sélectionne un membre",
124
+ "discordProfil.avatar": selectedUser?.displayAvatarURL({ size: 256 }) ?? "Sélectionne un membre",
125
+ "discordRoles.nombre": guild ? String(guild.roles.cache.size) : "Indisponible",
126
+ "discordSalons.nombre": guild ? String(guild.channels.cache.size) : "Indisponible",
127
+ "discordStatistiques.membres": guild ? String(guild.memberCount) : "Indisponible",
128
+ "discordStatistiques.roles": guild ? String(guild.roles.cache.size) : "Indisponible",
129
+ "discordStatistiques.salons": guild ? String(guild.channels.cache.size) : "Indisponible",
130
+ "discordLatence": String(Math.max(0, Math.round(interaction.client.ws.ping))) + " ms",
131
+ "discordAvatar": selectedUser?.displayAvatarURL({ size: 256 }) ?? interaction.user.displayAvatarURL({ size: 256 })
132
+ };
133
+ const optionMatch = source.trim().match(/^(?:discordOption|option)\.([a-z0-9_-]{1,32})$/);
134
+ if (optionMatch)
135
+ return optionValues.get(optionMatch[1]) ?? "Option indisponible : " + optionMatch[1];
136
+ return source.trim() in values ? values[source.trim()] : "Source inconnue : " + source.trim();
137
+ }
115
138
  runFile(filePath) {
116
139
  const absolute = node_path_1.default.resolve(filePath);
117
140
  if (!node_fs_1.default.existsSync(absolute))
@@ -188,10 +211,44 @@ class FbpCompiler {
188
211
  this.discordClient = client;
189
212
  client.once("ready", async () => {
190
213
  try {
191
- const commands = [...this.discordCommands.entries()].map(([commandName]) => new discord_js_1.SlashCommandBuilder()
192
- .setName(commandName)
193
- .setDescription(`Commande ${commandName} du bot FBP`)
194
- .toJSON());
214
+ const commands = [...this.discordCommands.entries()].map(([commandName, response]) => {
215
+ const builder = new discord_js_1.SlashCommandBuilder()
216
+ .setName(commandName)
217
+ .setDescription(`Commande ${commandName} du bot FBP`);
218
+ if (response.type === "embed" && response.userOption) {
219
+ builder.addUserOption(option => option.setName(response.userOption.name)
220
+ .setDescription(response.userOption.description)
221
+ .setRequired(false));
222
+ }
223
+ for (const option of response.options ?? []) {
224
+ const add = {
225
+ texte: () => builder.addStringOption(item => {
226
+ item.setName(option.name).setDescription(option.description).setRequired(option.required);
227
+ for (const choice of option.choices ?? [])
228
+ if (typeof choice.value === "string")
229
+ item.addChoices({ name: choice.name, value: choice.value });
230
+ return item;
231
+ }),
232
+ nombre: () => builder.addNumberOption(item => {
233
+ item.setName(option.name).setDescription(option.description).setRequired(option.required);
234
+ for (const choice of option.choices ?? [])
235
+ if (typeof choice.value === "number")
236
+ item.addChoices({ name: choice.name, value: choice.value });
237
+ return item;
238
+ }),
239
+ entier: () => builder.addIntegerOption(item => {
240
+ item.setName(option.name).setDescription(option.description).setRequired(option.required);
241
+ for (const choice of option.choices ?? [])
242
+ if (typeof choice.value === "number")
243
+ item.addChoices({ name: choice.name, value: choice.value });
244
+ return item;
245
+ }),
246
+ booleen: () => builder.addBooleanOption(item => item.setName(option.name).setDescription(option.description).setRequired(option.required))
247
+ }[option.type];
248
+ add();
249
+ }
250
+ return builder.toJSON();
251
+ });
195
252
  const manager = client.application?.commands;
196
253
  if (!manager)
197
254
  throw new Error("L'application Discord n'est pas disponible.");
@@ -217,21 +274,60 @@ class FbpCompiler {
217
274
  if (response === undefined)
218
275
  return;
219
276
  try {
277
+ const optionValues = new Map();
278
+ for (const option of response.options ?? []) {
279
+ let value = null;
280
+ if (option.type === "texte")
281
+ value = interaction.options.getString(option.name);
282
+ else if (option.type === "nombre") {
283
+ const number = interaction.options.getNumber(option.name);
284
+ value = number === null ? null : String(number);
285
+ }
286
+ else if (option.type === "entier") {
287
+ const number = interaction.options.getInteger(option.name);
288
+ value = number === null ? null : String(number);
289
+ }
290
+ else {
291
+ const boolean = interaction.options.getBoolean(option.name);
292
+ value = boolean === null ? null : String(boolean);
293
+ }
294
+ if (value === null && option.required) {
295
+ await interaction.reply({ content: "Option obligatoire indisponible : " + option.name, ephemeral: true });
296
+ return;
297
+ }
298
+ if (value !== null)
299
+ optionValues.set(option.name, value);
300
+ }
220
301
  if (response.type === "text") {
221
- await interaction.reply(response.content);
302
+ const content = response.content.replace(/\{\{([a-z0-9_-]{1,32})\}\}/g, (match, name) => optionValues.has(name) ? optionValues.get(name) : match);
303
+ await interaction.reply(content);
222
304
  }
223
305
  else {
224
306
  const embed = new discord_js_1.EmbedBuilder()
225
307
  .setTitle(response.title)
226
308
  .setDescription(response.description)
227
309
  .setColor(Number.parseInt(response.color.slice(1), 16));
228
- if (response.fields.length)
229
- embed.addFields(response.fields.map(field => ({ name: field.name, value: field.value, inline: false })));
310
+ const guild = interaction.guild;
311
+ const selectedUser = response.userOption ? interaction.options.getUser(response.userOption.name) : null;
312
+ const selectedMember = selectedUser && guild ? await guild.members.fetch(selectedUser.id).catch(() => null) : null;
313
+ if (response.userOption && selectedUser)
314
+ optionValues.set(response.userOption.name, selectedMember?.displayName ?? selectedUser.username);
315
+ const fields = response.fields.map(field => ({
316
+ name: field.name,
317
+ value: field.dynamic ? this.resolveDiscordValue(field.value, interaction, selectedUser, selectedMember, optionValues) : field.value.replace(/\{\{([a-z0-9_-]{1,32})\}\}/g, (match, name) => optionValues.get(name) ?? match),
318
+ inline: false
319
+ }));
320
+ if (fields.length)
321
+ embed.addFields(fields);
230
322
  await interaction.reply({ embeds: [embed] });
231
323
  }
232
324
  }
233
325
  catch (error) {
326
+ const message = "Une erreur est survenue pendant l'exécution de cette commande.";
234
327
  this.output("Erreur Discord : " + (error instanceof Error ? error.message : String(error)));
328
+ if (!interaction.replied && !interaction.deferred) {
329
+ await interaction.reply({ content: message, ephemeral: true }).catch(() => undefined);
330
+ }
235
331
  }
236
332
  });
237
333
  void client.login(token).catch((error) => this.output("Connexion Discord impossible : " + (error instanceof Error ? error.message : String(error))));
@@ -247,7 +343,7 @@ class FbpCompiler {
247
343
  if (!/^[a-z0-9_-]{1,32}$/.test(commandName)) {
248
344
  throw new errors_1.FbpError("Le nom d'une commande slash doit contenir 1 à 32 caractères : lettres minuscules, chiffres, tirets ou underscores.", undefined, line);
249
345
  }
250
- this.discordCommands.set(commandName, { type: "text", content: values[1] });
346
+ this.discordCommands.set(commandName, { type: "text", content: values[1], options: [] });
251
347
  return;
252
348
  }
253
349
  if (name === "discordCommandeEmbed") {
@@ -261,7 +357,96 @@ class FbpCompiler {
261
357
  throw new errors_1.FbpError("Le nom d'une commande slash doit contenir 1 à 32 caractères : lettres minuscules, chiffres, tirets ou underscores.", undefined, line);
262
358
  if (!/^#[0-9a-fA-F]{6}$/.test(color))
263
359
  throw new errors_1.FbpError("La couleur d'un embed doit être au format hexadécimal, par exemple #5865F2.", undefined, line);
264
- this.discordCommands.set(commandName, { type: "embed", title, description, color, fields: [] });
360
+ this.discordCommands.set(commandName, { type: "embed", title, description, color, fields: [], options: [] });
361
+ return;
362
+ }
363
+ if (name === "discordChampDynamique") {
364
+ if (values.length !== 3 || values.some(value => typeof value !== "string")) {
365
+ throw new errors_1.FbpError("Utilisation : discordChampDynamique(nomCommande, nomChamp, source).", undefined, line);
366
+ }
367
+ const [commandName, fieldName, source] = values;
368
+ const command = this.discordCommands.get(commandName);
369
+ if (!command || command.type !== "embed")
370
+ throw new errors_1.FbpError("La commande « " + commandName + " » n'est pas un embed déclaré.", undefined, line);
371
+ if (command.fields.length >= 25)
372
+ throw new errors_1.FbpError("Un embed Discord ne peut pas contenir plus de 25 champs.", undefined, line);
373
+ if (!fieldName.trim() || !source.trim() || fieldName.length > 256 || source.length > 1024)
374
+ throw new errors_1.FbpError("Un champ dynamique doit avoir un nom et une source valides.", undefined, line);
375
+ command.fields.push({ name: fieldName, value: source, dynamic: true });
376
+ return;
377
+ }
378
+ if (name === "discordChoixTexte" || name === "discordChoixEntier") {
379
+ if (values.length !== 4 || values.some(value => typeof value !== "string" && typeof value !== "number")) {
380
+ throw new errors_1.FbpError("Utilisation : discordChoixTexte(nomCommande, nomOption, nomChoix, valeur) ou discordChoixEntier(...).", undefined, line);
381
+ }
382
+ const [commandName, optionName, choiceName, choiceValue] = values;
383
+ if (typeof commandName !== "string" || typeof optionName !== "string" || typeof choiceName !== "string") {
384
+ throw new errors_1.FbpError("Le nom de la commande, de l'option et du choix doivent être du texte.", undefined, line);
385
+ }
386
+ const command = this.discordCommands.get(commandName);
387
+ if (!command)
388
+ throw new errors_1.FbpError("La commande « " + commandName + " » n'est pas déclarée.", undefined, line);
389
+ const option = (command.options ?? []).find(item => item.name === optionName);
390
+ const expectedType = name === "discordChoixTexte" ? "texte" : "entier";
391
+ if (!option || option.type !== expectedType)
392
+ throw new errors_1.FbpError("L'option « " + optionName + " » n'existe pas ou n'a pas le bon type.", undefined, line);
393
+ if (!choiceName.trim() || choiceName.length > 100)
394
+ throw new errors_1.FbpError("Le nom d'un choix doit contenir de 1 à 100 caractères.", undefined, line);
395
+ if (expectedType === "texte" && typeof choiceValue !== "string")
396
+ throw new errors_1.FbpError("La valeur d'un choix texte doit être du texte.", undefined, line);
397
+ if (expectedType === "entier" && (typeof choiceValue !== "number" || !Number.isInteger(choiceValue)))
398
+ throw new errors_1.FbpError("La valeur d'un choix entier doit être un nombre entier.", undefined, line);
399
+ const choices = option.choices ?? (option.choices = []);
400
+ if (choices.some(choice => choice.name === choiceName || choice.value === choiceValue))
401
+ throw new errors_1.FbpError("Ce nom ou cette valeur de choix existe déjà.", undefined, line);
402
+ if (choices.length >= 25)
403
+ throw new errors_1.FbpError("Une option Discord ne peut pas contenir plus de 25 choix.", undefined, line);
404
+ choices.push({ name: choiceName, value: choiceValue });
405
+ return;
406
+ }
407
+ if (name === "discordOptionTexte" || name === "discordOptionNombre" || name === "discordOptionEntier" || name === "discordOptionBooleen") {
408
+ if (values.length < 3 || values.length > 4 || values.some(value => typeof value !== "string" && typeof value !== "boolean")) {
409
+ throw new errors_1.FbpError("Utilisation : discordOptionTexte/Nombre/Entier/Booleen(nomCommande, nomOption, description, obligatoire).", undefined, line);
410
+ }
411
+ const [commandName, optionName, description] = values;
412
+ const required = values.length === 4 ? values[3] : false;
413
+ if (typeof commandName !== "string" || typeof optionName !== "string" || typeof description !== "string" || typeof required !== "boolean") {
414
+ throw new errors_1.FbpError("Les trois premiers arguments doivent être du texte et le quatrième, facultatif, doit être true ou false.", undefined, line);
415
+ }
416
+ const command = this.discordCommands.get(commandName);
417
+ if (!command)
418
+ throw new errors_1.FbpError("La commande « " + commandName + " » n'est pas déclarée.", undefined, line);
419
+ if (!/^[a-z0-9_-]{1,32}$/.test(optionName))
420
+ throw new errors_1.FbpError("Le nom de l'option doit contenir 1 à 32 caractères minuscules, chiffres, tirets ou underscores.", undefined, line);
421
+ if (!description.trim() || description.length > 100)
422
+ throw new errors_1.FbpError("La description de l'option doit contenir 1 à 100 caractères.", undefined, line);
423
+ const options = command.options ?? (command.options = []);
424
+ if (options.some(option => option.name === optionName) || (command.type === "embed" && command.userOption?.name === optionName))
425
+ throw new errors_1.FbpError("L'option « " + optionName + " » existe déjà pour cette commande.", undefined, line);
426
+ if (options.length + (command.type === "embed" && command.userOption ? 1 : 0) >= 25)
427
+ throw new errors_1.FbpError("Une commande slash Discord ne peut pas contenir plus de 25 options.", undefined, line);
428
+ const type = name === "discordOptionTexte" ? "texte" : name === "discordOptionNombre" ? "nombre" : name === "discordOptionEntier" ? "entier" : "booleen";
429
+ options.push({ name: optionName, description, type, required });
430
+ return;
431
+ }
432
+ if (name === "discordOptionUtilisateur") {
433
+ if (values.length !== 3 || values.some(value => typeof value !== "string")) {
434
+ throw new errors_1.FbpError("Utilisation : discordOptionUtilisateur(nomCommande, nomOption, description).", undefined, line);
435
+ }
436
+ const [commandName, optionName, description] = values;
437
+ const command = this.discordCommands.get(commandName);
438
+ if (!command || command.type !== "embed")
439
+ throw new errors_1.FbpError("La commande « " + commandName + " » n'est pas un embed déclaré.", undefined, line);
440
+ if (!/^[a-z0-9_-]{1,32}$/.test(optionName))
441
+ throw new errors_1.FbpError("Le nom de l'option doit contenir 1 à 32 caractères minuscules, chiffres, tirets ou underscores.", undefined, line);
442
+ if (!description.trim() || description.length > 100)
443
+ throw new errors_1.FbpError("La description de l'option doit contenir 1 à 100 caractères.", undefined, line);
444
+ const options = command.options ?? (command.options = []);
445
+ if (options.some(option => option.name === optionName) || command.userOption?.name === optionName)
446
+ throw new errors_1.FbpError("L'option « " + optionName + " » existe déjà pour cette commande.", undefined, line);
447
+ if (options.length + 1 > 25)
448
+ throw new errors_1.FbpError("Une commande slash Discord ne peut pas contenir plus de 25 options.", undefined, line);
449
+ command.userOption = { name: optionName, description };
265
450
  return;
266
451
  }
267
452
  if (name === "discordChamp") {
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "fbp-language",
3
3
  "displayName": "FBP - French Bot Programming",
4
- "description": "Langage FBP avec prise en charge des commandes slash et des embeds Discord.",
5
- "version": "1.1.3",
4
+ "description": "Langage FBP avec options slash Discord, choix prédéfinis et embeds dynamiques.",
5
+ "version": "1.1.6",
6
6
  "type": "commonjs",
7
7
  "main": "dist/compiler.js",
8
8
  "bin": {