mirror of
https://github.com/kc1awv/rrcd.git
synced 2026-06-08 14:11:53 -07:00
@@ -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
|
||||
|
||||
+6
-1
@@ -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 <room> <flag>`):
|
||||
- `+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 <key>` / `-k`: Room key/password (must provide key to join)
|
||||
- `+r` / `-r`: Registered room (read-only; use `/register` or `/unregister`)
|
||||
- `+o <hash>` / `-o <hash>`: Grant/remove operator status
|
||||
|
||||
@@ -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 <nick|hashprefix|hash>` — add a server-global ban (persists to
|
||||
`banned_identities`)
|
||||
- `/kline del <hash>` — remove a server-global ban (persists to
|
||||
@@ -207,6 +208,8 @@ server operators):
|
||||
- `/mode <room> (+m|-m)` — set moderated mode
|
||||
- `/mode <room> (+i|-i)` — set invite-only mode
|
||||
- `/mode <room> (+k|-k) [key]` — set/clear room key (password)
|
||||
- `/mode <room> (+p|-p)` — set/clear private mode (room hidden from `/list` and
|
||||
`/who` for non-operators)
|
||||
- `/mode <room> (+t|-t)` — set topic-ops-only (only ops can change topic)
|
||||
- `/mode <room> (+n|-n)` — set no-outside-messages
|
||||
- `/mode <room> (+r|-r)` — read-only; use /register or /unregister
|
||||
@@ -222,6 +225,7 @@ server operators):
|
||||
`NOTICE` to the target)
|
||||
- `/invite <room> del <nick|hashprefix|hash>` — remove a room-local invite
|
||||
- `/invite <room> list` — list room-local invites
|
||||
- `/list` — list all registered public rooms with their topics
|
||||
|
||||
Notes:
|
||||
|
||||
@@ -260,6 +264,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)
|
||||
|
||||
+4
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
__all__ = ["__version__"]
|
||||
|
||||
__version__ = "0.1.1"
|
||||
__version__ = "0.1.3"
|
||||
|
||||
+56
-2
@@ -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:
|
||||
@@ -2062,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)
|
||||
@@ -2476,7 +2523,7 @@ class HubService:
|
||||
outgoing,
|
||||
link,
|
||||
None,
|
||||
"usage: /mode <room> (+m|-m|+i|-i|+t|-t|+n|-n|+k|-k|+r|-r) [key] | /mode <room> (+o|-o|+v|-v) <nick|hashprefix|hash>",
|
||||
"usage: /mode <room> (+m|-m|+i|-i|+t|-t|+n|-n|+p|-p|+k|-k|+r|-r) [key] | /mode <room> (+o|-o|+v|-v) <nick|hashprefix|hash>",
|
||||
)
|
||||
return True
|
||||
try:
|
||||
@@ -2525,6 +2572,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 +2677,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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user