Модулі: як писати власні кроки й команди

Модуль це тека зі скриптом на JavaScript чи Lua і маніфестом module.json. Він додає в застосунок нові кроки дій (меню «Додати крок» → «Модулі») і команди чату. Мову вибирає поле language (js чи lua) у маніфесті.

Установка

Сторінка Модулі в бічній панелі: «Встановити з теки…» (тека з module.json) або покладіть теку в теку модулів (кнопка «Відкрити теку модулів») і поверніться на сторінку. Встановлений модуль вимкнений: кнопка «Увімкнути» показує, які дозволи він просить, і вмикає його лише після вашої згоди. Повторна установка того ж модуля потребує згоди знову.

Готові приклади з кодом і архівами: розділ «Приклади».

Маніфест

{
  "id": "dice",
  "name": "Кубик",
  "version": "1.0.0",
  "apiVersion": 1,
  "language": "js",
  "main": "main.js",
  "author": "...",
  "description": "...",
  "permissions": ["chat.send", "events", "overlay"],
  "steps": [
    { "id": "roll", "label": "Кинути кубик", "function": "roll",
      "props": [ { "key": "sides", "kind": "number", "default": "6", "label": "Граней", "min": 2, "max": 1000 } ] }
  ],
  "commands": [ { "name": "!кубик", "function": "onDice" } ],
  "events": [ { "on": "follow", "function": "onFollow" } ],
  "values": [ { "key": "last", "label": "Останній кидок" } ]
}
  • id: латиниця в нижньому регістрі, цифри й дефіс, 3–40 символів; тека модуля називається так само.
  • apiVersion: версія API, з якою писали модуль (зараз 1). Новішу, ніж знає застосунок, модуль не запустить.
  • main: файл скрипта поруч із маніфестом (.js).
  • steps[].props[].kind: text, number (з min/max), toggle, choice (з choices).
  • Не більше 50 кроків, 20 полів у кроці, 20 команд, 30 обробників подій, 50 значень. Усі помилки маніфесту видно на сторінці «Модулі».

Функції

Кожен крок і команда це функція вашого скрипта. Вона отримує один аргумент ctx:

ПолеЩо це
ctx.source"step" чи "command"
ctx.userхто запустив (логін)
ctx.inputтекст, з яким запустили (для команди все після її назви)
ctx.argsinput, розбитий за пробілами
ctx.propsзначення полів кроку (теги вже підставлено)

Крок: повернене значення стає $(result) для наступних кроків. Команда: повернений рядок іде в чат від імені бота (потрібен дозвіл chat.send). Помилка скрипта це збій кроку: її видно в історії дій і в журналі модуля.

Значення для оверлею

overlay.state(key, value) (дозвіл overlay) зберігає значення, яке стример показує в лейблі: джерело «Значення модуля» й ключ <id модуля>:<key>. Щоб редактор запропонував його зі списку, оголосіть ключ у values маніфесту (key і label). Оголошення лише підказка: ключ, не вказаний у values, теж працює, але його доведеться вписати руками.

Події каналу

Обробник events[] (потрібен дозвіл events) викликається, коли в каналі стається подія: follow, subscription, resub, gift-sub, raid, redeem, chat, donation, bits, power-up. Функція отримує ctx.source == "event" і ctx.event: kind, user та решту даних події (наприклад amount, message). Тестові події з вкладки «Сповіщення» приходять із ctx.event.test == "true"; повтори з історії оверлею не приходять. Події модуля йдуть по одній, у порядку надходження; якщо обробник повільний і в черзі більше 50 подій, нові відкидаються (про це є запис у журналі). Чат (chat) надходить на кожне повідомлення, тож тримайте такий обробник коротким.

API

ФункціяДозвілЩо робить
chat.send(text)chat.sendповідомлення в чат
vars.get(name, scope), vars.set(name, value, scope)varsзмінні дій; scope: "global" (типово) чи "user" (змінні поточного глядача)
actions.run(id, input)actions.runзапустити дію з бібліотеки (не чекає її кінця)
overlay.state(key, value)overlayзберегти значення для оверлею; щоб показати його, у лейблі оберіть джерело «Значення модуля» й впишіть <id модуля>:<key> (наприклад dice:last)
store.get(key), store.set(key, value)немаєвласне сховище модуля (будь-який JSON), переживає перезапуск; до 500 записів
log.info/warn/error(text)немаєжурнал модуля на сторінці «Модулі»
sleep(ms)немаєпауза (до 5 с за виклик)

