Erste Schritte

KirzenScript übernimmt seine Syntax von JavaScript und von Python und akzeptiert beide. Welche der beiden du schon schreibst, die kannst du hier schreiben, ohne etwas nachzuschlagen.

Es ist allerdings kein JavaScript, und der Unterschied zählt. Kirzen liest dein Skript und führt es selbst aus, statt es an die darunterliegende Engine weiterzureichen. Deshalb kommt ein Skript nicht an das Dateisystem, das Netzwerk, den Bot-Token oder die Datenbank, und deshalb kann ein Fehler in einem Befehl niemals den Bot lahmlegen.

const name = user.nick

if (args.length > 0) {
  reply(`Hi ${name}, you said ${input}`)
} else {
  reply(`Hi ${name}!`)
}

Skripte schreiben in Server → dein Server → Befehle → Neuer Befehl → Ein Skript. Ein eigener Befehl wird durch das ausgelöst, was jemand tippt, also gibst du ihm ein Präfix wie !roll.

Was der Editor für dich erledigt

  • Acht fertige Befehle. Fang mit einem Würfelwurf, einer Wirtschaft, einer Profilkarte oder einer Bestenliste an und ändere sie, statt bei null zu beginnen.
  • Er prüft, während du tippst. Kurz nachdem du aufhörst zu tippen, wird die fehlerhafte Zeile am Rand markiert und der Grund darunter ausgeschrieben.
  • Probier es aus. Führt das Skript mit erfundenen Werten aus und zeigt, was gesagt und was getan würde. Es wird nichts gesendet und nichts gespeichert.
  • Er verhält sich wie ein Code-Editor. Tab rückt ein, Klammern und Anführungszeichen schließen sich selbst, und die Eingabetaste behält deine Position bei.

Zwei Schreibweisen

Diese beiden Befehle sind derselbe Befehl. Geschweifte Klammern sind die Voreinstellung und das, was der Rest dieser Seite verwendet, aber ein Doppelpunkt mit Einrückung tut dasselbe, wo immer du ihn lieber magst. Die Schreibweise wird pro Block entschieden, du kannst sie also frei mischen.

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.")

Was aus Python übernommen wird

SchreibenBedeutet
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)

Was du weglassen kannst

  • Semikolons. In keiner der beiden Schreibweisen nötig.
  • Klammern um eine Bedingung. if x > 0 und if (x > 0) sind dasselbe.
  • let und const. Einem neuen Namen etwas zuzuweisen erstellt ihn. Nutze const wenn der Name festgeschrieben sein soll.
  • Geschweifte Klammern. Ein Doppelpunkt und eine Einrückung tun dasselbe.

Die einzige Regel bei Doppelpunkten: Die Zeilen im Block müssen weiter eingerückt sein als die Zeile, die ihn geöffnet hat. Kirzen sagt es deutlich, wenn das nicht so ist.

Was ein Skript weiß

Sechs Werte warten auf dich. Es sind Kopien, einen davon zu ändern ändert also nichts auf Discord.

WertWas es enthält
user id, name, nick, mention, avatar, joinedAt, createdAt, roles, topRole, isBooster
user.roles Eine Liste, die höchste zuerst. Jeder Eintrag hat id, name, color, mention. topRole ist der erste davon.
server id, name, members, icon, boosts, createdAt, roleCount, channelCount
channel id, name, mention, topic, isNsfw
args Was nach dem Befehl kam, an Leerzeichen getrennt
input Dasselbe als eine Zeichenkette
command Der Name des ausgeführten Befehls
reply(`${user.name} is in ${server.name}, which has ${server.members} members.`)

if (hasRole("123456789012345678")) {
  reply(" And they are a moderator.")
}

joinedAt und createdAt sind Sekunden, bereit zur Übergabe an timestamp().

Antwortet

reply() baut die Antwort des Befehls. Ruf es so oft auf, wie du willst; die Teile werden zusammengefügt.

reply("Hello ");
reply(user.name);
// The command answers "Hello Terra"

