Первый плагин: шаг за шагом¶
Напишем плагин, который сообщает оператору в Telegram о каждом новом пользователе и раз в день присылает сводку. По пути встретятся манифест, события, задачи по расписанию, настройки, своя база, запросы в интернет, переводы и тесты — почти всё, из чего состоит любой плагин.
Готовый результат лежит в examples/plugins/new-users.
Нужен только бинарник rospanel (тот же, что стоит на сервере; подойдёт и собранный под ваш
компьютер). Node.js не нужен.
1. Создаём заготовку¶
rospanel plugin new new-users
cd new-users
new-users/
plugin.json манифест: кто плагин, что ему можно, на что он отвечает
main.js код
migrations/0001_init.sql таблицы своей базы
i18n/ru.json, en.json строки для panel.t()
test.js тесты (в пакет не попадают)
dev.config.json настройки для тестов и dev (в пакет не попадают)
rospanel.d.ts типы для редактора (в пакет не попадает)
jsconfig.json подключает типы в VS Code
README.md
Заготовка уже рабочая: rospanel plugin test . проходит. Откройте папку в VS Code —
подсказки по panel.* появятся сами, благодаря rospanel.d.ts.
2. Манифест¶
plugin.json описывает плагин. Перепишите его так:
{
"id": "new-users",
"version": "1.0.0",
"api": 1,
"panel": ">=5.0.0",
"name": {"ru": "Новые пользователи — в Telegram", "en": "New users to Telegram"},
"description": {
"ru": "Пишет в ваш чат Telegram о каждом новом пользователе и раз в день присылает сводку.",
"en": "Tells your Telegram chat about every new user and sends a summary once a day."
},
"author": "you",
"license": "MIT",
"permissions": ["users.view"],
"net": ["api.telegram.org"],
"settings": [
{"key": "bot_token", "kind": "secret", "label": {"ru": "Токен бота", "en": "Bot token"},
"help": {"ru": "Создайте бота у @BotFather", "en": "Create a bot with @BotFather"}},
{"key": "chat_id", "kind": "text", "label": {"ru": "ID чата", "en": "Chat ID"}, "placeholder": "-1001234567890"},
{"key": "lang", "kind": "select", "label": {"ru": "Язык сообщений", "en": "Message language"}, "default": "ru",
"options": [{"value": "ru", "label": "Русский"}, {"value": "en", "label": "English"}]}
],
"provides": {
"events": ["user.created", "user.registered"],
"cron": [{"name": "summary", "schedule": "0 9 * * *"}]
}
}
Что здесь важно:
idникогда не меняется: по нему называется база плагина и записи в журнале панели.permissions— что плагин может делать через API панели и что ему передают. События пользователей несут пользователя, поэтому им нужноusers.view; оператор увидит этот список перед установкой.net— единственные хосты, куда плагин может ходить. Безapi.telegram.orgзапрос в Telegram будет отклонён.settings— форма, которую оператор заполнит после установки.secretхранится зашифрованным и больше никому не показывается.provides— на что плагин отвечает: два события и одна задача, каждый день в 9:00 по часовому поясу панели.
Каждое поле описано в справочнике по манифесту.
3. Своя база¶
migrations/0001_init.sql:
-- Один ряд на каждого объявленного пользователя: это и считает сводка, и не даёт
-- объявить одного человека дважды, если событие придёт повторно.
CREATE TABLE announced (
user_id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
source TEXT NOT NULL,
at INTEGER NOT NULL
);
Миграции применяются по порядку имён при включении плагина. Выпущенную миграцию не меняют —
для изменений добавляют новую (0002_….sql).
4. Строки¶
i18n/ru.json:
{
"new_user": "🆕 Новый пользователь: {name} (#{id})",
"self_registered": "зарегистрировался сам",
"created": "создан в панели",
"summary": "📊 За сутки новых пользователей: {count}"
}
И то же по-английски в i18n/en.json. panel.t("new_user", {name, id}, "ru") подставит
значения в {name} и {id}.
5. Код¶
main.js — один ES-модуль. Панель вызывает то, что он экспортирует:
// Пишет в Telegram оператора о новых пользователях и раз в день — сколько их пришло.
/** Отправляет сообщение через бота оператора; бросает ошибку, если Telegram отказал. */
function send(text) {
const res = panel.http.fetch(`https://api.telegram.org/bot${panel.config.bot_token}/sendMessage`, {
method: "POST",
body: { chat_id: panel.config.chat_id, text },
});
if (res.status !== 200) throw new Error(`Telegram answered ${res.status}: ${res.body.slice(0, 200)}`);
}
/** @param {PanelEvent<{id: number, name: string}>} e */
export function onEvent(e) {
const lang = panel.config.lang;
const user = e.data;
// Одна транзакция: если Telegram не ответил, ряд тоже откатится, и повторная доставка
// события отправит сообщение снова. Событие, пришедшее дважды, найдёт ряд (первичный
// ключ) и ничего не отправит.
panel.db.tx(() => {
const { changes } = panel.db.exec(
"INSERT OR IGNORE INTO announced(user_id, name, source, at) VALUES (?, ?, ?, ?)",
user.id, user.name, e.event, e.created_at,
);
if (changes === 0) return;
const how = panel.t(e.event === "user.registered" ? "self_registered" : "created", {}, lang);
send(`${panel.t("new_user", { name: user.name, id: String(user.id) }, lang)}\n${how}`);
});
}
export function summary() {
const since = Math.floor(Date.now() / 1000) - 86400;
const [{ n }] = panel.db.query("SELECT count(*) AS n FROM announced WHERE at >= ?", since);
send(panel.t("summary", { count: String(n) }, panel.config.lang));
}
Три вещи, которые надо понять про то, как панель вызывает код:
- Всё синхронно.
panel.http.fetchвозвращает ответ сразу — безawait, колбэков и таймеров. Ждать времени (setTimeout) нельзя. - Событие может прийти больше одного раза. Доставка «хотя бы один раз»: при сбое панель повторит её. Поэтому плагин помнит, кого уже объявил.
- Ошибка — это повтор. Если
onEventбросил исключение, панель доставит событие снова через 10 с, 30 с, 2 мин и 10 мин. Транзакция выше нужна ровно для этого: запись о пользователе появляется только вместе с отправленным сообщением.
Про panel.* — в справочнике по API, про события и остальные точки — в
справочнике по точкам расширения.
6. Тесты¶
test.js запускается рядом с плагином; сеть и API панели в нём настоящими не бывают — на всё
отвечают моки:
const tg = "https://api.telegram.org/bot123:abc/sendMessage";
test("a new user is announced once", () => {
mock.http(tg, { body: { ok: true } });
plugin.event("user.registered", { id: 7, name: "Ann" });
plugin.event("user.registered", { id: 7, name: "Ann" }); // то же событие ещё раз
const sent = mock.calls().filter((c) => c.kind === "http");
assert.equal(sent.length, 1);
assert.equal(JSON.parse(sent[0].body), {
chat_id: "-100500",
text: "🆕 Новый пользователь: Ann (#7)\nзарегистрировался сам",
});
});
test("after a Telegram error the retry sends it", () => {
mock.http(tg, { status: 502, body: "bad gateway" });
assert.throws(() => plugin.event("user.created", { id: 9, name: "Bob" }));
mock.reset();
mock.http(tg, { body: { ok: true } });
plugin.event("user.created", { id: 9, name: "Bob" }); // панель доставляет его снова
assert.equal(mock.calls().length, 1);
});
Настройки для тестов — в dev.config.json:
{"settings": {"bot_token": "123:abc", "chat_id": "-100500", "lang": "ru"}}
rospanel plugin test .
✓ a new user is announced once
✓ after a Telegram error the retry sends it
2 passed, 0 failed
Все возможности тестов — в справочнике по тестам.
7. Пробуем руками¶
rospanel plugin dev .
new-users is running. Type help for commands.
> mock http https://api.telegram.org/ {"ok": true}
> event user.created {"id": 1, "name": "Ann"}
> sql SELECT * FROM announced
> logs
dev перезагружает плагин при каждом сохранении файла. Запросы, которые не замоканы, уходят в
интернет по-настоящему — так можно проверить настоящего бота, вписав его токен в
dev.config.json.
8. Пакет и установка¶
rospanel plugin pack .
new-users-1.0.0.zip — 2 KB, sha256 …
not packed: dev.config.json, jsconfig.json, rospanel.d.ts, test.js
В панели: Настройки → Плагины → Добавить → выберите zip. Панель покажет экран согласия: права, хосты, что плагин делает. После установки плагин выключен — заполните настройки (шестерёнка на карточке: токен, чат) и включите его. Пока чего-то не хватает, у названия горит оранжевая точка — наведите на неё, она скажет, чего. Создайте пользователя — в чат придёт сообщение. Лог плагина показывает, что он делал и где упал.
Что дальше¶
- Кнопки в карточке пользователя, виджеты, блоки на странице подписки, свой способ оплаты, кнопки в боте — точки расширения.
- Обновления, миграции и как поделиться плагином — публикация.
- Лимиты и разбор частых ошибок — лимиты и ошибки.
- Другие готовые плагины — examples/plugins.