Premiers pas
KirzenScript emprunte sa syntaxe à JavaScript et à Python, et accepte les deux. Quelle que soit celle des deux que tu écris déjà, tu peux l'écrire ici sans rien chercher.
Ce n'est pourtant pas du JavaScript, et la différence compte. Kirzen lit ton script et l'exécute lui-même au lieu de le confier au moteur sous-jacent. C'est pourquoi un script n'atteint ni le système de fichiers, ni le réseau, ni le jeton du bot, ni la base de données, et pourquoi une erreur dans une commande ne peut jamais faire tomber le bot.
const name = user.nick
if (args.length > 0) {
reply(`Hi ${name}, you said ${input}`)
} else {
reply(`Hi ${name}!`)
}
Écris les scripts en Serveurs → ton serveur → Commandes → Nouvelle commande → Un script. Une commande personnalisée est déclenchée par ce que quelqu'un tape, tu lui donnes donc un préfixe comme !roll.
Ce que l'éditeur fait pour toi
- Huit commandes prêtes à l'emploi. Pars d'un lancer de dé, d'une économie, d'une carte de profil ou d'un classement et modifie-le, plutôt que de partir de rien.
- Il vérifie pendant que tu tapes. Un instant après que tu t'arrêtes, la ligne fautive est signalée dans la marge et la raison est détaillée en dessous.
- Essaie-le. Exécute le script sur des valeurs inventées et montre ce qui serait dit et ce qui serait fait. Rien n'est envoyé et rien n'est enregistré.
- Il se comporte comme un éditeur de code. Tab indente, les parenthèses et les guillemets se ferment tout seuls, et Entrée garde ta position.
Deux façons de l'écrire
Ces deux commandes sont la même commande. Les accolades sont la valeur par défaut et ce qu'utilise le reste de cette page, mais un deux-points et une indentation font le même travail là où tu les préfères. L'écriture se décide bloc par bloc : tu peux donc les mélanger librement.
const best = args.filter(a => a.length > 3);
if (best.length > 0) {
reply(best.join(", "));
} else {
reply("Nothing long enough.");
}
best = args.filter(a => len(a) > 3)
if len(best) > 0:
reply(best.join(", "))
else:
reply("Nothing long enough.")
Ce qui est repris de Python
| Écrire | Signifie |
|---|---|
and, or, not | &&, ||, ! |
True, False, None | true, false, null |
elif | else if |
def name(): | function name() {} |
for x in list: | for (const x of list) {} |
# comment | // comment |
len(x) | x.length |
str(x), int(x) | String(x), parseInt(x) |
Ce que tu peux omettre
- Points-virgules. Jamais nécessaire, dans aucune des deux écritures.
-
Des parenthèses autour d'une condition.
if x > 0etif (x > 0)sont identiques. -
let et const. Affecter une valeur à un nouveau nom le crée. Utilise
constquand tu veux que le nom soit figé. - Accolades. Un deux-points et une indentation font le même travail.
La seule règle avec les deux-points : les lignes d'un bloc doivent être plus indentées que la ligne qui l'a ouvert. Kirzen le dit clairement si ce n'est pas le cas.
Ce que sait un script
Six valeurs t'attendent. Ce sont des copies : en modifier une ne change rien sur Discord.
| Valeur | Ce qu'il contient |
|---|---|
user |
id, name, nick,
mention, avatar,
joinedAt, createdAt,
roles, topRole,
isBooster
|
user.roles |
Une liste, la plus élevée en premier. Chaque entrée a id,
name, color, mention.
topRole est le premier d'entre eux.
|
server |
id, name, members,
icon, boosts,
createdAt, roleCount,
channelCount
|
channel |
id, name, mention,
topic, isNsfw
|
args |
Ce qui suivait la commande, découpé aux espaces |
input |
La même chose sous forme d'une seule chaîne |
command |
Le nom de la commande en cours d'exécution |
reply(`${user.name} is in ${server.name}, which has ${server.members} members.`)
if (hasRole("123456789012345678")) {
reply(" And they are a moderator.")
}
joinedAt et createdAt sont des secondes, prêtes à être passées à timestamp().
Répond
reply() construit la réponse de la commande. Appelle-le autant de fois que tu veux ; les morceaux sont assemblés.
reply("Hello ");
reply(user.name);
// The command answers "Hello Terra"
send() est différent : il publie un message distinct dans le salon. Utilise-le quand tu veux un deuxième message plutôt qu'un premier plus long. dm() envoie en privé à la personne qui a lancé la commande.
Embeds et couleur
embed() attache un embed à la réponse de la commande. Appelle-le jusqu'à trois fois pour trois embeds, et utilise-le avec
reply() si tu veux du texte au-dessus.
embed({
title: `Profile of ${user.nick}`,
description: "Everything Kirzen knows about you.",
color: user.topRole.color,
thumbnail: user.avatar,
fields: [
{ name: "Top role", value: user.topRole.mention, inline: true },
{ name: "Joined", value: timestamp(user.joinedAt, "R"), inline: true },
{ name: "Roles", value: str(len(user.roles)), inline: true }
],
footer: { text: server.name },
timestamp: true
})
Chaque partie d'un embed
| Clé | Ce qu'il faut |
|---|---|
title | Texte, jusqu'à 256 caractères |
description | Texte, jusqu'à 4096 |
color | Un nom, un « #rrggbb » ou un nombre |
url | Fait du titre un lien |
thumbnail | Une URL d'image, affichée en petit dans le coin |
image | Une URL d'image, affichée pleine largeur |
author | Un nom, ou { name, icon, url } |
footer | Texte, ou { text, icon } |
timestamp | true pour l'horodater à maintenant |
fields | Jusqu'à 25 de { name, value, inline } |
Couleurs par nom
blurple green yellow
red fuchsia white
black grey gold
orange aqua purple
pink navy dark
embed({ title: "Warning", color: "red" })
embed({ title: "Done", color: "#57f287" })
embed({ title: "Match", color: user.topRole.color })
Chaque URL est vérifiée avant d'être utilisée. Tout ce qui n'est ni http ni https est écarté, pour qu'un script ne puisse pas glisser quelque chose d'étrange dans un message.
En envoyer un ailleurs
sendEmbed({ title: "Logged", description: input }, "123456789012345678")
sendTo("123456789012345678", "A plain message, in another channel.")
Omets le salon et cela va dans le salon où la commande a été utilisée. Si Kirzen ne peut pas écrire dans le salon que tu indiques, l'envoi est refusé et la commande le dit au lieu de publier ailleurs.
Passer à l'action
Un script ne touche jamais Discord lui-même. Il consigne ce qu'il aimerait voir fait, et Kirzen confronte chaque demande à ses permissions réelles avant d'exécuter quoi que ce soit. Si Kirzen ne peut pas gérer un rôle, on ne ment pas au script : l'action est simplement refusée et signalée.
| Appel | Ce qu'il demande |
|---|---|
addRole(id, who?) | Attribue un rôle. Omets la seconde partie et cela désigne la personne qui l'a lancée |
removeRole(id, who?) | Retirer un rôle |
setNickname(name, who?) | Renomme quelqu'un. Vide efface le surnom |
timeout(secs, who?, why?) | Exclut temporairement quelqu'un. Zéro seconde lève l'exclusion |
react(emoji) | Réagir au message déclencheur |
deleteMessage() | Supprimer ce message |
pin() | Épingler ce message |
createThread(name) | Ouvrir un fil dessus |
send(text) | Publier un message distinct |
sendTo(id, text) | Le publier dans un autre salon |
sendEmbed(obj) | Publier un embed comme message à part entière |
dm(text) | Envoyer un message privé |
react(), pin(),
deleteMessage() et createThread() agissent sur le message que quelqu'un a tapé pour déclencher la commande.
L'expulsion et le bannissement sont volontairement absents. Ils sont irréversibles, un script est vite mal écrit, et un modérateur qui en a besoin dispose de
/ban et /kick déjà.
timeout() est là à la place : cela fait le travail et cela finit par s'estomper.
Mémoriser
db est un petit espace de stockage qui appartient à ton serveur et survit d'une exécution à l'autre. C'est ce qui transforme une commande qui répond en une commande qui tient les comptes : une économie, un compteur, un profil, un classement.
db.add(`coins:${user.id}`, 10)
reply(`You now have ${db.get(`coins:${user.id}`)} coins.`)
| Appel | Ce qu'il fait |
|---|---|
db.get(key, fallback) | La lit, ou la valeur de repli quand il n'y a rien |
db.set(key, value) | Écrit un nombre, du texte, une liste ou un objet |
db.add(key, n) | Ajoute à un nombre et te donne le nouveau total |
db.has(key) | S'il y a quelque chose d'enregistré dessous |
db.delete(key) | La retire |
db.top(n, prefix) | Les n nombres les plus élevés, pour un classement |
db.keys(prefix) | Les clés qui commencent par quelque chose |
db.count() | Combien de clés le serveur utilise |
Nommer les clés
Une clé n'est que du texte : mets-y donc son sujet. Un préfixe est ce qui rend db.top et db.keys utiles, car tous deux travaillent dessus.
db.set(`coins:${user.id}`, 500) // one per member
db.set("event:name", "Winter cup") // one for the server
db.set(`profile:${user.id}`, { class: "Mage", level: 3 })
Un classement
let text = ""
let place = 1
for (const entry of db.top(5, "coins:")) {
const id = entry.key.replace("coins:", "")
text += `${place}. ${mentionUser(id)}, ${numberFormat(entry.value)}\n`
place++
}
embed({ title: "Richest members", description: text, color: "gold" })
Ce qu'il ne fera pas
- Cela vaut par serveur. Rien de ce que tu enregistres n'est visible par un autre serveur, et rien de ce qu'un autre serveur a enregistré n'est visible ici.
- Il est volontairement petit. 2 000 clés par serveur, 2 000 caractères par valeur et 25 lectures ou écritures par exécution. C'est une mémoire pour les commandes, pas une base de données.
- Une exécution qui s'arrête à mi-chemin conserve ce qu'elle a déjà écrit. Il n'y a pas d'annulation : crédite avant de dépenser, plutôt que l'inverse.
Regarder à la main
Serveurs → ton serveur → Commandes affiche une jauge du remplissage de l'espace de noms dès que quelque chose est enregistré, et Données enregistrées ouvre le panneau situé derrière.
- Regarde ce qu'il y a. Chaque clé avec son type, sa valeur et sa taille, filtrée par préfixe.
- Modifier une valeur. Écrit en JSON, pour qu'un nombre reste un nombre et un objet un objet. La prochaine exécution lit ce que tu laisses.
- Renommer une clé. Déplace la valeur. Plutôt que d'écraser une clé déjà existante, il refuse de s'y poser.
-
Supprime une entrée, un préfixe, ou tout. Filtrer par
coins:puis supprimer efface les données de cette seule commande sans toucher au reste.
L'anneau passe à l'ambre au-delà de 70 % et au rouge au-delà de 90 % : un espace de noms qui se remplit est donc visible avant qu'un script ne commence à être refusé.
D'autres personnes
Un script connaît au départ la personne qui l'a lancé. Ceci permet de lire n'importe qui d'autre sur le serveur.
| Appel | Renvoie |
|---|---|
getMember(id) |
La même forme que user: id,
name, nick, mention,
avatar, joinedAt,
isBooster, roles. Null si la personne n'est pas ici.
|
getRole(id) |
id, name, color, mention, position |
roleCount(id) | Combien de membres détiennent ce rôle |
getXp(who?) |
Leur progression : xp, level,
rank, messages,
percent. Omets l'identifiant pour désigner la personne qui l'a lancée.
|
mentioned() | Le premier identifiant donné à la commande, en mention ou brut |
mentions() | Tous, sous forme de liste |
const target = getMember(mentioned())
if (!target) {
reply("Mention somebody first.")
} else {
const level = getXp(target.id)
embed({
title: target.nick,
thumbnail: target.avatar,
color: target.roles.length ? target.roles[0].color : "blurple",
fields: [
{ name: "Level", value: str(level.level), inline: true },
{ name: "Rank", value: `#${level.rank}`, inline: true },
{ name: "Joined", value: timestamp(target.joinedAt, "R"), inline: true }
]
})
}
Les recherches sont plafonnées à 25 par exécution, tout comme les enregistrements. Lire un serveur entier dans une boucle n'est pas l'objet de ceci.
La langue
Variables
let count = 0 // can change
const name = "Kirzen" // locked, cannot be changed
total = 0 // no keyword at all also works
count += 5
count++
Texte
const a = "double quotes";
const b = 'single quotes';
const c = `a template with ${user.name} in it`;
const d = "joined " + "with plus";
Conditions
if (server.members > 100) {
reply("Busy server")
} else if (server.members > 10) {
reply("Getting there")
} else {
reply("Cosy")
}
const label = args.length ? "with arguments" : "without"
&&, || et ?? fonctionnent comme tu t'y attends, évaluation paresseuse comprise.
=== et == comparent tous deux par valeur.
Boucles
for (const word of args) {
reply(word.toUpperCase() + " ")
}
for (let i = 0; i < 5; i++) {
if (i === 2) continue
reply(i)
}
let n = 3
while (n > 0) {
reply(n)
n--
}
Listes et objets
const colours = ["red", "green", "blue"];
reply(colours[1]); // green
reply(colours.length); // 3
const profile = { name: user.name, level: 7 };
reply(`${profile.name} is level ${profile.level}`);
Fonctions
function double(n) {
return n * 2
}
const shout = (text) => text.toUpperCase() + "!"
reply(double(21))
reply(shout("hello"))
Commentaires
// a line
# also a line
/* or several
lines */
Fonctions intégrées
| Fonction | Ce qu'il fait |
|---|---|
len(x) | La longueur d'un texte ou d'une liste |
random(a, b) | Un nombre entier de a à b, tous deux inclus |
pick(list) | Un élément d'une liste, au hasard |
range(a, b) | Une liste de nombres de a jusqu'à b exclu |
keys(object) | Les noms d'un objet |
sum(list) | Additionne une liste de nombres |
unique(list) | Écarte les doublons |
shuffle(list) | Les mêmes éléments dans un ordre aléatoire |
json(value) | Transforme n'importe quoi en texte, pratique pendant le travail |
numberFormat(n) | 1234567 devient 1 234 567 |
date() | year month day hour minute weekday |
values(object) | Les valeurs d'un objet |
mentionUser(id) | Transforme un identifiant en mention |
mentionRole(id) | La même chose pour un rôle |
mentionChannel(id) | La même chose pour un salon |
Number(v), String(v), Boolean(v) | Conversions |
parseInt(v), parseFloat(v) | Extrait un nombre d'un texte |
str(v), int(v) | L'écriture Python des deux mêmes |
hasRole(id) | Si la personne qui l'a lancée possède ce rôle |
color(name) | Transforme un nom de couleur en nombre |
now() | L'heure actuelle, en secondes |
timestamp(s, style) |
Une heure que Discord affiche dans le fuseau de chaque lecteur. Styles :
t T d D f F R
|
truncate(text, n) | Coupe le texte et ajoute des points de suspension |
bold, italic, underline,
strike, spoiler, quote,
code, codeBlock, link
|
Le balisage de Discord, sans avoir à retenir la ponctuation |
Math.… |
floor, ceil, round,
abs, min, max,
pow, sqrt, random
|
Méthodes sur les valeurs
Texte
length toUpperCase toLowerCase
trim includes startsWith
endsWith indexOf charAt
slice split replace
replaceAll repeat padStart
padEnd
Listes
length join includes
indexOf slice concat
push reverse map
filter find some
every reduce sort
Nombres
toFixed toString
const names = args.map(a => a.toLowerCase()).filter(a => a.length > 2);
reply(names.sort().join(", "));
Limites
Chaque script s'exécute avec un budget. Atteindre l'une de ces limites n'est pas un plantage : la commande répond par une note et Kirzen continue.
| Limite | Plafond |
|---|---|
| Longueur du script | 20 000 caractères |
| Longueur de la réponse | 1 900 caractères, ensuite c'est coupé |
| Travail accompli | 200 000 étapes |
| Tours de boucle | 10 000 par boucle |
| Appels imbriqués | 50 niveaux de profondeur |
| Actions demandées | 10 par exécution |
| Embeds affichés | 3 par commande |
| Champs par embed | 25 |
| Lectures et écritures enregistrées | 25 par exécution |
| Recherches | 25 par exécution |
| Clés enregistrées | 2 000 par serveur |
| Taille d'une valeur | 2 000 caractères |
| Longueur de la liste | 5 000 éléments |
| Texte assemblé | 20 000 caractères |
Ce qui n'existe pas
Celles-ci existent en JavaScript et pas ici. Chacune est écartée parce qu'elle offrirait une sortie du bac à sable ou un moyen de bloquer le bot ; aucune ne sera donc ajoutée.
eval Function require
import process globalThis
setTimeout fetch new
class this async
await try __proto__
constructor prototype
La lecture de .constructor ou .__proto__ ne renvoie rien plutôt qu'une erreur, et écrire dans l'un ou l'autre est refusé.
Recettes
Un lancer de dé
const sides = int(args[0]) || 6
reply(`${user.mention} rolled a ${random(1, sides)} on a d${sides}.`)
Un rôle auto-attribuable
addRole("123456789012345678")
reply("Done, you have the role.")
En choisir un au hasard
if (args.length < 2) {
reply("Give me at least two things to choose between.")
} else {
reply(`I pick **${pick(args)}**.`)
}
Une liste bien rangée
const items = args
.map(a => a.trim())
.filter(a => a.length > 0)
.sort();
for (let i = 0; i < items.length; i++) {
reply(`${i + 1}. ${items[i]}\n`);
}
Une carte de profil
embed({
title: user.nick,
color: user.topRole ? user.topRole.color : "blurple",
thumbnail: user.avatar,
fields: [
{ name: "Joined", value: timestamp(user.joinedAt, "R"), inline: true },
{ name: "Account made", value: timestamp(user.createdAt, "D"), inline: true },
{ name: "Roles", value: user.roles.map(r => r.mention).join(" ") || "none" }
],
footer: { text: `${server.name} · ${server.members} members` }
})
Une récompense quotidienne
const key = `daily:${user.id}`
const last = db.get(key, 0)
const wait = 86400 - (now() - last)
if (wait > 0) {
reply(`Come back ${timestamp(last + 86400, "R")}.`)
} else {
const prize = random(50, 200)
const total = db.add(`coins:${user.id}`, prize)
db.set(key, now())
reply(`You picked up ${prize} coins. You now have ${numberFormat(total)}.`)
}
Réagir et nettoyer
react("✅")
deleteMessage()
send(`${user.mention} said: ${input}`)