send() ist anders: Es postet eine eigene Nachricht im Kanal. Nutze es, wenn du eine zweite Nachricht willst statt einer längeren ersten. dm() sendet privat an die Person, die den Befehl ausgeführt hat.

Embeds und Farbe

embed() hängt ein Embed an die Antwort des Befehls. Ruf es bis zu dreimal für drei Embeds auf und nutze es zusammen mit reply() wenn du Text darüber haben willst.

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
})

Jeder Teil eines Embeds

SchlüsselWas es braucht
titleText, bis zu 256 Zeichen
descriptionText, bis zu 4096
colorEin Name, ein „#rrggbb“ oder eine Zahl
urlMacht den Titel zu einem Link
thumbnailEine Bild-URL, klein in der Ecke angezeigt
imageEine Bild-URL, in voller Breite angezeigt
authorEin Name oder { name, icon, url }
footerText, oder { text, icon }
timestamptrue um es mit der aktuellen Zeit zu stempeln
fieldsBis zu 25 von { name, value, inline }

Farben mit Namen

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 })

Jede URL wird vor der Verwendung geprüft. Alles, was nicht http oder https ist, wird verworfen, damit ein Skript nichts Merkwürdiges in eine Nachricht schmuggeln kann.

Eines woanders hinschicken

sendEmbed({ title: "Logged", description: input }, "123456789012345678")
sendTo("123456789012345678", "A plain message, in another channel.")

Lass den Kanal weg, dann geht es in den Kanal, in dem der Befehl benutzt wurde. Kann Kirzen im angegebenen Kanal nicht posten, wird das Senden verweigert und der Befehl sagt es, statt anderswo zu posten.

Dinge tun

Ein Skript fasst Discord nie selbst an. Es hält fest, was es gern getan hätte, und Kirzen prüft jede Anforderung gegen die tatsächlichen Berechtigungen, bevor irgendetwas ausgeführt wird. Kann Kirzen eine Rolle nicht verwalten, wird dem Skript nichts vorgemacht: Die Aktion wird schlicht abgelehnt und gemeldet.

AufrufWonach er fragt
addRole(id, who?)Vergibt eine Rolle. Lass den zweiten Teil weg, dann ist die ausführende Person gemeint
removeRole(id, who?)Eine Rolle wegnehmen
setNickname(name, who?)Jemanden umbenennen. Leer löscht den Spitznamen
timeout(secs, who?, why?)Jemanden stummschalten. Null Sekunden hebt es auf
react(emoji)Auf die auslösende Nachricht reagieren
deleteMessage()Diese Nachricht löschen
pin()Diese Nachricht anheften
createThread(name)Einen Thread dazu starten
send(text)Eine eigene Nachricht posten
sendTo(id, text)In einem anderen Kanal posten
sendEmbed(obj)Ein Embed als eigene Nachricht posten
dm(text)Eine Direktnachricht senden

react(), pin(), deleteMessage() und createThread() beziehen sich auf die Nachricht, mit der jemand den Befehl ausgelöst hat.

Kicken und Bannen fehlen mit Absicht. Beides lässt sich nicht rückgängig machen, ein Skript ist schnell falsch geschrieben, und ein Moderator, der beides braucht, hat /ban und /kick schon. timeout() gibt es stattdessen: Es erledigt die Sache und läuft von selbst wieder aus.

Merken

db ist ein kleiner Speicher, der zu deinem Server gehört und Durchläufe überdauert. Er macht aus einem Befehl, der antwortet, einen Befehl, der mitzählt: eine Wirtschaft, einen Zähler, ein Profil, eine Bestenliste.

db.add(`coins:${user.id}`, 10)

reply(`You now have ${db.get(`coins:${user.id}`)} coins.`)
AufrufWas er tut
db.get(key, fallback)Liest ihn, oder den Ersatzwert, wenn nichts da ist
db.set(key, value)Schreibt eine Zahl, etwas Text, eine Liste oder ein Objekt
db.add(key, n)Addiert zu einer Zahl und gibt dir die neue Summe
db.has(key)Ob darunter etwas gespeichert ist
db.delete(key)Entfernt sie
db.top(n, prefix)Die n höchsten Zahlen, für eine Bestenliste
db.keys(prefix)Die Schlüssel, die mit etwas beginnen
db.count()Wie viele Schlüssel der Server belegt

