From 85d281a1fe4fd2b694a80455ccfd2ac105630cc6 Mon Sep 17 00:00:00 2001 From: kc1awv Date: Mon, 5 Jan 2026 07:39:53 -0500 Subject: [PATCH 1/2] add list command to list registered public rooms, add +p chanmode for private rooms to skip listing --- README.md | 3 +++ rrcd/service.py | 51 +++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c1432e8..76b6e82 100644 --- a/README.md +++ b/README.md @@ -207,6 +207,7 @@ server operators): - `/mode (+m|-m)` — set moderated mode - `/mode (+i|-i)` — set invite-only mode - `/mode (+k|-k) [key]` — set/clear room key (password) +- `/mode (+p|-p)` — set/clear private mode (room hidden from `/list`) - `/mode (+t|-t)` — set topic-ops-only (only ops can change topic) - `/mode (+n|-n)` — set no-outside-messages - `/mode (+r|-r)` — read-only; use /register or /unregister @@ -222,6 +223,7 @@ server operators): `NOTICE` to the target) - `/invite del ` — remove a room-local invite - `/invite list` — list room-local invites +- `/list` — list all registered public rooms with their topics Notes: @@ -260,6 +262,7 @@ Supported keys per room: - `invite_only`: whether the room is in +i (bool) - `topic_ops_only`: whether the room is in +t (bool) - `no_outside_msgs`: whether the room is in +n (bool) +- `private`: whether the room is in +p (bool; room hidden from `/list`) - `key`: room key/password for +k (string, optional) - `operators`: list of identity hashes (strings) - `voiced`: list of identity hashes (strings) diff --git a/rrcd/service.py b/rrcd/service.py index b8d4a9c..0514234 100644 --- a/rrcd/service.py +++ b/rrcd/service.py @@ -1132,6 +1132,7 @@ class HubService: invite_only = bool(st.get("invite_only", False)) topic_ops_only = bool(st.get("topic_ops_only", False)) no_outside_msgs = bool(st.get("no_outside_msgs", False)) + private = bool(st.get("private", False)) key = st.get("key") has_key = isinstance(key, str) and bool(key) return { @@ -1140,6 +1141,7 @@ class HubService: "invite_only": invite_only, "topic_ops_only": topic_ops_only, "no_outside_msgs": no_outside_msgs, + "private": private, "has_key": has_key, } @@ -1155,6 +1157,8 @@ class HubService: flags.append("m") if m.get("no_outside_msgs"): flags.append("n") + if m.get("private"): + flags.append("p") if m.get("registered"): flags.append("r") if m.get("topic_ops_only"): @@ -1407,6 +1411,7 @@ class HubService: "invite_only": bool(base.get("invite_only", False)), "topic_ops_only": bool(base.get("topic_ops_only", False)), "no_outside_msgs": bool(base.get("no_outside_msgs", False)), + "private": bool(base.get("private", False)), "key": base.get("key"), "ops": set(base.get("ops", set())), "voiced": set(base.get("voiced", set())), @@ -1425,6 +1430,7 @@ class HubService: "invite_only": False, "topic_ops_only": False, "no_outside_msgs": False, + "private": False, "key": None, "ops": set([founder]) if founder is not None else set(), "voiced": set(), @@ -2049,6 +2055,40 @@ class HubService: self._emit_notice(outgoing, link, None, self._format_stats()) return True + if cmd == "list": + # List all registered, non-private rooms with their topics + with self._state_lock: + registered_rooms = [] + for room_name, st in self._room_state.items(): + if st.get("registered") and not st.get("private"): + topic = st.get("topic") + registered_rooms.append((room_name, topic)) + + # Also check room registry for rooms not currently in room_state + for room_name, reg in self._room_registry.items(): + if room_name not in self._room_state: + if not reg.get("private"): + topic = reg.get("topic") + registered_rooms.append((room_name, topic)) + + if not registered_rooms: + self._emit_notice(outgoing, link, None, "No public rooms registered") + return True + + # Sort rooms alphabetically + registered_rooms.sort(key=lambda x: x[0]) + + # Format room list with topics + lines = ["Registered public rooms:"] + for room_name, topic in registered_rooms: + if topic: + lines.append(f" {room_name} - {topic}") + else: + lines.append(f" {room_name}") + + self._emit_notice(outgoing, link, None, "\n".join(lines)) + return True + if cmd in ("who", "names"): target_room = room if len(parts) >= 2: @@ -2476,7 +2516,7 @@ class HubService: outgoing, link, None, - "usage: /mode (+m|-m|+i|-i|+t|-t|+n|-n|+k|-k|+r|-r) [key] | /mode (+o|-o|+v|-v) ", + "usage: /mode (+m|-m|+i|-i|+t|-t|+n|-n|+p|-p|+k|-k|+r|-r) [key] | /mode (+o|-o|+v|-v) ", ) return True try: @@ -2525,6 +2565,13 @@ class HubService: self._broadcast_room_mode(r, outgoing) return True + if flag in ("+p", "-p"): + st["private"] = flag == "+p" + self._touch_room(r) + self._persist_room_state_to_registry(link, r) + self._broadcast_room_mode(r, outgoing) + return True + if flag in ("+k", "-k"): if flag == "+k": if len(parts) < 4: @@ -2623,7 +2670,7 @@ class HubService: outgoing, link, room, - "supported modes: +m -m +i -i +k -k +t -t +n -n +r -r +o -o +v -v", + "supported modes: +m -m +i -i +k -k +t -t +n -n +p -p +r -r +o -o +v -v", ) return True From 0e553f90e43ab7532a4a3c356938e0e0b506ba14 Mon Sep 17 00:00:00 2001 From: kc1awv Date: Mon, 5 Jan 2026 07:58:04 -0500 Subject: [PATCH 2/2] update changelog for version 0.1.3, add /list command, +p mode, and documentation updates; consolidate version handling --- CHANGELOG.md | 10 ++++++++++ EX1-RRCD.md | 7 ++++++- README.md | 6 ++++-- pyproject.toml | 5 ++++- rrcd/__init__.py | 2 +- rrcd/service.py | 7 +++++++ 6 files changed, 32 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92a62bd..75f9035 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ This project follows the versioning policy in VERSIONING.md. +## 0.1.3 - 2026-01-05 + +- Added `/list` command to discover registered public rooms with their topics (available to all users) +- Added `+p` (private) channel mode to hide rooms from `/list` and `/who` commands +- Private rooms are only visible in `/who` to server operators +- Updated mode handling to support `+p`/`-p` flags and persist private status to room registry +- Consolidated version number to single source in `rrcd/__init__.py` (pyproject.toml now reads it dynamically) +- Documentation updates for new command and mode in README.md and EX1-RRCD.md + + ## 0.1.2 - 2026-01-01 - Implemented RNS.Resource transfer for messages exceeding MTU limits, with resource envelope handling and automatic fallback diff --git a/EX1-RRCD.md b/EX1-RRCD.md index b8237ec..ddcb341 100644 --- a/EX1-RRCD.md +++ b/EX1-RRCD.md @@ -180,8 +180,11 @@ These work from any room (or no room): - `/reload`: Reload hub configuration (server operator only) - `/stats`: Display hub statistics (server operator only) -- `/who [room]`: List members in a room +- `/who [room]`: List members in a room. Private rooms (`+p`) are hidden from + non-operators. - `/names [room]`: Alias for `/who` +- `/list`: List all registered public rooms with their topics. Excludes private + rooms (`+p`) and ephemeral (non-registered) rooms. ### Room Management Commands @@ -218,6 +221,8 @@ IRC-style mode flags (set via `/mode `): - `+i` / `-i`: Invite-only (must be invited to join) - `+t` / `-t`: Topic protected (only operators can set topic) - `+n` / `-n`: No outside messages (must be in room to send messages) +- `+p` / `-p`: Private room (hidden from `/list` command and `/who` for + non-operators) - `+k ` / `-k`: Room key/password (must provide key to join) - `+r` / `-r`: Registered room (read-only; use `/register` or `/unregister`) - `+o ` / `-o `: Grant/remove operator status diff --git a/README.md b/README.md index 76b6e82..eae9a4d 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,8 @@ Server operator commands (require identity in `trusted_identities`): - `/stats` — show hub stats (uptime, clients, rooms, counters) - `/reload` — reload `rrcd.toml` and `rooms.toml` from disk -- `/who [room]` — list members (nick and/or hash prefix) +- `/who [room]` — list members (nick and/or hash prefix); private rooms (`+p`) + are hidden from non-operators - `/kline add ` — add a server-global ban (persists to `banned_identities`) - `/kline del ` — remove a server-global ban (persists to @@ -207,7 +208,8 @@ server operators): - `/mode (+m|-m)` — set moderated mode - `/mode (+i|-i)` — set invite-only mode - `/mode (+k|-k) [key]` — set/clear room key (password) -- `/mode (+p|-p)` — set/clear private mode (room hidden from `/list`) +- `/mode (+p|-p)` — set/clear private mode (room hidden from `/list` and + `/who` for non-operators) - `/mode (+t|-t)` — set topic-ops-only (only ops can change topic) - `/mode (+n|-n)` — set no-outside-messages - `/mode (+r|-r)` — read-only; use /register or /unregister diff --git a/pyproject.toml b/pyproject.toml index 67e2c0a..809de57 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "rrcd" -version = "0.1.2" +dynamic = ["version"] description = "Reticulum Relay Chat daemon (hub service)" readme = "README.md" license = { file = "LICENSE" } @@ -44,6 +44,9 @@ rrcd = "rrcd.cli:main" [tool.setuptools] packages = ["rrcd"] +[tool.setuptools.dynamic] +version = {attr = "rrcd.__version__"} + [tool.ruff] target-version = "py311" line-length = 88 diff --git a/rrcd/__init__.py b/rrcd/__init__.py index fd9a4ec..d4516cb 100644 --- a/rrcd/__init__.py +++ b/rrcd/__init__.py @@ -1,3 +1,3 @@ __all__ = ["__version__"] -__version__ = "0.1.1" +__version__ = "0.1.3" diff --git a/rrcd/service.py b/rrcd/service.py index 0514234..6e970c8 100644 --- a/rrcd/service.py +++ b/rrcd/service.py @@ -2102,6 +2102,13 @@ class HubService: self._emit_notice(outgoing, link, None, f"bad room: {e}") return True + # Check if room is private - only server operators can see private rooms + st = self._room_state_get(r) + if st and st.get("private"): + if not self._is_server_op(peer_hash): + self._emit_notice(outgoing, link, None, f"room {r} is private") + return True + members = [] for other in sorted(self.rooms.get(r, set()), key=lambda x: id(x)): s = self.sessions.get(other)