#!/usr/bin/env python3 """tg_topics.py — tự tạo forum TOPIC (phòng ban) trong 1 supergroup Telegram qua Bot API. Tư tưởng: AUTO tối đa — user chỉ cần (a) tạo supergroup bật Topics, (b) thêm bot làm admin có quyền "Manage Topics". Bot tự dựng toàn bộ topic + icon (+ ghim mô tả tuỳ chọn). Ràng buộc Telegram: BOT KHÔNG tạo được group/supergroup (chỉ tài khoản người dùng tạo). Bot CHỈ tạo được topic SAU KHI là admin (can_manage_topics) trong supergroup đã bật Topics. Token đọc từ ~/.hermes/.env (TELEGRAM_BOT_TOKEN). Chỉ dùng thư viện chuẩn. Lệnh: python3 lib/tg_topics.py whoami -> getMe (kiểm token) python3 lib/tg_topics.py chatinfo -> getChat (kiểm bot trong nhóm + is_forum) python3 lib/tg_topics.py list -> liệt kê topic hiện có (qua getForumTopicIconStickers? -> không; Telegram không có API list topic → dùng để kiểm quyền) python3 lib/tg_topics.py create "Tên 1" "Tên 2" ... -> tạo lần lượt; in JSON từng topic {name, ok, message_thread_id, error} python3 lib/tg_topics.py create --json '[{"name":"Sales","icon_color":7322096}]' -> tạo theo JSON (name + icon_color tuỳ chọn) Mã màu icon hợp lệ của Telegram (icon_color): 7322096 9367192 16766590 16749490 16478047 13338331. Tên phòng ban -> tự gán màu xoay vòng nếu không chỉ định. """ import os import sys import json import urllib.request import urllib.error ICON_COLORS = [7322096, 9367192, 16766590, 16749490, 16478047, 13338331] def _hermes_env_path(): """Định vị file .env cấu hình gốc.""" if os.path.exists("/root/.hermes/.env"): return "/root/.hermes/.env" return os.path.join(os.path.expanduser("~"), ".hermes", ".env") def load_token(): """Đọc TELEGRAM_BOT_TOKEN từ ~/.hermes/.env (hoặc biến môi trường).""" tok = os.environ.get("TELEGRAM_BOT_TOKEN", "").strip() if tok: return tok path = _hermes_env_path() try: with open(path, encoding="utf-8") as f: for line in f: line = line.strip() if line.startswith("TELEGRAM_BOT_TOKEN="): return line.split("=", 1)[1].strip() except OSError: pass return "" def api(token, method, params=None, timeout=20): """Gọi Telegram Bot API. Trả (ok, result_or_description).""" url = "https://api.telegram.org/bot%s/%s" % (token, method) data = None headers = {} if params is not None: data = json.dumps(params).encode("utf-8") headers["Content-Type"] = "application/json" req = urllib.request.Request(url, data=data, headers=headers, method="POST" if data else "GET") try: with urllib.request.urlopen(req, timeout=timeout) as resp: body = json.loads(resp.read().decode("utf-8", "replace")) except urllib.error.HTTPError as e: try: body = json.loads(e.read().decode("utf-8", "replace")) except Exception: return False, "HTTP %s" % e.code except Exception as e: return False, str(e) if body.get("ok"): return True, body.get("result") return False, body.get("description", "unknown error") def cmd_whoami(token, args): ok, res = api(token, "getMe") print(json.dumps({"ok": ok, "result": res}, ensure_ascii=False)) return 0 if ok else 1 def cmd_chatinfo(token, args): if not args: print(json.dumps({"ok": False, "error": "thiếu chat_id"}, ensure_ascii=False)); return 2 ok, res = api(token, "getChat", {"chat_id": args[0]}) out = {"ok": ok} if ok: out.update({"id": res.get("id"), "title": res.get("title"), "type": res.get("type"), "is_forum": res.get("is_forum", False)}) else: out["error"] = res print(json.dumps(out, ensure_ascii=False)) return 0 if ok else 1 def cmd_create(token, args): if not args: print(json.dumps({"ok": False, "error": "thiếu chat_id"}, ensure_ascii=False)); return 2 chat_id = args[0] rest = args[1:] topics = [] if rest and rest[0] == "--json": try: for t in json.loads(rest[1]): topics.append({"name": t["name"], "icon_color": t.get("icon_color"), "icon_custom_emoji_id": t.get("icon_custom_emoji_id")}) except Exception as e: print(json.dumps({"ok": False, "error": "JSON lỗi: %s" % e}, ensure_ascii=False)); return 2 else: topics = [{"name": n} for n in rest] if not topics: print(json.dumps({"ok": False, "error": "không có topic nào để tạo"}, ensure_ascii=False)); return 2 results = [] for i, t in enumerate(topics): params = {"chat_id": chat_id, "name": t["name"][:128]} color = t.get("icon_color") params["icon_color"] = color if color in ICON_COLORS else ICON_COLORS[i % len(ICON_COLORS)] if t.get("icon_custom_emoji_id"): params["icon_custom_emoji_id"] = t["icon_custom_emoji_id"] ok, res = api(token, "createForumTopic", params) results.append({ "name": t["name"], "ok": ok, "message_thread_id": (res or {}).get("message_thread_id") if ok else None, "error": None if ok else res, }) print(json.dumps({"ok": all(r["ok"] for r in results), "topics": results}, ensure_ascii=False, indent=2)) return 0 if all(r["ok"] for r in results) else 1 def cmd_setup(token, args): """Tạo nhiều topic + (tuỳ chọn) đăng tin mô tả & ghim trong mỗi topic — 1 lệnh. setup --json '[{"name":"Điều hành","intro":"...","pin":true,"icon_color":7322096}, ...]' """ if len(args) < 3 or args[1] != "--json": print(json.dumps({"ok": False, "error": "dùng: setup --json '[{name,intro?,pin?,icon_color?}]'"}, ensure_ascii=False)); return 2 chat_id = args[0] try: items = json.loads(args[2]) except Exception as e: print(json.dumps({"ok": False, "error": "JSON lỗi: %s" % e}, ensure_ascii=False)); return 2 results = [] for i, t in enumerate(items): params = {"chat_id": chat_id, "name": str(t["name"])[:128]} color = t.get("icon_color") params["icon_color"] = color if color in ICON_COLORS else ICON_COLORS[i % len(ICON_COLORS)] if t.get("icon_custom_emoji_id"): params["icon_custom_emoji_id"] = t["icon_custom_emoji_id"] ok, res = api(token, "createForumTopic", params) row = {"name": t["name"], "ok": ok, "message_thread_id": (res or {}).get("message_thread_id") if ok else None, "error": None if ok else res, "intro_posted": False, "pinned": False} if ok and t.get("intro"): tid = row["message_thread_id"] ok2, msg = api(token, "sendMessage", { "chat_id": chat_id, "message_thread_id": tid, "text": str(t["intro"])}) row["intro_posted"] = bool(ok2) if ok2 and t.get("pin"): ok3, _ = api(token, "pinChatMessage", { "chat_id": chat_id, "message_id": msg.get("message_id"), "disable_notification": True}) row["pinned"] = bool(ok3) results.append(row) print(json.dumps({"ok": all(r["ok"] for r in results), "topics": results}, ensure_ascii=False, indent=2)) return 0 if all(r["ok"] for r in results) else 1 def cmd_icons(token, args): """Liệt kê icon topic khả dụng, hoặc tra id của 1 emoji. icons [--find ]""" ok, res = api(token, "getForumTopicIconStickers") if not ok: print(json.dumps({"ok": False, "error": res}, ensure_ascii=False)); return 1 items = [{"emoji": s.get("emoji"), "custom_emoji_id": s.get("custom_emoji_id")} for s in res] if "--find" in args: target = args[args.index("--find") + 1] if args.index("--find") + 1 < len(args) else "" t = (target or "").replace("️", "") hit = [i for i in items if (i["emoji"] or "").replace("️", "") == t] print(json.dumps({"ok": True, "match": hit}, ensure_ascii=False)); return 0 print(json.dumps({"ok": True, "count": len(items), "icons": items}, ensure_ascii=False)); return 0 def cmd_edit(token, args): """Đổi tên/icon 1 topic. edit [--name X] [--icon ]""" if len(args) < 2: print(json.dumps({"ok": False, "error": "dùng: edit [--name X] [--icon ]"}, ensure_ascii=False)); return 2 p = {"chat_id": args[0], "message_thread_id": int(args[1])} if "--name" in args: p["name"] = args[args.index("--name") + 1][:128] if "--icon" in args: p["icon_custom_emoji_id"] = args[args.index("--icon") + 1] ok, res = api(token, "editForumTopic", p) if not ok and "NOT_MODIFIED" in str(res): ok, res = True, "đã giống sẵn" print(json.dumps({"ok": ok, "error": None if ok else res}, ensure_ascii=False)); return 0 if ok else 1 def main(): args = sys.argv[1:] cmd = args[0] if args else "whoami" token = load_token() if not token: print(json.dumps({"ok": False, "error": "thiếu TELEGRAM_BOT_TOKEN (~/.hermes/.env)"}, ensure_ascii=False)) sys.exit(2) handlers = {"whoami": cmd_whoami, "chatinfo": cmd_chatinfo, "create": cmd_create, "setup": cmd_setup, "icons": cmd_icons, "edit": cmd_edit} h = handlers.get(cmd) if not h: print(json.dumps({"ok": False, "error": "lệnh không rõ: %s" % cmd}, ensure_ascii=False)); sys.exit(1) sys.exit(h(token, args[1:])) if __name__ == "__main__": main()