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.
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.")
Was aus Python übernommen wird
| Schreiben | Bedeutet |
|---|---|
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) |
Was du weglassen kannst
- Semikolons. In keiner der beiden Schreibweisen nötig.
-
Klammern um eine Bedingung.
if x > 0undif (x > 0)sind dasselbe. -
let und const. Einem neuen Namen etwas zuzuweisen erstellt ihn. Nutze
constwenn 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.
| Wert | Was 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üssel | Was es braucht |
|---|---|
title | Text, bis zu 256 Zeichen |
description | Text, bis zu 4096 |
color | Ein Name, ein „#rrggbb“ oder eine Zahl |
url | Macht den Titel zu einem Link |
thumbnail | Eine Bild-URL, klein in der Ecke angezeigt |
image | Eine Bild-URL, in voller Breite angezeigt |
author | Ein Name oder { name, icon, url } |
footer | Text, oder { text, icon } |
timestamp | true um es mit der aktuellen Zeit zu stempeln |
fields | Bis 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.
| Aufruf | Wonach 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.`)
| Aufruf | Was 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.
| Aufruf | Gibt 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
| Funktion | Was 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.
| Grenze | Obergrenze |
|---|---|
| Skriptlänge | 20.000 Zeichen |
| Antwortlänge | 1.900 Zeichen, danach wird abgeschnitten |
| Erledigte Arbeit | 200.000 Schritte |
| Schleifendurchläufe | 10.000 pro Schleife |
| Verschachtelte Aufrufe | 50 Ebenen tief |
| Angeforderte Aktionen | 10 pro Durchlauf |
| Angezeigte Embeds | 3 pro Befehl |
| Felder pro Embed | 25 |
| Gespeicherte Lese- und Schreibvorgänge | 25 pro Durchlauf |
| Abfragen | 25 pro Durchlauf |
| Gespeicherte Schlüssel | 2.000 pro Server |
| Größe eines Werts | 2.000 Zeichen |
| Listenlänge | 5.000 Einträge |
| Zusammengesetzter Text | 20.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}`)