diff --git a/README.md b/README.md index 4407969..f4d7a43 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ - `/add <имя>` — создать клиента (бот пришлёт команду установки) - `/del <имя>` — удалить клиента - `/list` — список клиентов + - **Кнопки** — постоянная клавиатура: ➕ Создать клиента · 📋 Список · 🗑 Удалить (инлайн-выбор) · 📊 Статус · ❓ Помощь. (Работает только для чата из настроек `tg_chat_id`.) --- diff --git a/VERSION b/VERSION index 9084fa2..26aaba0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.0 +1.2.0 diff --git a/app.py b/app.py index 4ea52f9..c9d20ef 100644 --- a/app.py +++ b/app.py @@ -129,10 +129,65 @@ def delete_client_cli(cid): return True, "\U0001F5D1 Клиент %s удалён." % cid -def telegram_bot(): - """Poll Telegram getUpdates; allow the admin chat to /add, /del, /list clients.""" +def tg_api(method, payload, token=None): import urllib.request + if token is None: + token = load_settings().get("tg_bot_token", "") + if not token: + return None + try: + data = json.dumps(payload).encode() + req = urllib.request.Request("https://api.telegram.org/bot%s/%s" % (token, method), + data=data, headers={"Content-Type": "application/json"}) + return json.loads(urllib.request.urlopen(req, timeout=15).read()) + except Exception: + return None + + +MAIN_KB = { + "keyboard": [ + ["➕ Создать клиента", "\U0001F4CB Список"], + ["\U0001F5D1 Удалить", "\U0001F4CA Статус"], + ["❓ Помощь"], + ], + "resize_keyboard": True, +} + +_bot_state = {} + + +def _bot_status_text(): + servers = [{"ip": SERVER_IP, "is_primary": True}] + load_servers() + lines = ["\U0001F4CA Серверы:"] + for sv in servers: + ip = sv.get("ip", "") + st = "ok" if ip == SERVER_IP else server_stats_cache.get(ip, {}).get("status", "?") + emoji = "\U0001F7E2" if st == "ok" else ("\U0001F534" if st == "down" else "⚪") + lines.append("%s %s%s" % (emoji, ip, " (primary)" if sv.get("is_primary") else "")) + online = set() + try: + for p, info in get_wg_peers().items(): + if is_peer_online(info.get("latest handshake", "")): + online.add(p) + for _ip, hs in server_handshakes_cache.items(): + for p, tsv in hs.items(): + if tsv and (time.time() - tsv) < 180: + online.add(p) + except Exception: + pass + lines.append("\n\U0001F4F1 Клиенты:") + cl = get_clients() + if not cl: + lines.append("нет") + for c in cl: + on = c.get("public_key", "") in online + lines.append(("\U0001F7E2 " if on else "⚪ ") + c.get("client_id", "")) + return "\n".join(lines) + + +def telegram_bot(): offset = 0 + import urllib.request while True: try: s = load_settings() @@ -144,36 +199,68 @@ def telegram_bot(): data = json.loads(urllib.request.urlopen(url, timeout=40).read()) for upd in data.get("result", []): offset = upd["update_id"] + 1 + cq = upd.get("callback_query") + if cq: + if str(cq.get("message", {}).get("chat", {}).get("id", "")) != chat: + continue + cdata = cq.get("data", "") + if cdata.startswith("del:"): + _, rep = delete_client_cli(cdata[4:]) + tg_api("answerCallbackQuery", {"callback_query_id": cq["id"], "text": "OK"}) + tg_api("sendMessage", {"chat_id": chat, "text": rep, "parse_mode": "HTML", "reply_markup": MAIN_KB}) + continue m = upd.get("message") or upd.get("channel_post") if not m: continue if str(m.get("chat", {}).get("id", "")) != chat: continue text = (m.get("text") or "").strip() - if not text.startswith("/"): + if not text: continue - parts = text.split() - cmd = parts[0].split("@")[0].lower() - arg = parts[1] if len(parts) > 1 else "" - if cmd in ("/add", "/new", "/create"): - reply = "Использование: /add <имя>" if not arg else create_client_cli(arg)[1] - elif cmd in ("/del", "/delete", "/remove", "/rm"): - reply = "Использование: /del <имя>" if not arg else delete_client_cli(arg)[1] - elif cmd in ("/list", "/ls"): - cls = [c.get("client_id", "") for c in get_clients()] - reply = ("Клиенты (%d):\n" % len(cls)) + "\n".join("\u2022 " + c for c in cls) if cls else "Клиентов нет." - elif cmd in ("/help", "/start"): - reply = ("Команды PCA Phobos:\n" - "/add <имя> — создать клиента\n" - "/del <имя> — удалить клиента\n" - "/list — список клиентов") - else: + low = text.lower() + if _bot_state.get(chat) == "add" and not text.startswith("/"): + _bot_state.pop(chat, None) + _, rep = create_client_cli(text) + tg_api("sendMessage", {"chat_id": chat, "text": rep, "parse_mode": "HTML", "reply_markup": MAIN_KB}) continue - tg_send(reply) + toks = text.split() + cmd = toks[0].split("@")[0].lower() + arg = toks[1] if len(toks) > 1 else "" + if text.startswith("➕") or cmd in ("/add", "/new", "/create"): + if arg: + _, rep = create_client_cli(arg) + tg_api("sendMessage", {"chat_id": chat, "text": rep, "parse_mode": "HTML", "reply_markup": MAIN_KB}) + else: + _bot_state[chat] = "add" + tg_api("sendMessage", {"chat_id": chat, "text": "Введите имя нового клиента (латиница, цифры, _ и -):"}) + elif text.startswith("\U0001F5D1") or cmd in ("/del", "/delete", "/remove", "/rm"): + if arg: + _, rep = delete_client_cli(arg) + tg_api("sendMessage", {"chat_id": chat, "text": rep, "parse_mode": "HTML", "reply_markup": MAIN_KB}) + else: + cl = get_clients() + if not cl: + tg_api("sendMessage", {"chat_id": chat, "text": "Клиентов нет.", "reply_markup": MAIN_KB}) + else: + kb = {"inline_keyboard": [[{"text": "\U0001F5D1 " + c.get("client_id", ""), "callback_data": "del:" + c.get("client_id", "")}] for c in cl]} + tg_api("sendMessage", {"chat_id": chat, "text": "Кого удалить?", "reply_markup": kb}) + elif text.startswith("\U0001F4CB") or cmd in ("/list", "/ls"): + cl = [c.get("client_id", "") for c in get_clients()] + rep = ("Клиенты (%d):\n" % len(cl)) + "\n".join("• " + c for c in cl) if cl else "Клиентов нет." + tg_api("sendMessage", {"chat_id": chat, "text": rep, "reply_markup": MAIN_KB}) + elif text.startswith("\U0001F4CA") or cmd in ("/status", "/stat"): + tg_api("sendMessage", {"chat_id": chat, "text": _bot_status_text(), "parse_mode": "HTML", "reply_markup": MAIN_KB}) + elif text.startswith("❓") or cmd in ("/help", "/start"): + rep = ("PCA Phobos — бот\n\n" + "Кнопки внизу или команды:\n" + "➕ /add <имя> — создать клиента\n" + "\U0001F5D1 /del <имя> — удалить\n" + "\U0001F4CB /list — список\n" + "\U0001F4CA /status — статус") + tg_api("sendMessage", {"chat_id": chat, "text": rep, "parse_mode": "HTML", "reply_markup": MAIN_KB}) except Exception: time.sleep(10) - def get_wg_peers(): """Parse `wg show wg0` to get active peers with transfer/handshake info.""" try: