bot_status v0.2.1 [base]
Закреплённое статус-сообщение бота (/status, кнопка обновления).
| version | date | commit | файлов |
|---|---|---|---|
| 0.2.1 | 2026-09-17 | 91610a9fc155 | 7 |
README
# bot_status
Закреплённое статус-сообщение бота. Живой сводный экран: версии, аптайм, ключевые состояния.
Тип: Модуль. Категория: `base`. Зависимостей нет.
## Описание
Команда создаёт закреплённое сообщение-визитку при первом вызове и обновляет его при повторных. Кнопка обновления пересобирает текст из живых данных — ничего вручную править не нужно.
Текст собирается из самодиагностики: состояние каналов, кэша и здоровья подсистем. Привязка к сообщению (чат и номер сообщения) хранится в собственном кэше модуля.
## Возможности
- `bot_status.text {}` → текст статус-сообщения (сборка из живых данных)
## Права
- `bot_status` — Просмотр и обновление статус-сообщения (/status)
## Telegram
- `/status` — показать статус; кнопка «🔄 Обновить сейчас» — пересобрать
## Конфигурация (`config.schema.json`)
- Обязательных нет.
- Опциональные:
- `UPDATE_INTERVAL` = `300` — интервал обновления, секунд
Манифест
{
"name": "bot_status",
"version": "0.2.1",
"category": "base",
"description": "Закреплённое статус-сообщение бота (/status, кнопка обновления).",
"requires": [],
"rights": [],
"api_methods": [],
"web_routes": [],
"commit": "91610a9fc1556c139f02139ef0fd9a4504251dcf",
"updated": "2026-09-17 12:26:56 +0300",
"integrity": "sha256:d35463d7064e0b8a0c3fc8995e1be035411eda23e1055cbcfaa0383ad682694c",
"files": "[7 файлов — см. вкладки ниже]",
"note": "",
"wdesc": "",
"wbody": "",
"_extra": {
"api": "",
"events_emitted": "[]",
"events_subscribed": "[]",
"tg_commands": "",
"config_schema": "config.schema.json",
"env_example": ".env.example"
}
} Файлы и исходники
Дерево файлов
- · корень
- 1.5 КБ
- 0.1 КБ
- 0.8 КБ
- 7.1 КБ
- 0.9 КБ
- 2.2 КБ
- web/
- 0.6 КБ
Предпросмотр
Выберите файл в дереве выше — код откроется здесь.
Все исходники (.py) одним списком
module.py 7.1 КБ
"""bot_status.Module — закреплённое сообщение со статусом.
Состояние (chat_id/message_id) — в своём кеше. /status создаёт сообщение
(первый раз) или обновляет, кнопка refresh — то же через callback.
Периодическое обновление — через job_queue активного транспорта
(api transport_telegram.job); при переключении транспорта задача гаснет
вместе со старой очередью — перезапуск в И4 через on_reactivate.
"""
import logging
import time
from datetime import datetime
from core.base_module import BaseModule
from core.errors import ConfigError
logger = logging.getLogger(__name__)
def _format_uptime(seconds):
seconds = int(seconds or 0)
d, rem = divmod(seconds, 86400)
h, rem = divmod(rem, 3600)
m = rem // 60
if d:
return "%dд %02d:%02d" % (d, h, m)
if h:
return "%02d:%02d" % (h, m)
return "%d мин" % m
def _host_uptime():
try:
with open("/proc/uptime", "r") as f:
return float(f.read().split()[0])
except Exception: # noqa: BLE001
return None
class Module(BaseModule):
def setup(self, ctx) -> None:
self.config = ctx.module_config("bot_status")
self.cache = ctx.cache.ns("bot_status")
try:
self.interval = int(self.config.get("UPDATE_INTERVAL", "300") or 300)
except ValueError:
raise ConfigError("bot_status: UPDATE_INTERVAL не число")
self.started_at = time.time()
self._job = None
ctx.api.register("bot_status", "text", self.build_text)
ctx.api.register("bot_status", "refresh", self.refresh)
def _emoji(self):
try:
active = self.ctx.api.call("transport_telegram.active")
except Exception: # noqa: BLE001
active = None
order = (self.ctx.bootstrap.get("TELEGRAM_TRANSPORTS") or "http").split(",")
primary = order[0].strip() if order else "http"
return "🟢" if not active or active == primary else "🟡"
def build_text(self):
uptime = _format_uptime(time.time() - self.started_at)
host = _host_uptime()
host_str = _format_uptime(host) if host is not None else "—"
mods = ", ".join(
"%s:%s" % (m.name, "ok" if m.health().get("ok") else "FAIL")
for m in self.ctx.loaded_modules
)
return (
"%s Статус бота\n"
"──────────────\n"
"Обновлено: %s\n"
"Аптайм хоста: %s\n"
"Аптайм бота: %s\n"
"Модули: %s\n" % (
self._emoji(), datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
host_str, uptime, mods or "—",
)
)
def _markup(self):
try:
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
except ImportError:
return None
return InlineKeyboardMarkup(
[[InlineKeyboardButton("🔄 Обновить сейчас", callback_data="bot_status:refresh")]]
)
def _state(self):
return self.cache.get("pinned", {})
def refresh(self, chat_id=None):
"""Создать/обновить закреплённое сообщение. Возвращает текст для ответа."""
st = self._state()
target = chat_id or st.get("chat_id")
if target is None:
return "Нет чата для статуса: вызовите /status в личке бота."
if not st.get("message_id"):
try:
msg = self.ctx.api.call(
"transport_telegram.send", target, self.build_text(),
reply_markup=self._markup(),
)
self.cache.set("pinned", {"chat_id": target,
"message_id": msg.message_id,
"created_at": time.time()})
try:
self.ctx.api.call("transport_telegram.pin", target, msg.message_id)
except Exception as e: # noqa: BLE001
logger.warning("pin: %s", type(e).__name__)
return "Статус-сообщение создано и закреплено."
except Exception as e: # noqa: BLE001
logger.error("send status: %s", type(e).__name__)
return "⚠️ Не удалось отправить статус."
try:
self.ctx.api.call(
"transport_telegram.edit",
st["chat_id"], st["message_id"], self.build_text(),
reply_markup=self._markup(),
)
return "Статус-сообщение обновлено."
except Exception as e: # noqa: BLE001
text = str(e)
if "not found" in text or "message to edit not found" in text:
self.cache.set("pinned", {})
return self.refresh(target)
logger.error("edit status: %s", type(e).__name__)
return "⚠️ Сбой связи, сообщение не обновлено."
def _schedule(self, _active_name=None):
try:
queue = self.ctx.api.call("transport_telegram.job")
except Exception as e: # noqa: BLE001
logger.warning("job_queue недоступен, периодика отключена: %s", e)
return
if queue is None:
logger.warning("job_queue недоступен, периодика отключена.")
return
if self._job is not None:
try:
self._job.schedule_removal()
except Exception: # noqa: BLE001
pass
self._job = None
try:
self._job = queue.run_repeating(
lambda _: self.refresh(), interval=self.interval,
first=self.interval, name="bot_status_refresh",
)
logger.info("bot_status: периодика каждые %d с", self.interval)
except Exception as e: # noqa: BLE001
logger.warning("планирование refresh: %s", e)
def start(self) -> None:
# Очередь принадлежит активному транспорту: планируем по on_ready
# (первая активация и каждый возврат/переключение транспорта).
try:
self.ctx.api.call("transport_telegram.on_ready", self._schedule)
except Exception as e: # noqa: BLE001
logger.warning("on_ready недоступен: %s", e)
def stop(self) -> None:
if self._job is not None:
try:
self._job.schedule_removal()
except Exception: # noqa: BLE001
pass
self._job = None
def health(self) -> dict:
st = self._state()
return {"ok": True, "module": self.name,
"pinned": bool(st.get("message_id"))}
ui_tg.py 0.9 КБ
"""bot_status.ui_tg — /status (право bot_status) + кнопка обновления."""
TG_COMMANDS = {
"status": {"desc": "статус бота", "right": "bot_status", "menu": True},
}
TG_CALLBACKS = {
"refresh": {"desc": "обновить статус", "right": "bot_status"},
}
def handle_command(ctx, config, command: str, text: str, inv=None):
if command == "status":
return ctx.api.call("bot_status.refresh", (inv or {}).get("chat_id"))
return None
def handle_callback(ctx, config, action: str, args: dict):
if action == "refresh":
# refresh() уже перерисовал закреплённое сообщение на месте —
# в чат идёт только всплывашка-квитанция, не правка текста.
res = ctx.api.call("bot_status.refresh", args.get("chat_id"))
return ("toast", res)
return None
ui_web.py 2.2 КБ
"""bot_status.ui_web — витрина сервиса + статус JSON (для SPA/отладки)."""
ROUTES = [
("GET", "/api/bot_status/status", "status", {"right": "bot_status",
"desc": "Самодиагностика JSON (тот же текст, что в закрепе)"}),
("GET", "/api/bot_status/panel", "panel", {"right": None,
"desc": "Публичные факты процесса для панели мониторинга"}),
]
SERVICE = {"key": "bot_status", "icon": "💚", "title": "Статус бота", "right": "bot_status",
"phase": 2, "desc": "Самодиагностика: каналы, кэш, здоровье"}
PANELS = []
MONITOR_PANELS = [
{"id": "bot", "title": "Бот", "api": "/api/bot_status/panel",
"visibility": "public", "refresh_s": 15},
]
def handle_api(ctx, config, method, req):
from core.errors import UserError
if method == "panel":
try:
uptime_s = int(ctx.api.call("transport_web.uptime_s"))
except Exception: # noqa: BLE001
uptime_s = 0
try:
tg_active = ctx.api.call("transport_telegram.active")
except Exception: # noqa: BLE001
tg_active = None
rss_kb = None
try:
with open("/proc/self/status", encoding="ascii") as f:
for line in f:
if line.startswith("VmRSS:"):
rss_kb = int(line.split()[1])
break
except Exception: # noqa: BLE001
pass
try:
channels = sorted({m.name for m in ctx.loaded_modules})
except Exception: # noqa: BLE001
channels = []
import os as _os
return {"ok": True, "uptime_s": uptime_s, "pid": _os.getpid(),
"rss_kb": rss_kb, "channels": channels,
"telegram_transport": tg_active}
if method == "status":
try:
text = ctx.api.call("bot_status.text")
except Exception as e: # noqa: BLE001
return 500, {"error": str(e)[:400]}
return {"ok": True, "text": text}
raise UserError("неизвестный метод: %s" % method)