С чего начать

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 сохраняет место.

Два способа записи

Это одна и та же команда. Фигурные скобки — вариант по умолчанию, и им пользуется вся остальная страница, но двоеточие с отступом делают то же самое там, где вам так удобнее. Стиль выбирается для каждого блока, так что их можно свободно смешивать.

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

Что пришло из Python

ЗаписьЗначит
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)

Что можно опустить

  • Точки с запятой. Не нужны ни в том, ни в другом стиле.
  • Скобки вокруг условия. 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Делает заголовок ссылкой
thumbnailURL изображения, маленьким в углу
imageURL изображения, во всю ширину
authorНазвание или { name, icon, url }
footerТекст или { text, icon }
timestamptrue чтобы поставить текущее время
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}`)