Como começar

O KirzenScript pega a sintaxe do JavaScript e do Python, e aceita as duas. Seja qual for a que você já escreve, dá para escrever aqui sem consultar nada.

Mas não é JavaScript, e a diferença importa. O Kirzen lê o seu script e o executa ele mesmo, em vez de entregá-lo ao motor por baixo. É por isso que um script não alcança o sistema de arquivos, a rede, o token do bot ou o banco de dados, e por isso um erro num comando nunca derruba o bot.

const name = user.nick

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

Escreva scripts em Servidores → seu servidor → Comandos → Novo comando → Um script. Um comando personalizado é disparado pelo que alguém digita, então você dá a ele um prefixo como !roll.

O que o editor faz por você

  • Oito comandos prontos. Comece por uma rolagem de dado, uma economia, um cartão de perfil ou um ranking e mude o que quiser, em vez de começar do zero.
  • Ele confere enquanto você digita. Um instante depois que você para, a linha com o erro é marcada na margem e o motivo aparece escrito embaixo.
  • Experimente. Roda o script com valores inventados e mostra o que seria dito e o que seria feito. Nada é enviado e nada é salvo.
  • Ele se comporta como um editor de código. Tab indenta, colchetes e aspas se fecham sozinhos, e o Enter mantém o seu lugar.

Dois jeitos de escrever

Estes dois comandos são o mesmo comando. As chaves são o padrão e o que o resto desta página usa, mas dois-pontos e indentação fazem o mesmo trabalho onde você preferir. O estilo é decidido por bloco, então dá para misturar à vontade.

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

O que vem do Python

GravaçãoSignifica
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)

O que você pode deixar de fora

  • Ponto e vírgula. Nunca são necessários, em nenhum dos dois estilos.
  • Parênteses em volta da condição. if x > 0 e if (x > 0) são a mesma coisa.
  • let e const. Atribuir a um nome novo já o cria. Use const quando quiser travar o nome.
  • Chaves. Dois-pontos e indentação fazem o mesmo trabalho.

A única regra ao usar dois-pontos: as linhas de dentro do bloco precisam estar mais indentadas que a linha que o abriu. O Kirzen avisa com todas as letras quando não estão.

O que um script conhece

Seis valores já esperam por você. São cópias, então mudar um não muda nada no Discord.

ValorO que ele guarda
user id, name, nick, mention, avatar, joinedAt, createdAt, roles, topRole, isBooster
user.roles Uma lista, do maior para o menor. Cada uma tem id, name, color, mention. topRole é a primeira delas.
server id, name, members, icon, boosts, createdAt, roleCount, channelCount
channel id, name, mention, topic, isNsfw
args O que veio depois do comando, separado por espaços
input A mesma coisa como uma string só
command O nome do comando em execução
reply(`${user.name} is in ${server.name}, which has ${server.members} members.`)

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

joinedAt e createdAt são segundos, prontos para passar a timestamp().

Respondendo

reply() monta a resposta do comando. Chame quantas vezes quiser; os pedaços são juntados.

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

send() é diferente: ele publica uma mensagem separada no canal. Use quando quiser uma segunda mensagem em vez de uma primeira mais longa. dm() envia em particular para quem rodou o comando.

Embeds e cor

embed() anexa um embed à resposta do comando. Chame até três vezes para três embeds, e use junto com reply() se quiser texto acima deles.

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 um embed

ChaveO que ele recebe
titleTexto, até 256 caracteres
descriptionTexto, até 4096
colorUm nome, um "#rrggbb", ou um número
urlTransforma o título em link
thumbnailUma URL de imagem, mostrada pequena no canto
imageUma URL de imagem, mostrada em largura total
authorUm nome, ou { name, icon, url }
footerTexto, ou { text, icon }
timestamptrue para carimbar com o agora
fieldsAté 25 de { name, value, inline }