Schlüssel benennen

Ein Schlüssel ist nur Text, also schreib hinein, worum es geht. Ein Präfix macht db.top und db.keys nützlich, denn beide arbeiten damit.

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 })

Eine Bestenliste

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" })

Was es nicht tut

  • Das gilt pro Server. Nichts, was du speicherst, ist für einen anderen Server sichtbar, und nichts, was ein anderer Server gespeichert hat, ist hier sichtbar.
  • Es ist absichtlich klein. 2.000 Schlüssel pro Server, 2.000 Zeichen pro Wert und 25 Lese- oder Schreibvorgänge pro Durchlauf. Das ist ein Gedächtnis für Befehle, keine Datenbank.
  • Ein Durchlauf, der auf halber Strecke abbricht, behält, was er bereits geschrieben hat. Es gibt kein Rückgängig, also erst gutschreiben und dann abbuchen, nicht umgekehrt.

Von Hand nachsehen

Server → dein Server → Befehle zeigt eine Anzeige, wie voll der Namensraum ist, sobald etwas gespeichert wurde, und Gespeicherte Daten öffnet das Panel dahinter.

  • Sieh nach, was da ist. Jeder Schlüssel mit Typ, Wert und Größe, gefiltert nach Präfix.
  • Einen Wert bearbeiten. Als JSON geschrieben, damit eine Zahl eine Zahl bleibt und ein Objekt ein Objekt. Der nächste Durchlauf liest, was du hinterlässt.
  • Einen Schlüssel umbenennen. Verschiebt den Wert hinüber. Landet er auf einem bereits vorhandenen Schlüssel, wird das abgelehnt statt überschrieben.
  • Lösch einen Eintrag, ein Präfix oder alles. Filtern nach coins: und Löschen räumt die Daten dieses einen Befehls weg, ohne den Rest anzurühren.

Der Ring wird ab 70 % bernsteinfarben und ab 90 % rot, ein voll laufender Namensraum ist also sichtbar, bevor ein Skript abgewiesen wird.

Andere Personen

Ein Skript kennt zu Beginn die Person, die es ausgeführt hat. Damit liest du jede andere Person auf dem Server.

AufrufGibt zurück
getMember(id) Dieselbe Form wie user: id, name, nick, mention, avatar, joinedAt, isBooster, roles. Null, wenn die Person nicht hier ist.
getRole(id) id, name, color, mention, position
roleCount(id)Wie viele Mitglieder diese Rolle haben
getXp(who?) Ihr Level-Stand: xp, level, rank, messages, percent. Lass die ID weg, um die Person zu meinen, die den Befehl ausgeführt hat.
mentioned()Die erste ID, die dem Befehl übergeben wurde, als Erwähnung oder roh
mentions()Alle davon, als 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 }
    ]
  })
}

Nachschlagen ist auf 25 pro Durchlauf begrenzt, genau wie Speichern. Einen ganzen Server in einer Schleife auszulesen ist nicht der Zweck davon.

Die Sprache

Variablen

let count = 0           // can change
const name = "Kirzen"   // locked, cannot be changed
total = 0               // no keyword at all also works

count += 5
count++

Text

const a = "double quotes";
const b = 'single quotes';
const c = `a template with ${user.name} in it`;
const d = "joined " + "with plus";

Bedingungen

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"

&&, || und ?? verhalten sich wie erwartet, auch mit Kurzschlussauswertung. === und == vergleichen beide nach Wert.

Schleifen

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--
}

