С чего начать
KirzenScript берёт синтаксис из JavaScript и из Python и принимает оба. На каком бы из двух вы уже ни писали, здесь можно писать так же, ничего не подглядывая.
Но это не JavaScript, и разница важна. Kirzen читает ваш скрипт и выполняет его сам, а не передаёт движку под ним. Поэтому скрипт не достаёт ни до файловой системы, ни до сети, ни до токена бота, ни до базы данных, и поэтому ошибка в одной команде никогда не уронит бота.
const name = user.nick
if (args.length > 0) {
reply(`Hi ${name}, you said ${input}`)
} else {
reply(`Hi ${name}!`)
}
Пишите скрипты в Серверы → ваш сервер → Команды → Новая команда → Скрипт. Пользовательская команда срабатывает на то, что кто-то печатает, поэтому вы даёте ей префикс вроде !roll.
Что редактор делает за вас
- Восемь готовых команд. Начните с броска кубика, экономики, карточки профиля или таблицы лидеров и переделайте под себя, вместо того чтобы начинать с нуля.
- Он проверяет по ходу набора. Через мгновение после остановки строка с ошибкой отмечается на полях, а причина расписывается ниже.
- Попробуйте. Выполняет скрипт на вымышленных значениях и показывает, что было бы сказано и что сделано. Ничего не отправляется и не сохраняется.
- Он ведёт себя как редактор кода. Tab делает отступ, скобки и кавычки закрываются сами, а Enter сохраняет место.
Два способа записи
Это одна и та же команда. Фигурные скобки — вариант по умолчанию, и им пользуется вся остальная страница, но двоеточие с отступом делают то же самое там, где вам так удобнее. Стиль выбирается для каждого блока, так что их можно свободно смешивать.
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.")
Что пришло из Python
| Запись | Значит |
|---|---|
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) |
Что можно опустить
- Точки с запятой. Не нужны ни в том, ни в другом стиле.
-
Скобки вокруг условия.
if x > 0иif (x > 0)это одно и то же. -
let и const. Присваивание новому имени создаёт его. Используйте
constкогда хотите закрепить имя. - Фигурные скобки. Двоеточие и отступ делают то же самое.
Единственное правило при двоеточиях: строки внутри блока должны быть с большим отступом, чем строка, которая его открыла. Если это не так, Kirzen прямо об этом скажет.
Что знает скрипт
Шесть значений уже ждут вас. Это копии, так что изменение одного ничего не меняет в Discord.
| Значение | Что он хранит |
|---|---|
user |
id, name, nick,
mention, avatar,
joinedAt, createdAt,
roles, topRole,
isBooster
|
user.roles |
Список, от большего к меньшему. У каждого есть id,
name, color, mention.
topRole — первый из них.
|
server |
id, name, members,
icon, boosts,
createdAt, roleCount,
channelCount
|
channel |
id, name, mention,
topic, isNsfw
|
args |
Что шло после команды, разбитое по пробелам |
input |
То же самое одной строкой |
command |
Название выполняемой команды |
reply(`${user.name} is in ${server.name}, which has ${server.members} members.`)
if (hasRole("123456789012345678")) {
reply(" And they are a moderator.")
}
joinedAt и createdAt это секунды, готовые для передачи в timestamp().
Ответы
reply() собирает ответ команды. Вызывайте сколько угодно раз; куски соединяются вместе.
reply("Hello ");
reply(user.name);
// The command answers "Hello Terra"
send() работает иначе: он публикует в канале отдельное сообщение. Используйте, когда нужно второе сообщение, а не более длинное первое. dm() отправляет лично тому, кто вызвал команду.
Эмбеды и цвет
embed() прикрепляет эмбед к ответу команды. Вызовите до трёх раз для трёх эмбедов и используйте вместе с
reply() если хотите текст над ними.
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
})
Каждая часть эмбеда
| Ключ | Что он принимает |
|---|---|
title | Текст, до 256 символов |
description | Текст, до 4096 |
color | Название, «#rrggbb» или число |
url | Делает заголовок ссылкой |
thumbnail | URL изображения, маленьким в углу |
image | URL изображения, во всю ширину |
author | Название или { name, icon, url } |
footer | Текст или { text, icon } |
timestamp | true чтобы поставить текущее время |
fields | До 25 штук { name, value, inline } |
Цвета по названию
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 })
Каждый URL проверяется перед использованием. Всё, что не http или https, отбрасывается, так что скрипт не сможет протащить в сообщение что-то странное.
Отправка в другое место
sendEmbed({ title: "Logged", description: input }, "123456789012345678")
sendTo("123456789012345678", "A plain message, in another channel.")
Не указывайте канал — и оно уйдёт в тот канал, где вызвали команду. Если Kirzen не может писать в названный вами канал, отправка отклоняется, и команда об этом сообщает, вместо того чтобы опубликовать где-то ещё.
Действия
Скрипт никогда не трогает сам Discord. Он записывает, что хотел бы сделать, а Kirzen сверяет каждый запрос со своими реальными правами, прежде чем что-либо выполнить. Если Kirzen не может управлять ролью, скрипту не лгут: действие просто отклоняется, и об этом сообщается.
| Вызов | Что он запрашивает |
|---|---|
addRole(id, who?) | Выдать роль. Опустите вторую часть — и это тот, кто вызвал команду |
removeRole(id, who?) | Снять роль |
setNickname(name, who?) | Переименовать кого-нибудь. Пусто — ник сбрасывается |
timeout(secs, who?, why?) | Выдать тайм-аут. Ноль секунд снимает его |
react(emoji) | Отреагировать на сообщение, которое его вызвало |
deleteMessage() | Удалить это сообщение |
pin() | Закрепить это сообщение |
createThread(name) | Открыть на нём ветку |
send(text) | Опубликовать отдельное сообщение |
sendTo(id, text) | Опубликовать в другом канале |
sendEmbed(obj) | Опубликовать эмбед отдельным сообщением |
dm(text) | Отправить личное сообщение |
react(), pin(),
deleteMessage() и createThread() действуют на сообщение, которое кто-то написал, чтобы вызвать команду.
Кик и бан отсутствуют намеренно. Их нельзя отменить, в скрипте легко ошибиться, а у модератора, которому они нужны, уже есть
/ban и /kick и так.
timeout() есть вместо них: она делает своё дело и проходит сама.
Память
db это небольшое хранилище, которое принадлежит вашему серверу и живёт между запусками. Именно оно превращает команду, которая отвечает, в команду, которая ведёт счёт: экономику, счётчик, профиль, таблицу лидеров.
db.add(`coins:${user.id}`, 10)
reply(`You now have ${db.get(`coins:${user.id}`)} coins.`)
| Вызов | Что он делает |
|---|---|
db.get(key, fallback) | Читает его или запасное значение, если ничего нет |
db.set(key, value) | Записывает число, текст, список или объект |
db.add(key, n) | Прибавляет к числу и возвращает новую сумму |
db.has(key) | Есть ли что-нибудь под ним |
db.delete(key) | Убирает |
db.top(n, prefix) | n наибольших чисел, для таблицы лидеров |
db.keys(prefix) | Ключи, которые начинаются с чего-то |
db.count() | Сколько ключей занимает сервер |
Именование ключей
Ключ — это просто текст, поэтому впишите в него, о чём он. Префикс — это то, что делает db.top и db.keys полезными, потому что оба работают по нему.
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 })
Таблица лидеров
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" })
Чего он не сделает
- Это отдельно для каждого сервера. Ничто из сохранённого вами не видно другому серверу, и ничто, сохранённое другим сервером, не видно здесь.
- Оно маленькое намеренно. 2000 ключей на сервер, 2000 символов на значение и 25 чтений или записей за запуск. Это память для команд, а не база данных.
- Запуск, оборвавшийся на половине, сохраняет то, что уже записал. Отмены нет, поэтому сначала начисляйте, а потом списывайте, а не наоборот.
Посмотреть вручную
Серверы → ваш сервер → Команды показывает шкалу заполненности пространства имён, как только что-то сохраняется, а Сохранённые данные открывает панель за ней.
- Посмотрите, что там есть. Каждый ключ с его типом, значением и размером, с фильтром по префиксу.
- Изменить значение. Записывается в JSON, так что число остаётся числом, а объект объектом. Следующий запуск прочитает то, что вы оставили.
- Переименовать ключ. Переносит значение. Он откажется занять уже существующий ключ, вместо того чтобы его перезаписать.
-
Удалите один ключ, префикс или всё сразу. Отфильтровать по
coins:и удаление стирает данные только этой команды, не трогая остальное.
Кольцо желтеет после 70% и краснеет после 90%, так что заполняющееся пространство имён видно раньше, чем скриптам начнут отказывать.
Другие люди
Скрипт изначально знает о том, кто его запустил. Эти читают любого другого на сервере.
| Вызов | Возвращает |
|---|---|
getMember(id) |
Той же формы, что и user: id,
name, nick, mention,
avatar, joinedAt,
isBooster, roles. Null, если человека здесь нет.
|
getRole(id) |
id, name, color, mention, position |
roleCount(id) | Сколько участников с этой ролью |
getXp(who?) |
Его уровень: xp, level,
rank, messages,
percent. Не указывайте id, чтобы получить того, кто вызвал команду.
|
mentioned() | Первый id, переданный команде, упоминанием или как есть |
mentions() | Все они, списком |
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 }
]
})
}
Поиск ограничен 25 запросами за запуск, как и запись. Читать весь сервер в цикле — не то, для чего это нужно.
Язык
Переменные
let count = 0 // can change
const name = "Kirzen" // locked, cannot be changed
total = 0 // no keyword at all also works
count += 5
count++
Текст
const a = "double quotes";
const b = 'single quotes';
const c = `a template with ${user.name} in it`;
const d = "joined " + "with plus";
Условия
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"
&&, || и ?? работают как вы ожидаете, включая сокращённое вычисление.
=== и == сравнивают по значению.
Циклы
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--
}
Списки и объекты
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}`);
Функции
function double(n) {
return n * 2
}
const shout = (text) => text.toUpperCase() + "!"
reply(double(21))
reply(shout("hello"))
Комментарии
// a line
# also a line
/* or several
lines */
Встроенные функции
| Функция | Что он делает |
|---|---|
len(x) | Длина текста или списка |
random(a, b) | Целое число от a до b, включительно |
pick(list) | Один элемент из списка, случайно |
range(a, b) | Список чисел от a до b, не включая b |
keys(object) | Имена полей объекта |
sum(list) | Складывает список чисел |
unique(list) | Убирает повторы |
shuffle(list) | Те же элементы в случайном порядке |
json(value) | Превращает что угодно в текст, удобно в работе |
numberFormat(n) | 1234567 превращается в 1 234 567 |
date() | year month day hour minute weekday |
values(object) | Значения полей объекта |
mentionUser(id) | Превращает id в упоминание |
mentionRole(id) | То же для роли |
mentionChannel(id) | То же для канала |
Number(v), String(v), Boolean(v) | Преобразования |
parseInt(v), parseFloat(v) | Считывает число из текста |
str(v), int(v) | Написание тех же двух в стиле Python |
hasRole(id) | Есть ли эта роль у того, кто вызвал команду |
color(name) | Превращает название цвета в число |
now() | Текущее время, в секундах |
timestamp(s, style) |
Время, которое Discord показывает в часовом поясе каждого читателя. Стили:
t T d D f F R
|
truncate(text, n) | Обрезает текст и добавляет многоточие |
bold, italic, underline,
strike, spoiler, quote,
code, codeBlock, link
|
Разметка Discord, без запоминания знаков |
Math.… |
floor, ceil, round,
abs, min, max,
pow, sqrt, random
|
Методы значений
Текст
length toUpperCase toLowerCase
trim includes startsWith
endsWith indexOf charAt
slice split replace
replaceAll repeat padStart
padEnd
Списки
length join includes
indexOf slice concat
push reverse map
filter find some
every reduce sort
Числа
toFixed toString
const names = args.map(a => a.toLowerCase()).filter(a => a.length > 2);
reply(names.sort().join(", "));
Лимиты
Каждый скрипт работает в рамках лимита. Упереться в один из них — не сбой: команда ответит с пометкой, а Kirzen продолжит работу.
| Лимит | Округление вверх |
|---|---|
| Длина скрипта | 20 000 символов |
| Длина ответа | 1900 символов, дальше обрезается |
| Выполнено работы | 200 000 шагов |
| Витков цикла | 10 000 на цикл |
| Вложенные вызовы | 50 уровней вложенности |
| Запрошенные действия | 10 за запуск |
| Показано эмбедов | 3 на команду |
| Полей на эмбед | 25 |
| Чтений и записей в хранилище | 25 за запуск |
| Поиск | 25 за запуск |
| Сохранённых ключей | 2000 на сервер |
| Размер одного значения | 2000 символов |
| Длина списка | 5000 элементов |
| Накопленный текст | 20 000 символов |
Чего здесь нет
Это есть в JavaScript и нет здесь. Каждое исключено либо потому, что стало бы выходом из песочницы, либо потому, что позволило бы подвесить бота, так что ничего из этого добавлено не будет.
eval Function require
import process globalThis
setTimeout fetch new
class this async
await try __proto__
constructor prototype
Чтение .constructor или .__proto__ возвращает пустоту вместо ошибки, а запись в любой из них отклоняется.
Рецепты
Бросок кубика
const sides = int(args[0]) || 6
reply(`${user.mention} rolled a ${random(1, sides)} on a d${sides}.`)
Роль, которую берут сами
addRole("123456789012345678")
reply("Done, you have the role.")
Выбирает один случайно
if (args.length < 2) {
reply("Give me at least two things to choose between.")
} else {
reply(`I pick **${pick(args)}**.`)
}
Аккуратный список
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`);
}
Карточка профиля
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` }
})
Ежедневная награда
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)}.`)
}
Отреагировать и прибрать
react("✅")
deleteMessage()
send(`${user.mention} said: ${input}`)