Cores por nome

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 é conferida antes de ser usada. O que não for http ou https é descartado, então um script não consegue contrabandear nada estranho para dentro de uma mensagem.

Enviando para outro lugar

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

Deixe o canal de fora e vai para o canal onde o comando foi usado. Se o Kirzen não puder publicar no canal que você indicar, o envio é recusado e o comando avisa, em vez de publicar em outro lugar.

Fazendo coisas

Um script nunca encosta no Discord em si. Ele anota o que gostaria que fosse feito, e o Kirzen confere cada pedido contra as permissões reais antes de executar qualquer coisa. Se o Kirzen não pode gerenciar um cargo, o script não recebe uma mentira: a ação é simplesmente recusada e relatada.

ChamadaO que ele pede
addRole(id, who?)Dar um cargo. Deixe a segunda parte de fora e vale para quem rodou
removeRole(id, who?)Tirar um cargo
setNickname(name, who?)Renomear alguém. Vazio limpa o apelido
timeout(secs, who?, why?)Silenciar alguém. Zero segundos desfaz
react(emoji)Reagir à mensagem que disparou o comando
deleteMessage()Apagar aquela mensagem
pin()Fixar aquela mensagem
createThread(name)Abrir um tópico nela
send(text)Publicar uma mensagem separada
sendTo(id, text)Publicar em outro canal
sendEmbed(obj)Publicar um embed como mensagem própria
dm(text)Enviar mensagem direta

react(), pin(), deleteMessage() e createThread() agem sobre a mensagem que alguém digitou para disparar o comando.

Expulsar e banir estão de fora de propósito. Não têm volta, um script é fácil de errar, e o moderador que precisa deles já tem /ban e /kick já. timeout() está aqui no lugar: resolve e passa sozinho.

Lembrando

db é um armazenamento pequeno que pertence ao seu servidor e sobrevive entre execuções. É o que transforma um comando que responde num comando que guarda placar: uma economia, um contador, um perfil, um ranking.

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

reply(`You now have ${db.get(`coins:${user.id}`)} coins.`)
ChamadaO que faz
db.get(key, fallback)Lê o valor, ou o reserva quando não há nada
db.set(key, value)Grava um número, um texto, uma lista ou um objeto
db.add(key, n)Soma a um número e devolve o novo total
db.has(key)Se há algo guardado ali
db.delete(key)Remove
db.top(n, prefix)Os n maiores números, para um ranking
db.keys(prefix)As chaves que começam com algo
db.count()Quantas chaves o servidor está usando

Nomeando as chaves

Uma chave é só texto, então coloque nela do que ela trata. Um prefixo é o que torna db.top e db.keys úteis, porque os dois trabalham em cima de um.

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

Um ranking

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

O que ele não faz

  • É por servidor. Nada do que você salva fica visível para outro servidor, e nada que outro servidor salvou fica visível aqui.
  • É pequeno de propósito. 2.000 chaves por servidor, 2.000 caracteres por valor e 25 leituras ou gravações por execução. É uma memória para comandos, não um banco de dados.
  • Uma execução que para no meio mantém o que já gravou. Não tem desfazer, então acrescente antes de gastar, e não o contrário.

Olhando à mão

Servidores → seu servidor → Comandos mostra um medidor de quão cheio está o espaço de nomes assim que algo é salvo, e Dados salvos abre o painel por trás dele.

  • Ver o que existe. Toda chave com o tipo, o valor e o tamanho, filtrada por prefixo.
  • Editar um valor. Gravado em JSON, então um número continua número e um objeto continua objeto. A próxima execução lê o que você deixou.
  • Renomear uma chave. Leva o valor junto. Ele se recusa a cair numa chave que já existe, em vez de sobrescrever.
  • Apagar uma, um prefixo, ou tudo. Filtrar por coins: e apagar limpa os dados só daquele comando, sem encostar no resto.

O anel fica âmbar acima de 70% e vermelho acima de 90%, então um espaço de nomes enchendo aparece antes de um script começar a ser recusado.

