#!/usr/bin/env python3 """setup_departments.py — TỰ ĐỘNG dựng phòng ban (forum topics) trong nhóm Telegram, 1 LỆNH. Vì sao 1 script: model yếu không tự điều phối nhiều bước được (đọc source, quét DB, hỏi lại lung tung). Script này gói hết: lấy chat_id từ ENV (HERMES_SESSION_CHAT_ID), lấy cơ cấu phòng ban từ hồ sơ user, verify nhóm là forum + bot là admin, tạo topic + ghim mô tả. Agent chỉ việc CHẠY rồi đọc JSON. 🚫 KHÔNG quét ~/.hermes / state.db / channel_directory để mò chat_id — chat_id CHỈ đến từ env. Dùng: python3 lib/setup_departments.py -> tự lấy cơ cấu từ hồ sơ python3 lib/setup_departments.py --departments "A, B, C" """ import os import re import sys import json sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import tg_topics # load_token, api, ICON_COLORS import identity # resolve_user, load_index # Bỏ emoji/biểu tượng khỏi TÊN phòng (icon là riêng qua custom_emoji_id → tránh lặp emoji-trong-tên + icon) _EMOJI = re.compile( "[\U0001F000-\U0001FAFF\U00002600-\U000027BF\U0001F1E6-\U0001F1FF" "←-⇿⌀-⏿⬀-⯿️‍⃣]+", flags=re.UNICODE, ) def clean_name(n): return _EMOJI.sub("", n or "").strip() def _arg(name): a = sys.argv[1:] if name in a: i = a.index(name) if i + 1 < len(a): return a[i + 1] return None def _chat_id_from_session_key(key): """Trích chat_id nhóm từ session_key dạng agent:main:telegram:group:-100...:.""" if key and ":group:" in key: try: cid = key.split(":group:", 1)[1].split(":")[0].strip() if cid: return cid except Exception: pass return "" def resolve_chat_id(): """Lấy chat_id nhóm BỀN BỈ. Env CHAT_ID có thể STALE (trả id DM dù đang ở nhóm) → KHÔNG tin env mù quáng: ưu tiên nguồn chỉ-RÕ-là-nhóm (id âm / session_key group / sessions.json). Deterministic, do SCRIPT đọc — không phải LLM mò.""" env_cid = (os.environ.get("HERMES_SESSION_CHAT_ID") or "").strip() # 1) env là NHÓM (id âm -100...) → tin ngay if env_cid.startswith("-"): return env_cid, "env" # 2) session_key của phiên hiện tại chỉ ra nhóm (ưu tiên hơn env dương/stale) key_cid = _chat_id_from_session_key((os.environ.get("HERMES_SESSION_KEY") or "").strip()) if key_cid: return key_cid, "session_key" # 3) phiên NHÓM cập nhật gần nhất trong sessions.json try: sp = os.path.join(os.path.expanduser("~"), ".hermes", "sessions", "sessions.json") with open(sp, encoding="utf-8") as f: data = json.load(f) groups = [(v.get("updated_at", ""), k) for k, v in data.items() if ":group:" in str(k)] if groups: groups.sort() cid = _chat_id_from_session_key(groups[-1][1]) if cid: return cid, "sessions.json" except Exception: pass # 4) cuối cùng: env dương = DM thật (không có nguồn nhóm nào) if env_cid: return env_cid, "env_dm" return "", "none" def main(): chat_id, chat_src = resolve_chat_id() user_id = identity.resolve_user() # 1) Phải là NHÓM (chat_id âm, dạng -100...). DM = id dương; rỗng = cold-start nhóm mới. if not chat_id: # Thường gặp ở tin ĐẦU TIÊN vào nhóm vừa tạo (forum đang khởi tạo) → retry là được. print(json.dumps({ "ok": False, "stage": "context", "error": "Chưa lấy được nhóm (có thể là tin đầu tiên vào nhóm mới). Anh/chị @nhắc em LẠI MỘT LẦN NỮA trong nhóm là được.", "chat_id": None, }, ensure_ascii=False)) return 2 if not chat_id.startswith("-"): print(json.dumps({ "ok": False, "stage": "context", "error": "Đang ở chat riêng (DM), không phải nhóm. Hãy @nhắc em NGAY TRONG nhóm công ty.", "chat_id": chat_id, }, ensure_ascii=False)) return 2 # CHỐNG TẠO TRÙNG: đã dựng phòng ban cho nhóm này rồi + KHÔNG có --departments mới → DỪNG. if not _arg("--departments") and user_id: rec = identity.load_index().get("users", {}).get(user_id, {}) if rec.get("departments_chat_id") == chat_id and rec.get("departments_setup"): print(json.dumps({ "ok": True, "stage": "already_done", "chat_id": chat_id, "message": "Phòng ban đã dựng xong cho nhóm này rồi — em KHÔNG tạo trùng. Muốn thêm phòng mới thì cho em biết tên phòng cụ thể nhé.", }, ensure_ascii=False)) return 0 token = tg_topics.load_token() if not token: print(json.dumps({"ok": False, "stage": "token", "error": "thiếu TELEGRAM_BOT_TOKEN"}, ensure_ascii=False)) return 1 # 2) Verify nhóm là forum + bot ở trong nhóm/đủ quyền (getChat) ok, info = tg_topics.api(token, "getChat", {"chat_id": chat_id}) if not ok: print(json.dumps({ "ok": False, "stage": "getChat", "error": info, "hint": "Bot chưa ở trong nhóm hoặc chưa là admin. Thêm bot vào nhóm + cấp Admin (Manage Topics).", }, ensure_ascii=False)) return 1 if not info.get("is_forum"): print(json.dumps({ "ok": False, "stage": "forum", "error": "Nhóm chưa bật Topics (Chủ đề).", "hint": "Vào Manage Group → bật Topics, rồi thử lại.", }, ensure_ascii=False)) return 1 # 2b) Kiểm QUYỀN: admin KHÔNG tự có quyền "Manage Topics" — phải bật toggle riêng. mok, me = tg_topics.api(token, "getMe") bid = me.get("id") if mok else None if bid: okm, mem = tg_topics.api(token, "getChatMember", {"chat_id": chat_id, "user_id": bid}) if okm: status = mem.get("status") if status != "administrator": print(json.dumps({ "ok": False, "stage": "permission", "error": "Em chưa phải admin trong nhóm.", "hint": "Thêm em làm Admin và BẬT quyền 'Quản lý chủ đề (Manage Topics)'.", }, ensure_ascii=False)) return 1 if not mem.get("can_manage_topics"): print(json.dumps({ "ok": False, "stage": "permission", "error": "Em là admin rồi nhưng CHƯA được bật quyền 'Quản lý chủ đề (Manage Topics)'.", "hint": "Vào Manage Group → Administrators → chọn em → BẬT toggle 'Quản lý chủ đề (Manage Topics)' → Lưu, rồi nhắn lại em.", }, ensure_ascii=False)) return 1 # 3) Cơ cấu phòng ban: ưu tiên --departments, sau đó hồ sơ (registry) depts = _arg("--departments") if not depts and user_id: rec = identity.load_index().get("users", {}).get(user_id, {}) depts = rec.get("departments", "") # clean_name: bỏ emoji khỏi TÊN (icon là riêng → không nhét emoji vào tên gây lặp) names = [clean_name(d) for d in (depts or "").replace("\n", ",").split(",") if clean_name(d)] if not names: print(json.dumps({ "ok": False, "stage": "departments", "error": "Chưa có cơ cấu phòng ban trong hồ sơ.", "hint": "Hỏi user muốn những phòng ban nào rồi chạy lại: --departments \"Phòng A, Phòng B, Phòng C\"", }, ensure_ascii=False)) return 1 # 4) Tạo topic + đăng mô tả + ghim (1 vòng, không hỏi lại) results = [] for i, name in enumerate(names): params = {"chat_id": chat_id, "name": name[:128], "icon_color": tg_topics.ICON_COLORS[i % len(tg_topics.ICON_COLORS)]} ok2, res = tg_topics.api(token, "createForumTopic", params) row = {"name": name, "ok": ok2, "message_thread_id": (res or {}).get("message_thread_id") if ok2 else None, "error": None if ok2 else res} if ok2: tid = row["message_thread_id"] intro = "Phòng \"%s\". Trong phòng này, @nhắc trợ lý để được hỗ trợ đúng việc của phòng." % name ok3, msg = tg_topics.api(token, "sendMessage", {"chat_id": chat_id, "message_thread_id": tid, "text": intro}) if ok3: tg_topics.api(token, "pinChatMessage", {"chat_id": chat_id, "message_id": msg.get("message_id"), "disable_notification": True}) results.append(row) # 5) Lưu lại map phòng→thread vào registry để lần sau không dựng trùng if user_id: try: mp = {r["name"]: r["message_thread_id"] for r in results if r["ok"]} identity.upsert_index(user_id, departments_setup=json.dumps(mp, ensure_ascii=False), departments_chat_id=chat_id) except Exception: pass print(json.dumps({"ok": all(r["ok"] for r in results), "chat_id": chat_id, "chat_id_source": chat_src, "topics": results}, ensure_ascii=False, indent=2)) return 0 if all(r["ok"] for r in results) else 1 if __name__ == "__main__": sys.exit(main())