fbp-language 1.1.4 → 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.4"
26
+ "fbp-language": "^1.1.6"
27
27
  }
28
28
  }
29
29
  ```
@@ -129,7 +129,7 @@ npm test
129
129
  Les fichiers d'essai internes se trouvent dans `tests/fixtures`. Ils ne font pas partie du paquet publié.
130
130
 
131
131
 
132
- ## Informations Discord dynamiques (v1.1.4)
132
+ ## Informations Discord dynamiques (v1.1.6)
133
133
 
134
134
  Les champs d'un embed peuvent récupérer les informations accessibles au moment où la commande est utilisée. Exemple :
135
135
 
@@ -149,3 +149,54 @@ discordChampDynamique("serveur-info", "Avatar", "discordAvatar")
149
149
  ```
150
150
 
151
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.4";
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,7 +112,7 @@ class FbpCompiler {
112
112
  constructor(options = {}) {
113
113
  this.output = options.onOutput ?? (value => console.log(value));
114
114
  }
115
- resolveDiscordValue(source, interaction, selectedUser, selectedMember) {
115
+ resolveDiscordValue(source, interaction, selectedUser, selectedMember, optionValues) {
116
116
  const guild = interaction.guild;
117
117
  const values = {
118
118
  "discordServeur.nom": guild?.name ?? "Serveur indisponible",
@@ -130,6 +130,9 @@ class FbpCompiler {
130
130
  "discordLatence": String(Math.max(0, Math.round(interaction.client.ws.ping))) + " ms",
131
131
  "discordAvatar": selectedUser?.displayAvatarURL({ size: 256 }) ?? interaction.user.displayAvatarURL({ size: 256 })
132
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];
133
136
  return source.trim() in values ? values[source.trim()] : "Source inconnue : " + source.trim();
134
137
  }
135
138
  runFile(filePath) {
@@ -217,6 +220,33 @@ class FbpCompiler {
217
220
  .setDescription(response.userOption.description)
218
221
  .setRequired(false));
219
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
+ }
220
250
  return builder.toJSON();
221
251
  });
222
252
  const manager = client.application?.commands;
@@ -244,8 +274,33 @@ class FbpCompiler {
244
274
  if (response === undefined)
245
275
  return;
246
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
+ }
247
301
  if (response.type === "text") {
248
- 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);
249
304
  }
250
305
  else {
251
306
  const embed = new discord_js_1.EmbedBuilder()
@@ -255,9 +310,11 @@ class FbpCompiler {
255
310
  const guild = interaction.guild;
256
311
  const selectedUser = response.userOption ? interaction.options.getUser(response.userOption.name) : null;
257
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);
258
315
  const fields = response.fields.map(field => ({
259
316
  name: field.name,
260
- value: field.dynamic ? this.resolveDiscordValue(field.value, interaction, selectedUser, selectedMember) : field.value,
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),
261
318
  inline: false
262
319
  }));
263
320
  if (fields.length)
@@ -266,7 +323,11 @@ class FbpCompiler {
266
323
  }
267
324
  }
268
325
  catch (error) {
326
+ const message = "Une erreur est survenue pendant l'exécution de cette commande.";
269
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
+ }
270
331
  }
271
332
  });
272
333
  void client.login(token).catch((error) => this.output("Connexion Discord impossible : " + (error instanceof Error ? error.message : String(error))));
@@ -282,7 +343,7 @@ class FbpCompiler {
282
343
  if (!/^[a-z0-9_-]{1,32}$/.test(commandName)) {
283
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);
284
345
  }
285
- this.discordCommands.set(commandName, { type: "text", content: values[1] });
346
+ this.discordCommands.set(commandName, { type: "text", content: values[1], options: [] });
286
347
  return;
287
348
  }
288
349
  if (name === "discordCommandeEmbed") {
@@ -296,7 +357,7 @@ class FbpCompiler {
296
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);
297
358
  if (!/^#[0-9a-fA-F]{6}$/.test(color))
298
359
  throw new errors_1.FbpError("La couleur d'un embed doit être au format hexadécimal, par exemple #5865F2.", undefined, line);
299
- this.discordCommands.set(commandName, { type: "embed", title, description, color, fields: [] });
360
+ this.discordCommands.set(commandName, { type: "embed", title, description, color, fields: [], options: [] });
300
361
  return;
301
362
  }
302
363
  if (name === "discordChampDynamique") {
@@ -314,6 +375,60 @@ class FbpCompiler {
314
375
  command.fields.push({ name: fieldName, value: source, dynamic: true });
315
376
  return;
316
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
+ }
317
432
  if (name === "discordOptionUtilisateur") {
318
433
  if (values.length !== 3 || values.some(value => typeof value !== "string")) {
319
434
  throw new errors_1.FbpError("Utilisation : discordOptionUtilisateur(nomCommande, nomOption, description).", undefined, line);
@@ -326,6 +441,11 @@ class FbpCompiler {
326
441
  throw new errors_1.FbpError("Le nom de l'option doit contenir 1 à 32 caractères minuscules, chiffres, tirets ou underscores.", undefined, line);
327
442
  if (!description.trim() || description.length > 100)
328
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);
329
449
  command.userOption = { name: optionName, description };
330
450
  return;
331
451
  }
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.4",
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": {