Listen und Objekte

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}`);

Funktionen

function double(n) {
  return n * 2
}

const shout = (text) => text.toUpperCase() + "!"

reply(double(21))
reply(shout("hello"))

Kommentare

// a line
# also a line
/* or several
   lines */

Eingebaute Funktionen

FunktionWas er tut
len(x)Wie lang ein Text oder eine Liste ist
random(a, b)Eine ganze Zahl von a bis b, beide eingeschlossen
pick(list)Ein Eintrag aus einer Liste, zufällig
range(a, b)Eine Liste von Zahlen ab a bis ausschließlich b
keys(object)Die Namen in einem Objekt
sum(list)Zählt eine Liste von Zahlen zusammen
unique(list)Verwirft Wiederholungen
shuffle(list)Dieselben Einträge in zufälliger Reihenfolge
json(value)Macht aus allem Text, praktisch beim Arbeiten
numberFormat(n)Aus 1234567 wird 1.234.567
date()year month day hour minute weekday
values(object)Die Werte in einem Objekt
mentionUser(id)Macht aus einer ID eine Erwähnung
mentionRole(id)Dasselbe für eine Rolle
mentionChannel(id)Dasselbe für einen Kanal
Number(v), String(v), Boolean(v)Umwandlungen
parseInt(v), parseFloat(v)Liest eine Zahl aus Text heraus
str(v), int(v)Die Python-Schreibweise derselben beiden
hasRole(id)Ob die ausführende Person diese Rolle hat
color(name)Macht aus einem Farbnamen eine Zahl
now()Die aktuelle Zeit, in Sekunden
timestamp(s, style) Eine Zeit, die Discord in der Zeitzone jedes Lesers anzeigt. Formate: t T d D f F R
truncate(text, n)Kürzt Text und setzt Auslassungspunkte
bold, italic, underline, strike, spoiler, quote, code, codeBlock, link Discords Auszeichnung, ohne sich die Satzzeichen merken zu müssen
Math.… floor, ceil, round, abs, min, max, pow, sqrt, random

Methoden auf Werten

Text

length toUpperCase toLowerCase trim includes startsWith endsWith indexOf charAt slice split replace replaceAll repeat padStart padEnd

Listen

length join includes indexOf slice concat push reverse map filter find some every reduce sort

Zahlen

toFixed toString

const names = args.map(a => a.toLowerCase()).filter(a => a.length > 2);
reply(names.sort().join(", "));

Grenzen

Jedes Skript läuft gegen ein Budget. Eine dieser Grenzen zu erreichen ist kein Absturz: Der Befehl antwortet mit einem Hinweis und Kirzen macht weiter.

GrenzeObergrenze
Skriptlänge20.000 Zeichen
Antwortlänge1.900 Zeichen, danach wird abgeschnitten
Erledigte Arbeit200.000 Schritte
Schleifendurchläufe10.000 pro Schleife
Verschachtelte Aufrufe50 Ebenen tief
Angeforderte Aktionen10 pro Durchlauf
Angezeigte Embeds3 pro Befehl
Felder pro Embed25
Gespeicherte Lese- und Schreibvorgänge25 pro Durchlauf
Abfragen25 pro Durchlauf
Gespeicherte Schlüssel2.000 pro Server
Größe eines Werts2.000 Zeichen
Listenlänge5.000 Einträge
Zusammengesetzter Text20.000 Zeichen

Was es nicht gibt

Die gibt es in JavaScript und hier nicht. Jede fehlt entweder, weil sie ein Weg aus der Sandbox wäre, oder weil sie den Bot aufhängen könnte; keine davon wird also nachgereicht.

eval Function require import process globalThis setTimeout fetch new class this async await try __proto__ constructor prototype

Das Lesen von .constructor oder .__proto__ gibt nichts zurück statt eines Fehlers, und beides zu beschreiben wird abgelehnt.

Rezepte

Ein Würfelwurf

const sides = int(args[0]) || 6

reply(`${user.mention} rolled a ${random(1, sides)} on a d${sides}.`)

Eine selbst vergebbare Rolle

addRole("123456789012345678")
reply("Done, you have the role.")

Zufällig eines auswählen

if (args.length < 2) {
  reply("Give me at least two things to choose between.")
} else {
  reply(`I pick **${pick(args)}**.`)
}

Eine aufgeräumte Liste

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`);
}

Eine Profilkarte

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` }
})

Eine tägliche Belohnung

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

Reagieren und aufräumen

react("✅")
deleteMessage()
send(`${user.mention} said: ${input}`)