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.
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.")
O que vem do Python
| Gravação | 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) |
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 > 0eif (x > 0)são a mesma coisa. -
let e const. Atribuir a um nome novo já o cria. Use
constquando 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.
| Valor | O 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
| Chave | O que ele recebe |
|---|---|
title | Texto, até 256 caracteres |
description | Texto, até 4096 |
color | Um nome, um "#rrggbb", ou um número |
url | Transforma o título em link |
thumbnail | Uma URL de imagem, mostrada pequena no canto |
image | Uma URL de imagem, mostrada em largura total |
author | Um nome, ou { name, icon, url } |
footer | Texto, ou { text, icon } |
timestamp | true para carimbar com o agora |
fields | Até 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.
| Chamada | O 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.`)
| Chamada | O 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.
| Chamada | Devolve |
|---|---|
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ção | O 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.
| Limite | Teto |
|---|---|
| Tamanho do script | 20.000 caracteres |
| Tamanho da resposta | 1.900 caracteres, e depois é cortado |
| Trabalho feito | 200.000 passos |
| Voltas de laço | 10.000 por laço |
| Chamadas aninhadas | 50 de profundidade |
| Ações pedidas | 10 por execução |
| Embeds mostrados | 3 por comando |
| Campos por embed | 25 |
| Leituras e gravações salvas | 25 por execução |
| Consultas | 25 por execução |
| Chaves salvas | 2.000 por servidor |
| Tamanho de um valor | 2.000 caracteres |
| Tamanho da lista | 5.000 itens |
| Texto acumulado | 20.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}`)