"""Directory bot registry: submitted groups/channels with super-user approval. Mirrors the core of SimpleX's directory service: groups are registered (pending), a super-user approves/rejects, and approved entries are published to the bot's website (web//data/listing.json) in the format index.html expects. State is a per-bot JSON file so it survives restarts: data/bots/_directory.json """ import json from datetime import datetime, timezone from pathlib import Path BASE = Path(__file__).parent DATA_DIR = BASE / "data" / "bots" WEB_DIR = BASE.parent / "web" def _now() -> str: return datetime.now(timezone.utc).isoformat() def _state_path(safe: str) -> Path: return DATA_DIR / f"{safe}_directory.json" def load_state(safe: str) -> dict: p = _state_path(safe) if p.exists(): try: return json.loads(p.read_text(encoding="utf-8")) except Exception: pass return {"seq": 0, "entries": []} def save_state(safe: str, state: dict) -> None: DATA_DIR.mkdir(parents=True, exist_ok=True) _state_path(safe).write_text(json.dumps(state, indent=2), encoding="utf-8") def find_by_group(state: dict, group_id: int) -> dict | None: for e in state["entries"]: if e["group_id"] == group_id: return e return None def add_pending( safe: str, group_id: int, display_name: str, link: str, is_channel: bool, summary: dict, short_descr: str | None, submitted_by: str, ) -> tuple[dict, bool]: """Register a group as pending. Returns (entry, is_new).""" state = load_state(safe) existing = find_by_group(state, group_id) if existing: return existing, False state["seq"] += 1 entry = { "id": state["seq"], "group_id": group_id, "status": "pending", "displayName": display_name, "link": link, "is_channel": bool(is_channel), "summary": summary or {}, "shortDescr": short_descr, "submitted_by": submitted_by, "createdAt": _now(), "activeAt": _now(), } state["entries"].append(entry) save_state(safe, state) return entry, True def set_status(safe: str, entry_id: int, status: str) -> dict | None: state = load_state(safe) for e in state["entries"]: if e["id"] == entry_id: e["status"] = status save_state(safe, state) return e return None def entries_by_status(safe: str, status: str) -> list[dict]: return [e for e in load_state(safe)["entries"] if e["status"] == status] def search(safe: str, query: str) -> list[dict]: q = query.lower().strip() out = [] for e in entries_by_status(safe, "approved"): if q in e["displayName"].lower() or (e.get("shortDescr") or "").lower().find(q) >= 0: out.append(e) return out def publish(safe: str) -> int: """Write approved entries to web//data/listing.json (website schema). Returns count.""" entries = [] for e in entries_by_status(safe, "approved"): entry_type = { "type": "channel" if e["is_channel"] else "group", "summary": e.get("summary") or {}, } if e["is_channel"]: entry_type["groupType"] = "channel" entries.append({ "entryType": entry_type, "displayName": e["displayName"], "groupLink": {"connShortLink": e["link"]} if e.get("link") else {}, "shortDescr": e.get("shortDescr"), "createdAt": e["createdAt"], "activeAt": e["activeAt"], }) out = WEB_DIR / safe / "data" / "listing.json" out.parent.mkdir(parents=True, exist_ok=True) out.write_text(json.dumps({"entries": entries}, indent=2), encoding="utf-8") return len(entries)