Виклик без підтвердженого дозволу кидає помилку з назвою дозволу. Секретів застосунку (токени, ключі) скрипт не бачить.

Lua

API, дозволи й ліміти ті самі, що в JS: у вашому скрипті ті самі глобальні таблиці chat, vars, actions, store, overlay, log і функція sleep. Різниця лише в мові:

  • ctx.args і будь-які масиви це таблиці Lua, що індексуються з 1: ctx.args[1] перший аргумент, #ctx.args їх кількість.
  • nil це «нічого» (у JS null). store.get для відсутнього ключа повертає nil.
  • Значення, які повертає функція чи кладеш у store: рядки, числа, true/false, nil і таблиці. Таблиця без ключів-рядків і з послідовними числовими ключами 1..n це масив, інакше об'єкт. Функції й цикли в таблицях не зберігаються (помилка).
  • Доступні лише бібліотеки math, string, table і базові функції. Немає os, io, debug, package, require, dofile, loadfile, load, collectgarbage, print (для виводу використовуйте log.info).
  • Глобальні змінні скрипта живуть між викликами, як і в JS; усе, що має пережити збій чи перезапуск, кладіть у store.
  • Ліміт пам'яті в Lua грубий: рахуються алокації за виклик (до 8 лімітів пам'яті). Скрипт, що одним махом будує гігантський рядок (s = s .. s у циклі), може на кілька секунд забрати гігабайти пам'яті, перш ніж .NET його зупинить. Пишіть без таких прийомів.

Обмеження й безпека

  • Скрипт працює в пісочниці: без доступу до файлів, процесів, системи; без eval і new Function.
  • Ліміти одного виклику: 5 с часу, ~32 МБ пам'яті (у Lua грубо, див. вище), 2 млн операцій, рекурсія до 64 (у Lua глибина обмежена самим рушієм). Перевищення це збій кроку.
  • Після 5 збоїв поспіль модуль автоматично вимикається (причина в його журналі).
  • Команда одного глядача викликається не частіше, ніж раз на 2 секунди.
  • Стан скрипта (глобальні змінні) живе між викликами, але скидається при збої, перевстановленні чи «Перезавантажити». Усе, що має пережити це, зберігайте через store.
  • Мережі (net) поки нема в API; дозвіл існує для наступних версій.

Розробка

  • Змінили main.js? Наступний виклик сам перечитає скрипт, кнопка «Перезавантажити» не обов'язкова.
  • Журнал модуля на сторінці «Модулі» показує log.* і помилки.
  • Перевірте крок кнопкою «Тест» у дії, яка його використовує; команду напишіть у чаті.

Приклади

Кожен приклад це готовий модуль. Завантажте архів, розпакуйте його (вийде тека з назвою модуля), на сторінці «Модулі» натисніть «Встановити з теки…» й оберіть цю теку. Далі модуль треба ввімкнути, погодивши дозволи.

Кубик (JavaScript): крок і команда

Завантажити: dice.zip

dice/module.json

{
  "id": "dice",
  "name": "Кубик",
  "version": "1.0.0",
  "apiVersion": 1,
  "language": "js",
  "main": "main.js",
  "author": "BotValeriy",
  "description": "Крок «Кинути кубик» і команда !кубик. Приклад модуля: рахує, скільки разів кидали, у власному сховищі.",
  "permissions": ["chat.send", "overlay"],
  "steps": [
    {
      "id": "roll",
      "label": "Кинути кубик",
      "hint": "Кидає кубик із заданою кількістю граней; результат лягає в $(result)",
      "function": "roll",
      "props": [
        { "key": "sides", "kind": "number", "default": "6", "label": "Граней", "min": 2, "max": 1000 }
      ]
    }
  ],
  "values": [ { "key": "last", "label": "Останній кидок" } ],
  "commands": [
    { "name": "!кубик", "function": "onDice" }
  ]
}

dice/main.js

// Приклад модуля «Кубик». / Example module "Dice".
//
// Функції отримують один аргумент ctx: { source, user, input, args, props }.
// Every function gets one argument, ctx: { source, user, input, args, props }.
//
// Повернене значення кроку стає $(result) для наступних кроків; повернений текст команди йде в чат (потрібен дозвіл chat.send).
// A step's return value becomes $(result) for the next steps; a command's returned text goes to chat (needs the chat.send permission).

function clampSides(value) {
  var sides = parseInt(value, 10);
  if (!sides || sides < 2) {
    return 6;
  }
  return Math.min(sides, 1000);
}

