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>
This commit is contained in:
114
manager/directory.py
Normal file
114
manager/directory.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user