fbp-language 1.1.4 → 1.1.7

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,75 @@ 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.
203
+
204
+
205
+ ## Limites numériques des options (v1.1.7)
206
+
207
+ Les options slash de type nombre et entier peuvent imposer une valeur minimale et maximale. Déclare l'option d'abord, puis ses limites :
208
+
209
+ ```fbp
210
+ discordConnexion()
211
+ discordCommande("age", "Âge sélectionné : {{age}}")
212
+ discordOptionEntier("age", "age", "Choisis un âge", true)
213
+ discordOptionEntierLimites("age", "age", 13, 99)
214
+
215
+ discordCommande("note", "Note sélectionnée : {{note}}")
216
+ discordOptionNombre("note", "note", "Choisis une note", true)
217
+ discordOptionNombreLimites("note", "note", 0, 20)
218
+ ```
219
+
220
+ - `discordOptionEntierLimites(commande, option, minimum, maximum)` accepte uniquement des bornes entières.
221
+ - `discordOptionNombreLimites(commande, option, minimum, maximum)` accepte des bornes numériques, y compris décimales.
222
+ - Le minimum doit être inférieur ou égal au maximum.
223
+ - Les limites s'appliquent à l'interface Discord. Une option avec des choix prédéfinis ne peut pas recevoir de limites.
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.7";
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,41 @@ 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
+ if (option.minValue !== undefined)
235
+ item.setMinValue(option.minValue);
236
+ if (option.maxValue !== undefined)
237
+ item.setMaxValue(option.maxValue);
238
+ for (const choice of option.choices ?? [])
239
+ if (typeof choice.value === "number")
240
+ item.addChoices({ name: choice.name, value: choice.value });
241
+ return item;
242
+ }),
243
+ entier: () => builder.addIntegerOption(item => {
244
+ item.setName(option.name).setDescription(option.description).setRequired(option.required);
245
+ if (option.minValue !== undefined)
246
+ item.setMinValue(option.minValue);
247
+ if (option.maxValue !== undefined)
248
+ item.setMaxValue(option.maxValue);
249
+ for (const choice of option.choices ?? [])
250
+ if (typeof choice.value === "number")
251
+ item.addChoices({ name: choice.name, value: choice.value });
252
+ return item;
253
+ }),
254
+ booleen: () => builder.addBooleanOption(item => item.setName(option.name).setDescription(option.description).setRequired(option.required))
255
+ }[option.type];
256
+ add();
257
+ }
220
258
  return builder.toJSON();
221
259
  });
222
260
  const manager = client.application?.commands;
@@ -244,8 +282,33 @@ class FbpCompiler {
244
282
  if (response === undefined)
245
283
  return;
246
284
  try {
285
+ const optionValues = new Map();
286
+ for (const option of response.options ?? []) {
287
+ let value = null;
288
+ if (option.type === "texte")
289
+ value = interaction.options.getString(option.name);
290
+ else if (option.type === "nombre") {
291
+ const number = interaction.options.getNumber(option.name);
292
+ value = number === null ? null : String(number);
293
+ }
294
+ else if (option.type === "entier") {
295
+ const number = interaction.options.getInteger(option.name);
296
+ value = number === null ? null : String(number);
297
+ }
298
+ else {
299
+ const boolean = interaction.options.getBoolean(option.name);
300
+ value = boolean === null ? null : String(boolean);
301
+ }
302
+ if (value === null && option.required) {
303
+ await interaction.reply({ content: "Option obligatoire indisponible : " + option.name, ephemeral: true });
304
+ return;
305
+ }
306
+ if (value !== null)
307
+ optionValues.set(option.name, value);
308
+ }
247
309
  if (response.type === "text") {
248
- await interaction.reply(response.content);
310
+ const content = response.content.replace(/\{\{([a-z0-9_-]{1,32})\}\}/g, (match, name) => optionValues.has(name) ? optionValues.get(name) : match);
311
+ await interaction.reply(content);
249
312
  }
250
313
  else {
251
314
  const embed = new discord_js_1.EmbedBuilder()
@@ -255,9 +318,11 @@ class FbpCompiler {
255
318
  const guild = interaction.guild;
256
319
  const selectedUser = response.userOption ? interaction.options.getUser(response.userOption.name) : null;
257
320
  const selectedMember = selectedUser && guild ? await guild.members.fetch(selectedUser.id).catch(() => null) : null;
321
+ if (response.userOption && selectedUser)
322
+ optionValues.set(response.userOption.name, selectedMember?.displayName ?? selectedUser.username);
258
323
  const fields = response.fields.map(field => ({
259
324
  name: field.name,
260
- value: field.dynamic ? this.resolveDiscordValue(field.value, interaction, selectedUser, selectedMember) : field.value,
325
+ 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
326
  inline: false
262
327
  }));
263
328
  if (fields.length)
@@ -266,7 +331,11 @@ class FbpCompiler {
266
331
  }
267
332
  }
268
333
  catch (error) {
334
+ const message = "Une erreur est survenue pendant l'exécution de cette commande.";
269
335
  this.output("Erreur Discord : " + (error instanceof Error ? error.message : String(error)));
336
+ if (!interaction.replied && !interaction.deferred) {
337
+ await interaction.reply({ content: message, ephemeral: true }).catch(() => undefined);
338
+ }
270
339
  }
271
340
  });
272
341
  void client.login(token).catch((error) => this.output("Connexion Discord impossible : " + (error instanceof Error ? error.message : String(error))));
@@ -282,7 +351,7 @@ class FbpCompiler {
282
351
  if (!/^[a-z0-9_-]{1,32}$/.test(commandName)) {
283
352
  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
353
  }
285
- this.discordCommands.set(commandName, { type: "text", content: values[1] });
354
+ this.discordCommands.set(commandName, { type: "text", content: values[1], options: [] });
286
355
  return;
287
356
  }
288
357
  if (name === "discordCommandeEmbed") {
@@ -296,7 +365,7 @@ class FbpCompiler {
296
365
  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
366
  if (!/^#[0-9a-fA-F]{6}$/.test(color))
298
367
  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: [] });
368
+ this.discordCommands.set(commandName, { type: "embed", title, description, color, fields: [], options: [] });
300
369
  return;
301
370
  }
302
371
  if (name === "discordChampDynamique") {
@@ -314,6 +383,83 @@ class FbpCompiler {
314
383
  command.fields.push({ name: fieldName, value: source, dynamic: true });
315
384
  return;
316
385
  }
386
+ if (name === "discordChoixTexte" || name === "discordChoixEntier") {
387
+ if (values.length !== 4 || values.some(value => typeof value !== "string" && typeof value !== "number")) {
388
+ throw new errors_1.FbpError("Utilisation : discordChoixTexte(nomCommande, nomOption, nomChoix, valeur) ou discordChoixEntier(...).", undefined, line);
389
+ }
390
+ const [commandName, optionName, choiceName, choiceValue] = values;
391
+ if (typeof commandName !== "string" || typeof optionName !== "string" || typeof choiceName !== "string") {
392
+ throw new errors_1.FbpError("Le nom de la commande, de l'option et du choix doivent être du texte.", undefined, line);
393
+ }
394
+ const command = this.discordCommands.get(commandName);
395
+ if (!command)
396
+ throw new errors_1.FbpError("La commande « " + commandName + " » n'est pas déclarée.", undefined, line);
397
+ const option = (command.options ?? []).find(item => item.name === optionName);
398
+ const expectedType = name === "discordChoixTexte" ? "texte" : "entier";
399
+ if (!option || option.type !== expectedType)
400
+ throw new errors_1.FbpError("L'option « " + optionName + " » n'existe pas ou n'a pas le bon type.", undefined, line);
401
+ if (!choiceName.trim() || choiceName.length > 100)
402
+ throw new errors_1.FbpError("Le nom d'un choix doit contenir de 1 à 100 caractères.", undefined, line);
403
+ if (expectedType === "texte" && typeof choiceValue !== "string")
404
+ throw new errors_1.FbpError("La valeur d'un choix texte doit être du texte.", undefined, line);
405
+ if (expectedType === "entier" && (typeof choiceValue !== "number" || !Number.isInteger(choiceValue)))
406
+ throw new errors_1.FbpError("La valeur d'un choix entier doit être un nombre entier.", undefined, line);
407
+ if (option.minValue !== undefined || option.maxValue !== undefined)
408
+ throw new errors_1.FbpError("Une option avec des limites numériques ne peut pas recevoir de choix prédéfinis.", undefined, line);
409
+ const choices = option.choices ?? (option.choices = []);
410
+ if (choices.some(choice => choice.name === choiceName || choice.value === choiceValue))
411
+ throw new errors_1.FbpError("Ce nom ou cette valeur de choix existe déjà.", undefined, line);
412
+ if (choices.length >= 25)
413
+ throw new errors_1.FbpError("Une option Discord ne peut pas contenir plus de 25 choix.", undefined, line);
414
+ choices.push({ name: choiceName, value: choiceValue });
415
+ return;
416
+ }
417
+ if (name === "discordOptionNombreLimites" || name === "discordOptionEntierLimites") {
418
+ if (values.length !== 4 || typeof values[0] !== "string" || typeof values[1] !== "string" || typeof values[2] !== "number" || typeof values[3] !== "number") {
419
+ throw new errors_1.FbpError("Utilisation : discordOptionNombreLimites(commande, option, minimum, maximum) ou discordOptionEntierLimites(...).", undefined, line);
420
+ }
421
+ const [commandName, optionName, minimum, maximum] = values;
422
+ const command = this.discordCommands.get(commandName);
423
+ if (!command)
424
+ throw new errors_1.FbpError("La commande « " + commandName + " » n'est pas déclarée.", undefined, line);
425
+ const expectedType = name === "discordOptionNombreLimites" ? "nombre" : "entier";
426
+ const option = (command.options ?? []).find(item => item.name === optionName);
427
+ if (!option || option.type !== expectedType)
428
+ throw new errors_1.FbpError("L'option « " + optionName + " » n'existe pas ou n'a pas le type numérique attendu.", undefined, line);
429
+ if (!Number.isFinite(minimum) || !Number.isFinite(maximum) || minimum > maximum || (expectedType === "entier" && (!Number.isInteger(minimum) || !Number.isInteger(maximum)))) {
430
+ throw new errors_1.FbpError("Les limites doivent être des nombres valides, minimum inférieur ou égal au maximum ; elles doivent être entières pour une option entière.", undefined, line);
431
+ }
432
+ if (option.choices?.length)
433
+ throw new errors_1.FbpError("Une option avec des choix prédéfinis ne peut pas recevoir de limites numériques.", undefined, line);
434
+ option.minValue = minimum;
435
+ option.maxValue = maximum;
436
+ return;
437
+ }
438
+ if (name === "discordOptionTexte" || name === "discordOptionNombre" || name === "discordOptionEntier" || name === "discordOptionBooleen") {
439
+ if (values.length < 3 || values.length > 4 || values.some(value => typeof value !== "string" && typeof value !== "boolean")) {
440
+ throw new errors_1.FbpError("Utilisation : discordOptionTexte/Nombre/Entier/Booleen(nomCommande, nomOption, description, obligatoire).", undefined, line);
441
+ }
442
+ const [commandName, optionName, description] = values;
443
+ const required = values.length === 4 ? values[3] : false;
444
+ if (typeof commandName !== "string" || typeof optionName !== "string" || typeof description !== "string" || typeof required !== "boolean") {
445
+ 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);
446
+ }
447
+ const command = this.discordCommands.get(commandName);
448
+ if (!command)
449
+ throw new errors_1.FbpError("La commande « " + commandName + " » n'est pas déclarée.", undefined, line);
450
+ if (!/^[a-z0-9_-]{1,32}$/.test(optionName))
451
+ throw new errors_1.FbpError("Le nom de l'option doit contenir 1 à 32 caractères minuscules, chiffres, tirets ou underscores.", undefined, line);
452
+ if (!description.trim() || description.length > 100)
453
+ throw new errors_1.FbpError("La description de l'option doit contenir 1 à 100 caractères.", undefined, line);
454
+ const options = command.options ?? (command.options = []);
455
+ if (options.some(option => option.name === optionName) || (command.type === "embed" && command.userOption?.name === optionName))
456
+ throw new errors_1.FbpError("L'option « " + optionName + " » existe déjà pour cette commande.", undefined, line);
457
+ if (options.length + (command.type === "embed" && command.userOption ? 1 : 0) >= 25)
458
+ throw new errors_1.FbpError("Une commande slash Discord ne peut pas contenir plus de 25 options.", undefined, line);
459
+ const type = name === "discordOptionTexte" ? "texte" : name === "discordOptionNombre" ? "nombre" : name === "discordOptionEntier" ? "entier" : "booleen";
460
+ options.push({ name: optionName, description, type, required });
461
+ return;
462
+ }
317
463
  if (name === "discordOptionUtilisateur") {
318
464
  if (values.length !== 3 || values.some(value => typeof value !== "string")) {
319
465
  throw new errors_1.FbpError("Utilisation : discordOptionUtilisateur(nomCommande, nomOption, description).", undefined, line);
@@ -326,6 +472,11 @@ class FbpCompiler {
326
472
  throw new errors_1.FbpError("Le nom de l'option doit contenir 1 à 32 caractères minuscules, chiffres, tirets ou underscores.", undefined, line);
327
473
  if (!description.trim() || description.length > 100)
328
474
  throw new errors_1.FbpError("La description de l'option doit contenir 1 à 100 caractères.", undefined, line);
475
+ const options = command.options ?? (command.options = []);
476
+ if (options.some(option => option.name === optionName) || command.userOption?.name === optionName)
477
+ throw new errors_1.FbpError("L'option « " + optionName + " » existe déjà pour cette commande.", undefined, line);
478
+ if (options.length + 1 > 25)
479
+ throw new errors_1.FbpError("Une commande slash Discord ne peut pas contenir plus de 25 options.", undefined, line);
329
480
  command.userOption = { name: optionName, description };
330
481
  return;
331
482
  }
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, limites numériques, choix prédéfinis et embeds dynamiques.",
5
+ "version": "1.1.7",
6
6
  "type": "commonjs",
7
7
  "main": "dist/compiler.js",
8
8
  "bin": {