Модулі: як писати власні кроки й команди
Модуль це тека зі скриптом на 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.args | input, розбитий за пробілами |
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це «нічого» (у JSnull).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