function rollDice(sides, user) {
  var value = 1 + Math.floor(Math.random() * sides);

  // Власне сховище модуля: дозволу не потребує й переживає перезапуск застосунку.
  // The module's own store: needs no permission and survives an app restart.
  var stats = store.get('stats') || { rolls: 0 };
  stats.rolls += 1;
  store.set('stats', stats);

  // Значення для оверлею: у лейблі джерело «Значення модуля» → dice:last (оголошено в module.json, values).
  // A value for the overlay: in a label pick the "Module value" source → dice:last (declared in module.json, values).
  overlay.state('last', String(value));

  log.info(user + ' кинув d' + sides + ' і отримав ' + value + ' (усього кидків: ' + stats.rolls + ')');
  return value;
}

// Крок дії «Кинути кубик»: число граней береться з поля кроку.
// The "Roll a die" action step: the number of sides comes from the step's field.
function roll(ctx) {
  return String(rollDice(clampSides(ctx.props.sides), ctx.user));
}

// Команда !кубик [граней]: відповідь іде в чат.
// The !кубик [sides] command: the reply goes to chat.
function onDice(ctx) {
  var sides = clampSides(ctx.args[0]);
  return ctx.user + ' кинув d' + sides + ': ' + rollDice(sides, ctx.user);
}

Кубик (Lua): те саме на Lua

Завантажити: dice-lua.zip

dice-lua/module.json

{
  "id": "dice-lua",
  "name": "Кубик (Lua)",
  "version": "1.0.0",
  "apiVersion": 1,
  "language": "lua",
  "main": "main.lua",
  "author": "BotValeriy",
  "description": "Той самий кубик, що й у прикладі на JavaScript, але на Lua: крок «Кинути кубик (Lua)» і команда !кубик2.",
  "permissions": ["chat.send", "overlay"],
  "steps": [
    {
      "id": "roll",
      "label": "Кинути кубик (Lua)",
      "hint": "Кидає кубик із заданою кількістю граней; результат лягає в $(result)",
      "function": "roll",
      "props": [
        { "key": "sides", "kind": "number", "default": "6", "label": "Граней", "min": 2, "max": 1000 }
      ]
    }
  ],
  "values": [ { "key": "last", "label": "Останній кидок" } ],
  "commands": [
    { "name": "!кубик2", "function": "onDice" }
  ]
}

dice-lua/main.lua

-- Приклад модуля «Кубик» на Lua. / Example module "Dice" in Lua.
--
-- Функції отримують один аргумент ctx: { source, user, input, args, props }. args і props це таблиці;
-- args індексується з 1, як звичайні масиви Lua.
-- Every function gets one argument, ctx: { source, user, input, args, props }. args and props are tables;
-- args is indexed from 1, like ordinary Lua arrays.
--
-- Повернене значення кроку стає $(result); повернений текст команди йде в чат (потрібен дозвіл chat.send).
-- A step's return value becomes $(result); a command's returned text goes to chat (needs the chat.send permission).

local function clampSides(value)
  local sides = math.floor(tonumber(value) or 6)
  if sides < 2 then
    return 6
  end
  return math.min(sides, 1000)
end

local function rollDice(sides, user)
  local value = math.random(1, sides)

  -- Власне сховище модуля: дозволу не потребує й переживає перезапуск застосунку.
  -- The module's own store: needs no permission and survives an app restart.
  local stats = store.get('stats') or { rolls = 0 }
  stats.rolls = stats.rolls + 1
  store.set('stats', stats)

  -- Значення для оверлею: у лейблі джерело «Значення модуля» → dice-lua:last (оголошено в module.json, values).
  -- A value for the overlay: in a label pick the "Module value" source → dice-lua:last (declared in module.json, values).
  overlay.state('last', tostring(value))

  log.info(user .. ' кинув d' .. sides .. ' і отримав ' .. value .. ' (усього кидків: ' .. stats.rolls .. ')')
  return value
end

-- Крок дії «Кинути кубик (Lua)»: число граней береться з поля кроку.
-- The "Roll a die (Lua)" action step: the number of sides comes from the step's field.
function roll(ctx)
  return tostring(rollDice(clampSides(ctx.props.sides), ctx.user))
end

-- Команда !кубик2 [граней]: відповідь іде в чат.
-- The !кубик2 [sides] command: the reply goes to chat.
function onDice(ctx)
  local sides = clampSides(ctx.args[1])
  return ctx.user .. ' кинув d' .. sides .. ': ' .. rollDice(sides, ctx.user)
end