Outras pessoas

Um script já começa sabendo de quem o rodou. Estas leem qualquer outra pessoa do servidor.

ChamadaDevolve
getMember(id) Do mesmo formato que user: id, name, nick, mention, avatar, joinedAt, isBooster, roles. Nulo se a pessoa não estiver aqui.
getRole(id) id, name, color, mention, position
roleCount(id)Quantos membros têm aquele cargo
getXp(who?) O nível da pessoa: xp, level, rank, messages, percent. Deixe o id de fora para quem rodou o comando.
mentioned()O primeiro id que o comando recebeu, menção ou cru
mentions()Todos eles, 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 }
    ]
  })
}

As consultas têm teto de 25 por execução, igual às gravações. Ler um servidor inteiro num laço não é para isso que isto serve.

A linguagem

Variáveis

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";

Condições

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"

&&, || e ?? funcionam como você espera, inclusive com avaliação curta. === e == comparam por valor.

Laços

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

Funções

function double(n) {
  return n * 2
}

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

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

Comentários

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

Funções nativas

FunçãoO que faz
len(x)O tamanho de um texto ou de uma lista
random(a, b)Um número inteiro de a até b, os dois incluídos
pick(list)Um item de uma lista, ao acaso
range(a, b)Uma lista de números de a até b, sem incluir b
keys(object)Os nomes de um objeto
sum(list)Soma uma lista de números
unique(list)Descarta repetidos
shuffle(list)Os mesmos itens em ordem aleatória
json(value)Transforma qualquer coisa em texto, útil enquanto se trabalha
numberFormat(n)1234567 vira 1.234.567
date()year month day hour minute weekday
values(object)Os valores de um objeto
mentionUser(id)Transforma um id numa menção
mentionRole(id)O mesmo para um cargo
mentionChannel(id)O mesmo para um canal
Number(v), String(v), Boolean(v)Conversões
parseInt(v), parseFloat(v)Lê um número de dentro de um texto
str(v), int(v)A grafia do Python para os mesmos dois
hasRole(id)Se quem rodou tem aquele cargo
color(name)Transforma um nome de cor num número
now()A hora de agora, em segundos
timestamp(s, style) Uma hora que o Discord mostra no fuso de cada leitor. Estilos: t T d D f F R
truncate(text, n)Corta o texto e acrescenta reticências
bold, italic, underline, strike, spoiler, quote, code, codeBlock, link A formatação do Discord, sem precisar decorar a pontuação
Math.… floor, ceil, round, abs, min, max, pow, sqrt, random

Métodos dos 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(", "));

Limites

Todo script roda dentro de um orçamento. Bater num destes não é uma falha: o comando responde com uma observação e o Kirzen segue em frente.

LimiteTeto
Tamanho do script20.000 caracteres
Tamanho da resposta1.900 caracteres, e depois é cortado
Trabalho feito200.000 passos
Voltas de laço10.000 por laço
Chamadas aninhadas50 de profundidade
Ações pedidas10 por execução
Embeds mostrados3 por comando
Campos por embed25
Leituras e gravações salvas25 por execução
Consultas25 por execução
Chaves salvas2.000 por servidor
Tamanho de um valor2.000 caracteres
Tamanho da lista5.000 itens
Texto acumulado20.000 caracteres

O que não existe aqui

Estes existem no JavaScript e não aqui. Cada um ficou de fora ou porque seria uma saída da caixa de areia, ou porque seria um jeito de travar o bot, então nenhum deles será acrescentado.

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

Ler .constructor ou .__proto__ devolve nada em vez de um erro, e gravar em qualquer um dos dois é recusado.

Receitas

Uma rolagem de dado

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

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

Um cargo que a pessoa se dá

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

Escolhe uma ao acaso

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

Uma lista organizada

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

Um cartão 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` }
})

Uma recompensa diária

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

Reagir e limpar

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