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.

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

Lo que viene de Python

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

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 > 0 y if (x > 0) son lo mismo.
  • let y const. Asignar a un nombre nuevo lo crea. Usa const cuando 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.

ValorLo 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

ClaveLo que recibe
titleTexto, hasta 256 caracteres
descriptionTexto, hasta 4096
colorUn nombre, un "#rrggbb" o un número
urlConvierte el título en un enlace
thumbnailUna URL de imagen, mostrada pequeña en la esquina
imageUna URL de imagen, mostrada a todo el ancho
authorUn nombre, o { name, icon, url }
footerTexto, o { text, icon }
timestamptrue para sellarlo con el ahora
fieldsHasta 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.

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

LlamadaDevuelve
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ónLo 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ímiteTecho
Longitud del script20.000 caracteres
Longitud de la respuesta1.900 caracteres, y luego se corta
Trabajo hecho200.000 pasos
Vueltas de bucle10.000 por bucle
Llamadas anidadas50 de profundidad
Acciones solicitadas10 por ejecución
Embeds mostrados3 por comando
Campos por embed25
Lecturas y escrituras guardadas25 por ejecución
Consultas25 por ejecución
Claves guardadas2.000 por servidor
Tamaño de un valor2.000 caracteres
Longitud de la lista5.000 elementos
Texto acumulado20.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}`)