Вітання (JavaScript): події каналу й значення для оверлея

Завантажити: welcome.zip

welcome/module.json

{
  "id": "welcome",
  "name": "Вітання",
  "version": "1.0.0",
  "apiVersion": 1,
  "language": "js",
  "main": "main.js",
  "author": "BotValeriy",
  "description": "Дякує в чаті за фол і привітає рейд. Приклад обробників подій: рахує нових глядачів і виставляє останнього фоловера для оверлея.",
  "permissions": ["chat.send", "events", "overlay"],
  "events": [
    { "on": "follow", "function": "onFollow" },
    { "on": "raid", "function": "onRaid" }
  ],
  "values": [
    { "key": "last-follower", "label": "Останній фоловер" }
  ]
}

welcome/main.js

// Приклад модуля «Вітання»: обробники подій каналу. / Example module "Welcome": channel event handlers.
//
// Подія приходить у ctx.event: { kind, user, ...решта даних події }. Тестові події з вкладки «Сповіщення» мають ctx.event.test == "true".
// The event arrives in ctx.event: { kind, user, ...the rest of the event data }. Test events from the Alerts tab have ctx.event.test == "true".
//
// Потрібні дозволи: events (читати події), chat.send (писати в чат), overlay (показати значення в лейблі оверлея).
// Permissions needed: events (read events), chat.send (write to chat), overlay (show a value in an overlay label).

function onFollow(ctx) {
  // Власне сховище переживає перезапуск застосунку.
  // The module's own store survives an app restart.
  var follows = (store.get('follows') || 0) + 1;
  store.set('follows', follows);

  // У лейблі оверлея: джерело «Значення модуля» → welcome:last-follower (оголошено в module.json, values).
  // In an overlay label: the "Module value" source → welcome:last-follower (declared in module.json, values).
  overlay.state('last-follower', ctx.event.user);

  chat.send('Дякуємо за фолов, ' + ctx.event.user + '! Нових глядачів: ' + follows + '.');
}

// Рейд: просто вітаємо в чаті.
// A raid: just greet in chat.
function onRaid(ctx) {
  chat.send('Рейд від ' + ctx.event.user + '! Ласкаво просимо!');
}

Вітання (Lua): те саме на Lua

Завантажити: welcome-lua.zip

welcome-lua/module.json

{
  "id": "welcome-lua",
  "name": "Вітання (Lua)",
  "version": "1.0.0",
  "apiVersion": 1,
  "language": "lua",
  "main": "main.lua",
  "author": "BotValeriy",
  "description": "Дякує в чаті за фол і привітає рейд. Приклад обробників подій: рахує нових глядачів і виставляє останнього фоловера для оверлея.",
  "permissions": ["chat.send", "events", "overlay"],
  "events": [
    { "on": "follow", "function": "onFollow" },
    { "on": "raid", "function": "onRaid" }
  ],
  "values": [
    { "key": "last-follower", "label": "Останній фоловер" }
  ]
}

welcome-lua/main.lua

-- Приклад модуля «Вітання» на Lua: обробники подій каналу. / Example module "Welcome" in Lua: channel event handlers.
--
-- Подія приходить у ctx.event: таблиця { kind, user, ... }. Тестові події з вкладки «Сповіщення» мають ctx.event.test == "true".
-- The event arrives in ctx.event: a table { kind, user, ... }. Test events from the Alerts tab have ctx.event.test == "true".
--
-- Потрібні дозволи: events (читати події), chat.send (писати в чат), overlay (показати значення в лейблі оверлея).
-- Permissions needed: events (read events), chat.send (write to chat), overlay (show a value in an overlay label).

function onFollow(ctx)
  -- Власне сховище переживає перезапуск застосунку.
  -- The module's own store survives an app restart.
  local follows = (store.get('follows') or 0) + 1
  store.set('follows', follows)

  -- У лейблі оверлея: джерело «Значення модуля» → welcome-lua:last-follower (оголошено в module.json, values).
  -- In an overlay label: the "Module value" source → welcome-lua:last-follower (declared in module.json, values).
  overlay.state('last-follower', ctx.event.user)

  chat.send('Дякуємо за фолов, ' .. ctx.event.user .. '! Нових глядачів: ' .. follows .. '.')
end

-- Рейд: просто вітаємо в чаті.
-- A raid: just greet in chat.
function onRaid(ctx)
  chat.send('Рейд від ' .. ctx.event.user .. '! Ласкаво просимо!')
end
Модулі: свої кроки й команди · Valeriy