Files
simplex-manager/manager/directory.py
Jon c1bb9cb955 Add chat actions, channels/groups mgmt, directory + deadmans bots, network status
Profiles/bots (profiles.py, main.py):
- Surface real send errors; fix group send and member-count staleness (refresh on view)
- Group/channel actions: join, leave, owner delete; consistent member counts
- Contacts: always-visible Chat, plus Clear chat and Delete contact
- Support bot: OpenAI-compatible LLM backend (Grok/Ollama/OpenAI) per-bot config
- Deadmans bot: check-in window, trigger message, recipients, owner
- Directory bot: add-to-group registration, super-user /approve /reject /list, search,
  publishes listing.json in the website schema (directory.py registry module)
- Profile edit (name/bio/avatar) + avatars on list pages
- Global status + /network page (operators, SMP/XFTP servers) and Settings network info

UI (templates):
- Chat rooms; collapsible left sidebar with notifications + network widget
- Per-directory-bot website generated on creation (name substituted)
- Matrix theme; copy/hyperlink addresses; site footer

Ignore runtime bot state and generated directory sites.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 21:26:16 +01:00

115 lines
3.7 KiB
Python

"""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/<safe>/data/listing.json) in the format index.html expects.
State is a per-bot JSON file so it survives restarts: data/bots/<safe>_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/<safe>/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)