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>
115 lines
3.7 KiB
Python
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)
|