feat: перенос демона Telegram-бота с Go на Ucode, добавление вкладки настроек LuCI и локализации

This commit is contained in:
Dushnilin
2026-07-13 15:59:29 +03:00
parent b3bfcea547
commit bafe70dd66
8 changed files with 487 additions and 0 deletions
@@ -681,8 +681,93 @@ function createSettingsContent(section, capabilities) {
o.rmempty = false;
}
function createTelegramContent(section) {
let o = section.option(
form.Flag,
"enabled",
_("Enable Telegram Bot"),
_("Enables the background daemon that polls Telegram for commands and sends notifications."),
);
o.default = "0";
o.rmempty = false;
o = section.option(
form.Value,
"bot_token",
_("Telegram Bot Token"),
_("Enter the API token obtained from @BotFather."),
);
o.depends("enabled", "1");
o.password = true;
o.rmempty = false;
o.validate = function (section_id, value) {
if (this.section.formvalue(section_id, "enabled") === "1" && !value) {
return _("Token is required when the bot is enabled");
}
return true;
};
o = section.option(
form.Value,
"admin_ids",
_("Administrator Chat IDs"),
_("Comma-separated list of Telegram user IDs authorized to control the bot."),
);
o.depends("enabled", "1");
o.rmempty = false;
o.validate = function (section_id, value) {
if (this.section.formvalue(section_id, "enabled") === "1") {
if (!value) {
return _("At least one Admin Chat ID is required");
}
if (!/^[0-9]+(,[0-9]+)*$/.test(value)) {
return _("Must be a comma-separated list of numeric IDs");
}
}
return true;
};
o = section.option(
form.Value,
"poll_interval",
_("Polling Interval (seconds)"),
_("How often to check Telegram for new messages (default: 5)."),
);
o.depends("enabled", "1");
o.default = "5";
o.rmempty = false;
o.validate = function (section_id, value) {
const val = parseInt(value);
if (isNaN(val) || val < 1) {
return _("Polling interval must be at least 1 second");
}
return true;
};
o = section.option(
form.Flag,
"notify_crash",
_("Notify on Core Crashes"),
_("Send a Telegram notification if sing-box or nftables rules crash and get auto-restored."),
);
o.depends("enabled", "1");
o.default = "1";
o.rmempty = false;
o = section.option(
form.Flag,
"notify_restart",
_("Notify on Service Restarts"),
_("Send a Telegram notification if the Tachyon service is restarted by the watchdog."),
);
o.depends("enabled", "1");
o.default = "1";
o.rmempty = false;
}
const EntryPoint = {
createSettingsContent,
createTelegramContent,
};
return baseclass.extend(EntryPoint);
@@ -431,6 +431,18 @@ const EntryPoint = {
};
settings.createSettingsContent(settingsSection, uiCapabilities);
const telegramSection = tachyonMap.section(
form.TypedSection,
"telegram",
_("Telegram Bot"),
);
telegramSection.anonymous = true;
telegramSection.addremove = false;
telegramSection.cfgsections = function () {
return ["telegram"];
};
settings.createTelegramContent(telegramSection);
const diagnosticSection = tachyonMap.section(
form.TypedSection,
"diagnostic",
+51
View File
@@ -1877,6 +1877,57 @@ msgstr "Tailscale endpoint"
msgid "Tailscale hostname"
msgstr "Hostname Tailscale"
msgid "Telegram Bot"
msgstr "Telegram бот"
msgid "Enable Telegram Bot"
msgstr "Включить Telegram бот"
msgid "Enables the background daemon that polls Telegram for commands and sends notifications."
msgstr "Включает фоновую службу, которая опрашивает Telegram на предмет команд и отправляет уведомления."
msgid "Telegram Bot Token"
msgstr "Токен Telegram-бота"
msgid "Enter the API token obtained from @BotFather."
msgstr "Введите API токен, полученный от @BotFather."
msgid "Token is required when the bot is enabled"
msgstr "Токен обязателен при включенном боте"
msgid "Administrator Chat IDs"
msgstr "Chat ID администраторов"
msgid "Comma-separated list of Telegram user IDs authorized to control the bot."
msgstr "Список ID пользователей Telegram (через запятую), которым разрешено управлять ботом."
msgid "At least one Admin Chat ID is required"
msgstr "Требуется указать как минимум один Chat ID администратора"
msgid "Must be a comma-separated list of numeric IDs"
msgstr "Должен быть списком числовых ID через запятую"
msgid "Polling Interval (seconds)"
msgstr "Интервал опроса (секунды)"
msgid "How often to check Telegram for new messages (default: 5)."
msgstr "Как часто опрашивать Telegram о наличии новых сообщений (по умолчанию: 5)."
msgid "Polling interval must be at least 1 second"
msgstr "Интервал опроса должен быть не менее 1 секунды"
msgid "Notify on Core Crashes"
msgstr "Уведомлять о падениях ядра"
msgid "Send a Telegram notification if sing-box or nftables rules crash and get auto-restored."
msgstr "Отправлять уведомление в Telegram, если sing-box или правила nftables падают и восстанавливаются."
msgid "Notify on Service Restarts"
msgstr "Уведомлять о перезапуске служб"
msgid "Send a Telegram notification if the Tachyon service is restarted by the watchdog."
msgstr "Отправлять уведомление в Telegram, если служба Tachyon перезапускается сторожевым таймером."
msgid "Test latency"
msgstr "Тестирование задержки"
+12
View File
@@ -158,3 +158,15 @@ config settings 'settings'
# option tailscale_accept_routes '0'
# option tailscale_advertise_exit_node '1'
# #list tailscale_advertise_routes '192.168.1.0/24'
#config telegram 'telegram'
# option enabled '0'
# option bot_token ''
# option admin_ids ''
# option poll_interval '5'
# option notify_crash '1'
# option notify_restart '1'
# option notify_server_switch '1'
# option notify_subscription '1'
# option notify_cert '1'
# option notify_dns_leak '1'
+1
View File
@@ -197,6 +197,7 @@ function command_spec(command) {
doctor: [ "diagnostics/runtime.uc", "doctor", 1 ],
ai_doctor: [ "diagnostics/runtime.uc", "ai-doctor", 0 ],
watchdog: [ "service/watchdog.uc", "", 1 ],
telegram: [ "service/telegram.uc", "", 2 ],
validate_nfqws_strategy_json: [ "diagnostics/runtime.uc", "validate-nfqws-strategy-json", 1 ],
validate_nfqws2_strategy_json: [ "diagnostics/runtime.uc", "validate-nfqws2-strategy-json", 1 ],
validate_byedpi_strategy_json: [ "providers/byedpi/validator.uc", "validate-json", 1 ],
@@ -112,6 +112,7 @@ const ZAPRET2_UC = LIB_DIR + "/providers/zapret2/runtime.uc";
const BYEDPI_UC = LIB_DIR + "/providers/byedpi/runtime.uc";
const PACKAGES_UC = LIB_DIR + "/core/packages.uc";
const WATCHDOG_UC = LIB_DIR + "/service/watchdog.uc";
const TELEGRAM_UC = LIB_DIR + "/service/telegram.uc";
let start_subscription_update_lock_held = false;
let subscription_caches_prepared = getenv("TACHYON_SUBSCRIPTION_CACHES_PREPARED") || "0";
@@ -787,6 +788,11 @@ function start_impl() {
return status;
}
status = module_status(TELEGRAM_UC, [ "start-runtime" ]);
if (status != 0) {
log_message("Failed to start Telegram Bot runtime", "warn");
}
module_background(DIAGNOSTICS_UC, [ "get-system-info" ]);
return 0;
}
@@ -797,6 +803,7 @@ function stop_main() {
log_message("Stopping Tachyon", "info");
module_success(DNS_FAILOVER_UC, [ "stop-runtime" ]);
module_success(WATCHDOG_UC, [ "stop-runtime" ]);
module_success(TELEGRAM_UC, [ "stop-runtime" ]);
module_success(PRIORITY_UC, [ "stop-runtime" ]);
module_success(SUBSCRIPTION_CACHE_UC, [ "stop-deferred-bootstrap-worker" ]);
module_success(UPDATES_UC, [ "stop-list-update" ]);
+303
View File
@@ -0,0 +1,303 @@
#!/usr/bin/env ucode
let fs = require("fs");
let uci_core = require("core.uci");
let common = require("core.common");
const CONFIG_NAME = getenv("TACHYON_CONFIG_NAME") || "tachyon";
const LIB_DIR = getenv("TACHYON_LIB") || "/usr/lib/tachyon";
const PID_FILE = "/var/run/tachyon_telegram.pid";
const OFFSET_FILE = "/var/run/tachyon_telegram_offset";
let as_string = common.as_string;
let shell_quote = common.shell_quote;
function command_from_args(args) {
let result = [];
for (let arg in args)
push(result, shell_quote(arg));
return join(" ", result);
}
function command_status(command) {
let status = int(system(command));
return status > 255 ? int(status / 256) : status;
}
function command_success_from_args(args) {
return command_status(command_from_args(args) + " >/dev/null 2>&1") == 0;
}
function command_capture(command) {
let pipe = fs.popen(command, "r");
if (!pipe)
return { status: 1, output: "" };
let data = pipe.read("all");
let status = pipe.close();
status = status > 255 ? int(status / 256) : status;
return { status, output: data == null ? "" : as_string(data) };
}
function settings() {
return common.object_or_empty(uci_core.get_all(CONFIG_NAME, "telegram"));
}
function get_proxy_args() {
if (command_success_from_args(["pidof", "sing-box"])) {
return [ "--proxy", "http://127.0.0.1:4534" ];
}
return [];
}
function tg_request(token, method, payload) {
if (!token) return null;
let url = "https://api.telegram.org/bot" + token + "/" + method;
let payload_path = "/tmp/tg_payload_" + method + ".json";
fs.writefile(payload_path, json(payload));
let args = [ "curl", "-s", "-X", "POST", "-H", "Content-Type: application/json", "-d", "@" + payload_path ];
let proxy = get_proxy_args();
for (let p in proxy) {
push(args, p);
}
push(args, url);
let res = command_capture(command_from_args(args));
fs.unlink(payload_path);
if (res.status != 0 || res.output == "") {
return null;
}
try {
return json(res.output);
}
catch (e) {
return null;
}
}
function send_message(token, chat_id, text, parse_mode) {
let payload = {
chat_id: int(chat_id),
text: text
};
if (parse_mode) {
payload.parse_mode = parse_mode;
}
return tg_request(token, "sendMessage", payload);
}
function is_admin(chat_id, admin_ids_str) {
if (!admin_ids_str) return false;
let admins = split(admin_ids_str, /,/);
for (let admin in admins) {
if (trim(admin) == as_string(chat_id)) return true;
}
return false;
}
function get_system_status() {
let status_obj = {};
// CPU
let stat = fs.readfile("/proc/loadavg") || "";
let load = split(stat, / /);
status_obj.cpu = (length(load) > 0) ? load[0] : "unknown";
// RAM
let mem = fs.readfile("/proc/meminfo") || "";
let total_kb = 0, avail_kb = 0;
for (let line in split(mem, "\n")) {
if (index(line, "MemTotal:") == 0) {
total_kb = int(split(trim(line), /[ \t]+/)[1]);
}
else if (index(line, "MemAvailable:") == 0) {
avail_kb = int(split(trim(line), /[ \t]+/)[1]);
}
}
status_obj.ram_total = total_kb ? int(total_kb / 1024) : 0;
status_obj.ram_avail = avail_kb ? int(avail_kb / 1024) : 0;
// Uptime
let upt = fs.readfile("/proc/uptime") || "";
let up_sec = double(split(upt, / /)[0] || 0);
let d = int(up_sec / 86400);
let h = int((up_sec - (d * 86400)) / 3600);
let m = int((up_sec - (d * 86400) - (h * 3600)) / 60);
status_obj.uptime = sprintf("%d дн. %02d:%02d", d, h, m);
// sing-box status
let sb_running = command_success_from_args(["pidof", "sing-box"]);
status_obj.singbox = sb_running ? "🟢 running" : "🔴 stopped";
return status_obj;
}
function process_command(token, chat_id, text) {
let cmd = trim(as_string(text));
if (cmd == "/start" || cmd == "/help") {
let help_text = "🤖 *Tachyon Bot* — Панель управления роутером\n\n" +
"Доступные команды:\n" +
"⚡ /status — Статус системы и RAM/CPU\n" +
"🩺 /doctor — Диагностика и ремонт ошибок\n" +
"🔄 /restart — Перезапустить Tachyon\n" +
"📋 /logs — Посмотреть последние логи\n";
send_message(token, chat_id, help_text, "Markdown");
}
else if (cmd == "/status") {
let sys = get_system_status();
let status_text = "📊 *Статус Tachyon роутера*\n\n" +
"Uptime: `" + sys.uptime + "`\n" +
"CPU Load: `" + sys.cpu + "`\n" +
"RAM: `" + sys.ram_avail + "MB / " + sys.ram_total + "MB` свободного\n" +
"sing-box: `" + sys.singbox + "`";
send_message(token, chat_id, status_text, "Markdown");
}
else if (cmd == "/doctor") {
send_message(token, chat_id, "⏳ *Запуск диагностики и авто-исправления...*");
let doctor_res = command_capture("/usr/bin/tachyon doctor");
let report = doctor_res.output || "Нет вывода диагностики.";
send_message(token, chat_id, "🩺 *Результаты Tachyon Doctor:*\n\n```\n" + report + "\n```", "Markdown");
}
else if (cmd == "/restart") {
send_message(token, chat_id, "🔄 *Перезапускаю службы Tachyon...*");
let restart_status = command_status("/usr/bin/tachyon restart");
if (restart_status == 0) {
send_message(token, chat_id, "✅ *Перезапуск выполнен успешно!*");
} else {
send_message(token, chat_id, "❌ *Ошибка при перезапуске служб.*");
}
}
else if (cmd == "/logs") {
let logs_res = command_capture("logread -e tachyon | tail -n 25");
let logs = logs_res.output || "Логи отсутствуют.";
send_message(token, chat_id, "📋 *Последние логи Tachyon:*\n\n```\n" + logs + "\n```", "Markdown");
}
else {
send_message(token, chat_id, "⚠️ Неизвестная команда. Введите /help для списка команд.");
}
}
function process_updates(token, admin_ids) {
let offset = int(trim(fs.readfile(OFFSET_FILE) || "0"));
let res = tg_request(token, "getUpdates", { offset: offset, timeout: 20 });
if (!res || !res.ok || !res.result || length(res.result) == 0) {
return;
}
for (let upd in res.result) {
let update_id = upd.update_id;
if (update_id >= offset) {
offset = update_id + 1;
fs.writefile(OFFSET_FILE, as_string(offset));
}
let msg = upd.message;
if (!msg || !msg.text) continue;
let chat_id = msg.chat.id;
if (!is_admin(chat_id, admin_ids)) {
send_message(token, chat_id, "❌ Доступ запрещен. Ваш Chat ID: `" + chat_id + "`", "Markdown");
continue;
}
process_command(token, chat_id, msg.text);
}
}
function worker() {
let cfg = settings();
if (cfg.enabled != "1" || !cfg.bot_token || !cfg.admin_ids) {
return 0;
}
let poll_interval = int(cfg.poll_interval || "5");
if (poll_interval < 1) poll_interval = 1;
while (true) {
cfg = settings();
if (cfg.enabled != "1") {
break;
}
process_updates(cfg.bot_token, cfg.admin_ids);
sleep(poll_interval * 1000);
}
return 0;
}
function start_runtime() {
let cfg = settings();
stop_runtime();
if (cfg.enabled != "1" || !cfg.bot_token || !cfg.admin_ids) {
return 0;
}
let command = command_from_args([ "ucode", "-L", LIB_DIR, LIB_DIR + "/service/telegram.uc", "worker" ]) +
" >/dev/null 2>&1 & echo $! >" + shell_quote(PID_FILE);
return command_status(command);
}
function stop_runtime() {
let pid = trim(fs.readfile(PID_FILE) || "");
if (pid != "" && match(pid, /^[0-9]+$/) != null && command_success_from_args([ "kill", "-0", pid ])) {
command_success_from_args([ "kill", pid ]);
}
fs.unlink(PID_FILE);
return 0;
}
function get_status() {
let pid = trim(fs.readfile(PID_FILE) || "");
if (pid != "" && match(pid, /^[0-9]+$/) != null && command_success_from_args([ "kill", "-0", pid ])) {
print("running (pid " + pid + ")\n");
return 0;
}
print("stopped\n");
return 1;
}
function send_api(message) {
let cfg = settings();
if (cfg.enabled != "1" || !cfg.bot_token || !cfg.admin_ids) {
return 1;
}
let admins = split(cfg.admin_ids, /,/);
for (let admin in admins) {
let chat_id = trim(admin);
if (chat_id != "") {
send_message(cfg.bot_token, chat_id, message, "Markdown");
}
}
return 0;
}
let mode = (ARGV[0] == "") ? ARGV[1] : ARGV[0];
if (!mode) mode = "";
if (mode == "start-runtime") {
exit(start_runtime());
}
else if (mode == "stop-runtime") {
exit(stop_runtime());
}
else if (mode == "worker") {
exit(worker());
}
else if (mode == "status") {
exit(get_status());
}
else if (mode == "send") {
let msg = (ARGV[1] == "") ? ARGV[2] : ARGV[1];
exit(send_api(msg));
}
else {
warn("Usage: service/telegram.uc <start-runtime|stop-runtime|worker|status|send> ...\n");
exit(1);
}
+16
View File
@@ -53,6 +53,13 @@ function log_message(message, level) {
command_success_from_args([ "logger", "-t", "tachyon", "[" + as_string(level || "info") + "] Watchdog: " + as_string(message) ]);
}
function send_telegram_notification(message) {
let tcfg = common.object_or_empty(uci_core.get_all(CONFIG_NAME, "telegram"));
if (tcfg.enabled == "1" && tcfg.bot_token && tcfg.admin_ids) {
system("/usr/bin/tachyon telegram send " + shell_quote(message) + " >/dev/null 2>&1 &");
}
}
function parse_json_or_null(str) {
try {
return json(str);
@@ -293,6 +300,10 @@ function worker() {
let pid = (length(pids) > 0 && pids[0] != "") ? pids[0] : "";
if (pid == "" && has_sections) {
log_message("sing-box is stopped. Restarting Tachyon...", "warn");
let tcfg = common.object_or_empty(uci_core.get_all(CONFIG_NAME, "telegram"));
if (tcfg.notify_crash != "0") {
send_telegram_notification("⚠️ *Watchdog:* sing-box остановлен. Перезапускаю службы Tachyon...");
}
command_status("/usr/bin/tachyon restart >/dev/null 2>&1");
}
@@ -302,6 +313,10 @@ function worker() {
let out_nft = command_output_from_args(["nft", "list", "table", "inet", nft_table]);
if (index(out_nft, "tproxy") < 0) {
log_message("nftables rules are missing or corrupted. Rebuilding...", "warn");
let tcfg = common.object_or_empty(uci_core.get_all(CONFIG_NAME, "telegram"));
if (tcfg.notify_crash != "0") {
send_telegram_notification("⚠️ *Watchdog:* правила nftables повреждены или отсутствуют. Выполняю пересборку...");
}
command_status("/usr/bin/tachyon restart >/dev/null 2>&1");
}
}
@@ -327,6 +342,7 @@ function worker() {
let logread_lower = lc(logread_out);
if (index(logread_lower, "out of memory") >= 0 || index(logread_lower, "oom-killer") >= 0) {
log_message("OOM event detected! Reducing GOMEMLIMIT scaling...", "err");
send_telegram_notification("🚨 *Watchdog:* Обнаружено событие OOM (Out Of Memory)! Уменьшаю GOMEMLIMIT и перезапускаю службы...");
let scale = 1.0;
let scale_path = "/etc/tachyon/mem_scale";
let scale_data = fs.readfile(scale_path);