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.

JavaScript
const best = args.filter(a => a.length > 3);

if (best.length > 0) {
  reply(best.join(", "));
} else {
  reply("Nothing long enough.");
}
Python
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

ÉcrireSignifie
and, or, not&&, ||, !
True, False, Nonetrue, false, null
elifelse 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 > 0 et if (x > 0) sont identiques.
  • let et const. Affecter une valeur à un nouveau nom le crée. Utilise const quand 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.

ValeurCe 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
titleTexte, jusqu'à 256 caractères
descriptionTexte, jusqu'à 4096
colorUn nom, un « #rrggbb » ou un nombre
urlFait du titre un lien
thumbnailUne URL d'image, affichée en petit dans le coin
imageUne URL d'image, affichée pleine largeur
authorUn nom, ou { name, icon, url }
footerTexte, ou { text, icon }
timestamptrue pour l'horodater à maintenant
fieldsJusqu'à 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.

AppelCe 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.`)
AppelCe 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.

AppelRenvoie
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

FonctionCe 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.

LimitePlafond
Longueur du script20 000 caractères
Longueur de la réponse1 900 caractères, ensuite c'est coupé
Travail accompli200 000 étapes
Tours de boucle10 000 par boucle
Appels imbriqués50 niveaux de profondeur
Actions demandées10 par exécution
Embeds affichés3 par commande
Champs par embed25
Lectures et écritures enregistrées25 par exécution
Recherches25 par exécution
Clés enregistrées2 000 par serveur
Taille d'une valeur2 000 caractères
Longueur de la liste5 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}`)