Para empezar
KirzenScript toma su sintaxis de JavaScript y de Python, y acepta las dos. Escribas la que escribas de esas dos, puedes escribirla aquí sin consultar nada.
Pero no es JavaScript, y la diferencia importa. Kirzen lee tu script y lo ejecuta él mismo en vez de entregárselo al motor de debajo. Por eso un script no llega al sistema de archivos, ni a la red, ni al token del bot, ni a la base de datos, y por eso un fallo en un comando nunca puede tumbar el bot.
const name = user.nick
if (args.length > 0) {
reply(`Hi ${name}, you said ${input}`)
} else {
reply(`Hi ${name}!`)
}
Escribe scripts en Servidores → tu servidor → Comandos → Comando nuevo → Un script. Un comando personalizado se lanza con lo que alguien escribe, así que le das un prefijo como !roll.
Lo que el editor hace por ti
- Ocho comandos ya hechos. Empieza por una tirada de dados, una economía, una tarjeta de perfil o una clasificación y cámbiala, en vez de partir de cero.
- Comprueba mientras escribes. Un momento después de que pares, la línea con el fallo se marca en el margen y el motivo aparece escrito debajo.
- Pruébalo. Ejecuta el script con valores inventados y muestra qué se diría y qué se haría. No se envía nada ni se guarda nada.
- Se comporta como un editor de código. El tabulador sangra, los corchetes y las comillas se cierran solos, y el Intro mantiene tu sitio.
Dos formas de escribirlo
Estos dos comandos son el mismo comando. Las llaves son lo predeterminado y lo que usa el resto de esta página, pero dos puntos y una sangría hacen lo mismo donde los prefieras. El estilo se decide por bloque, así que puedes mezclarlos a gusto.
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.")
Lo que viene de Python
| Escritura | Significa |
|---|---|
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) |
Lo que puedes dejar fuera
- Punto y coma. Nunca hacen falta, en ninguno de los dos estilos.
-
Paréntesis alrededor de la condición.
if x > 0yif (x > 0)son lo mismo. -
let y const. Asignar a un nombre nuevo lo crea. Usa
constcuando quieras fijar el nombre. - Llaves. Dos puntos y una sangría hacen lo mismo.
La única regla al usar dos puntos: las líneas de dentro del bloque tienen que ir más sangradas que la línea que lo abrió. Kirzen lo dice claramente si no es así.
Lo que sabe un script
Seis valores ya te esperan. Son copias, así que cambiar uno no cambia nada en Discord.
| Valor | Lo que guarda |
|---|---|
user |
id, name, nick,
mention, avatar,
joinedAt, createdAt,
roles, topRole,
isBooster
|
user.roles |
Una lista, de mayor a menor. Cada una tiene id,
name, color, mention.
topRole es la primera de ellas.
|
server |
id, name, members,
icon, boosts,
createdAt, roleCount,
channelCount
|
channel |
id, name, mention,
topic, isNsfw
|
args |
Lo que vino después del comando, separado por espacios |
input |
Lo mismo como una sola cadena |
command |
El nombre del comando que se está ejecutando |
reply(`${user.name} is in ${server.name}, which has ${server.members} members.`)
if (hasRole("123456789012345678")) {
reply(" And they are a moderator.")
}
joinedAt y createdAt son segundos, listos para pasárselos a timestamp().
Respondiendo
reply() construye la respuesta del comando. Llámalo tantas veces como quieras; los trozos se unen.
reply("Hello ");
reply(user.name);
// The command answers "Hello Terra"
send() es distinto: publica un mensaje aparte en el canal. Úsalo cuando quieras un segundo mensaje en lugar de un primero más largo. dm() envía en privado a quien ejecutó el comando.
Embeds y color
embed() adjunta un embed a la respuesta del comando. Llámalo hasta tres veces para tres embeds, y úsalo junto a
reply() si quieres texto encima de ellos.
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
})
Cada parte de un embed
| Clave | Lo que recibe |
|---|---|
title | Texto, hasta 256 caracteres |
description | Texto, hasta 4096 |
color | Un nombre, un "#rrggbb" o un número |
url | Convierte el título en un enlace |
thumbnail | Una URL de imagen, mostrada pequeña en la esquina |
image | Una URL de imagen, mostrada a todo el ancho |
author | Un nombre, o { name, icon, url } |
footer | Texto, o { text, icon } |
timestamp | true para sellarlo con el ahora |
fields | Hasta 25 de { name, value, inline } |
Colores por nombre
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 })
Toda URL se comprueba antes de usarse. Lo que no sea http o https se descarta, así un script no puede colar nada raro en un mensaje.
Enviarlo a otro sitio
sendEmbed({ title: "Logged", description: input }, "123456789012345678")
sendTo("123456789012345678", "A plain message, in another channel.")
Deja fuera el canal y va al canal donde se usó el comando. Si Kirzen no puede publicar en el canal que indiques, el envío se rechaza y el comando lo dice, en lugar de publicar en otro sitio.
Haciendo cosas
Un script nunca toca Discord en sí. Anota lo que querría que se hiciera, y Kirzen comprueba cada petición contra sus permisos reales antes de ejecutar nada. Si Kirzen no puede gestionar un rol, al script no se le miente: la acción simplemente se rechaza y se informa.
| Llamada | Lo que pide |
|---|---|
addRole(id, who?) | Dar un rol. Deja fuera la segunda parte y se refiere a quien lo ejecutó |
removeRole(id, who?) | Quitar un rol |
setNickname(name, who?) | Renombrar a alguien. Vacío borra el apodo |
timeout(secs, who?, why?) | Silenciar a alguien. Cero segundos lo deshace |
react(emoji) | Reaccionar al mensaje que lo disparó |
deleteMessage() | Borrar ese mensaje |
pin() | Fijar ese mensaje |
createThread(name) | Abrir un hilo en él |
send(text) | Publicar un mensaje aparte |
sendTo(id, text) | Publicarlo en otro canal |
sendEmbed(obj) | Publicar un embed como mensaje propio |
dm(text) | Enviar un mensaje directo |
react(), pin(),
deleteMessage() y createThread() actúan sobre el mensaje que alguien escribió para lanzar el comando.
Expulsar y banear están fuera a propósito. No tienen vuelta atrás, un script es fácil de equivocar, y un moderador que los necesite ya tiene
/ban y /kick ya.
timeout() está aquí en su lugar: hace el trabajo y se pasa solo.
Recordando
db es un pequeño almacén que pertenece a tu servidor y sobrevive entre ejecuciones. Es lo que convierte un comando que responde en un comando que lleva la cuenta: una economía, un contador, un perfil, una clasificación.
db.add(`coins:${user.id}`, 10)
reply(`You now have ${db.get(`coins:${user.id}`)} coins.`)
| Llamada | Lo que hace |
|---|---|
db.get(key, fallback) | Lo lee, o el valor de reserva cuando no hay nada |
db.set(key, value) | Escribe un número, un texto, una lista o un objeto |
db.add(key, n) | Suma a un número y te devuelve el nuevo total |
db.has(key) | Si hay algo guardado bajo ella |
db.delete(key) | Lo quita |
db.top(n, prefix) | Los n números más altos, para una clasificación |
db.keys(prefix) | Las claves que empiezan por algo |
db.count() | Cuántas claves usa el servidor |
Nombrar las claves
Una clave es solo texto, así que ponle de qué trata. Un prefijo es lo que hace db.top y db.keys útiles, porque los dos trabajan sobre uno.
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 })
Una clasificación
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" })
Lo que no hace
- Es por servidor. Nada de lo que guardes es visible para otro servidor, y nada que otro servidor guardara es visible aquí.
- Es pequeño a propósito. 2.000 claves por servidor, 2.000 caracteres por valor y 25 lecturas o escrituras por ejecución. Es una memoria para comandos, no una base de datos.
- Una ejecución que se detiene a medias conserva lo que ya escribió. No hay deshacer, así que suma antes de gastar y no al revés.
Mirándolo a mano
Servidores → tu servidor → Comandos muestra un medidor de cuán lleno está el espacio de nombres en cuanto se guarda algo, y Datos guardados abre el panel que hay detrás.
- Mira lo que hay. Cada clave con su tipo, su valor y su tamaño, filtrada por prefijo.
- Editar un valor. Se guarda en JSON, así un número sigue siendo número y un objeto sigue siendo objeto. La siguiente ejecución lee lo que dejes.
- Renombrar una clave. Lleva el valor consigo. Se niega a caer en una clave que ya existe en lugar de sobrescribirla.
-
Borra una, un prefijo, o todo. Filtrar por
coins:y borrar limpia los datos de ese comando sin tocar el resto.
El anillo se pone ámbar por encima del 70% y rojo por encima del 90%, así que un espacio de nombres que se llena se ve antes de que un script empiece a rechazarse.
Otras personas
Un script empieza sabiendo quién lo ejecutó. Estas leen a cualquier otra persona del servidor.
| Llamada | Devuelve |
|---|---|
getMember(id) |
De la misma forma que user: id,
name, nick, mention,
avatar, joinedAt,
isBooster, roles. Nulo si la persona no está aquí.
|
getRole(id) |
id, name, color, mention, position |
roleCount(id) | Cuántos miembros tienen ese rol |
getXp(who?) |
Su nivel: xp, level,
rank, messages,
percent. Deja fuera el id para quien lo ejecutó.
|
mentioned() | El primer id que recibió el comando, mención o en crudo |
mentions() | Todos ellos, como lista |
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 }
]
})
}
Las consultas tienen un tope de 25 por ejecución, igual que las escrituras. Leer un servidor entero en un bucle no es para lo que sirve esto.
El lenguaje
Variables
let count = 0 // can change
const name = "Kirzen" // locked, cannot be changed
total = 0 // no keyword at all also works
count += 5
count++
Texto
const a = "double quotes";
const b = 'single quotes';
const c = `a template with ${user.name} in it`;
const d = "joined " + "with plus";
Condiciones
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"
&&, || y ?? funcionan como esperas, incluida la evaluación perezosa.
=== y == comparan por valor.
Bucles
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--
}
Listas y objetos
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}`);
Funciones
function double(n) {
return n * 2
}
const shout = (text) => text.toUpperCase() + "!"
reply(double(21))
reply(shout("hello"))
Comentarios
// a line
# also a line
/* or several
lines */
Funciones integradas
| Función | Lo que hace |
|---|---|
len(x) | La longitud de un texto o de una lista |
random(a, b) | Un número entero de a a b, ambos incluidos |
pick(list) | Un elemento de una lista, al azar |
range(a, b) | Una lista de números desde a hasta b, sin incluir b |
keys(object) | Los nombres de un objeto |
sum(list) | Suma una lista de números |
unique(list) | Descarta los repetidos |
shuffle(list) | Los mismos elementos en orden aleatorio |
json(value) | Convierte cualquier cosa en texto, útil mientras trabajas |
numberFormat(n) | 1234567 se convierte en 1.234.567 |
date() | year month day hour minute weekday |
values(object) | Los valores de un objeto |
mentionUser(id) | Convierte un id en una mención |
mentionRole(id) | Lo mismo para un rol |
mentionChannel(id) | Lo mismo para un canal |
Number(v), String(v), Boolean(v) | Conversiones |
parseInt(v), parseFloat(v) | Lee un número dentro de un texto |
str(v), int(v) | La forma de escribirlos en Python |
hasRole(id) | Si quien lo ejecutó tiene ese rol |
color(name) | Convierte un nombre de color en un número |
now() | La hora actual, en segundos |
timestamp(s, style) |
Una hora que Discord muestra en la zona horaria de cada lector. Estilos:
t T d D f F R
|
truncate(text, n) | Corta el texto y añade puntos suspensivos |
bold, italic, underline,
strike, spoiler, quote,
code, codeBlock, link
|
El formato de Discord, sin tener que recordar los signos |
Math.… |
floor, ceil, round,
abs, min, max,
pow, sqrt, random
|
Métodos de los valores
Texto
length toUpperCase toLowerCase
trim includes startsWith
endsWith indexOf charAt
slice split replace
replaceAll repeat padStart
padEnd
Listas
length join includes
indexOf slice concat
push reverse map
filter find some
every reduce sort
Números
toFixed toString
const names = args.map(a => a.toLowerCase()).filter(a => a.length > 2);
reply(names.sort().join(", "));
Límites
Cada script corre dentro de un presupuesto. Tocar uno de estos límites no es un fallo: el comando responde con una nota y Kirzen sigue.
| Límite | Techo |
|---|---|
| Longitud del script | 20.000 caracteres |
| Longitud de la respuesta | 1.900 caracteres, y luego se corta |
| Trabajo hecho | 200.000 pasos |
| Vueltas de bucle | 10.000 por bucle |
| Llamadas anidadas | 50 de profundidad |
| Acciones solicitadas | 10 por ejecución |
| Embeds mostrados | 3 por comando |
| Campos por embed | 25 |
| Lecturas y escrituras guardadas | 25 por ejecución |
| Consultas | 25 por ejecución |
| Claves guardadas | 2.000 por servidor |
| Tamaño de un valor | 2.000 caracteres |
| Longitud de la lista | 5.000 elementos |
| Texto acumulado | 20.000 caracteres |
Lo que no está aquí
Estos existen en JavaScript y aquí no. Cada uno queda fuera o porque sería una salida del entorno aislado, o porque sería una forma de colgar el bot, así que ninguno se añadirá.
eval Function require
import process globalThis
setTimeout fetch new
class this async
await try __proto__
constructor prototype
Leer .constructor o .__proto__ devuelve nada en lugar de un error, y escribir en cualquiera de los dos se rechaza.
Recetas
Una tirada de dados
const sides = int(args[0]) || 6
reply(`${user.mention} rolled a ${random(1, sides)} on a d${sides}.`)
Un rol que uno se asigna
addRole("123456789012345678")
reply("Done, you have the role.")
Elige uno al azar
if (args.length < 2) {
reply("Give me at least two things to choose between.")
} else {
reply(`I pick **${pick(args)}**.`)
}
Una lista ordenada
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`);
}
Una tarjeta de perfil
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` }
})
Una recompensa diaria
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)}.`)
}
Reaccionar y limpiar
react("✅")
deleteMessage()
send(`${user.mention} said: ${input}`)