diff --git a/CHANGELOG.md b/CHANGELOG.md index b2dd9d9..83be106 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ This project follows the versioning policy in VERSIONING.md. +## 0.2.1 - 2026-01-08 + +- **JOINED/PARTED room notifications**: Existing room members now receive real-time notifications when users join or leave + - When a user joins a room, existing members receive a `JOINED` message with the joining user's identity hash + - When a user leaves a room, remaining members receive a `PARTED` message with the parting user's identity hash + - Joining/parting users continue to receive the full member list (when `include_joined_member_list` is enabled) + - See EX1-RRCD.md for detailed protocol documentation + ## 0.2.0 - 2026-01-07 - **Major internal refactoring**: Improved code organization and maintainability diff --git a/EX1-RRCD.md b/EX1-RRCD.md index ddcb341..40cc59a 100644 --- a/EX1-RRCD.md +++ b/EX1-RRCD.md @@ -271,6 +271,88 @@ periodically. room_invite_timeout_s = 900.0 ``` +## Extension: JOINED and PARTED Room Notifications + +The RRC specification defines `JOINED` and `PARTED` messages but doesn't specify +whether room members should be notified when users join or leave. rrcd +implements dual-mode notifications: + +### JOIN Behavior + +When a user joins a room: + +1. **Joining user receives**: A `JOINED` message containing the full list of + room members (if `include_joined_member_list` is enabled in config). This + allows the client to know who is already in the room. + + ```python + { + 0: 1, # protocol version + 1: T_JOINED, # message type + 2: , + 3: , + 4: , # src + 5: , + 6: [, , ...] # body: list of all member identity hashes + } + ``` + +2. **Existing room members receive**: A `JOINED` message containing **only** the + identity hash of the user who just joined. This allows room members to update + their member lists. + + ```python + { + 0: 1, + 1: T_JOINED, + 2: , + 3: , + 4: , + 5: , + 6: [] # body: single-element list + } + ``` + +### PART Behavior + +When a user leaves a room: + +1. **Parting user receives**: A `PARTED` message containing the list of + remaining room members (if `include_joined_member_list` is enabled). + +2. **Remaining room members receive**: A `PARTED` message containing **only** + the identity hash of the user who just left. + + ```python + { + 0: 1, + 1: T_PARTED, + 2: , + 3: , + 4: , + 5: , + 6: [] # body: single-element list + } + ``` + +### Configuration + +```toml +include_joined_member_list = true # default: true +``` + +When disabled, all `JOINED` and `PARTED` messages have `null` or empty bodies. + +### Client Implementation Notes + +- **JOINED bodies** may contain either a full member list (multiple hashes) or a + single hash. Clients should handle both cases. +- **PARTED bodies** follow the same pattern. +- The message source (`K_SRC`) is always the hub's identity hash, not the + joining/parting user. +- This extension allows clients to maintain accurate room member lists without + polling or issuing `/who` commands after every join/part. + ## Extension: Nickname Normalization The RRC spec says nicknames are "advisory" and may be "ridiculous." rrcd diff --git a/rrcd/__init__.py b/rrcd/__init__.py index ae3175a..b6caf65 100644 --- a/rrcd/__init__.py +++ b/rrcd/__init__.py @@ -1,3 +1,3 @@ __all__ = ["__version__"] -__version__ = "0.2.0" +__version__ = "0.2.1" diff --git a/rrcd/router.py b/rrcd/router.py index 27ea735..4f857d5 100644 --- a/rrcd/router.py +++ b/rrcd/router.py @@ -492,6 +492,24 @@ class MessageRouter: self.hub.room_manager.touch_room(r) + # Notify existing room members about the new joiner + # Send JOINED message with single identity hash to existing members + existing_members = [ + member_link + for member_link in self.hub.room_manager.get_room_members(r) + if member_link != link + ] + if existing_members: + member_notification = make_envelope( + T_JOINED, src=self.hub.identity.hash, room=r, body=[peer_hash] + ) + member_notification_payload = encode(member_notification) + for member_link in existing_members: + self.hub.message_helper.queue_payload( + outgoing, member_link, member_notification_payload + ) + + # Send JOINED message with full member list to the joining user joined_body = None if self.hub.config.include_joined_member_list: members: list[bytes] = [] @@ -563,6 +581,14 @@ class MessageRouter: return sess["rooms"].discard(r) + + # Get remaining members before removing the parting user + remaining_members = [ + member_link + for member_link in self.hub.room_manager.get_room_members(r) + if member_link != link + ] + if self.hub.room_manager.get_room_members(r): self.hub.room_manager.remove_member(r, link) if not self.hub.room_manager.get_room_members(r): @@ -575,6 +601,19 @@ class MessageRouter: if st is not None and not st.get("registered"): self.hub.room_manager._room_state.pop(r, None) + # Notify remaining members about the user parting + # Send PARTED message with single identity hash to remaining members + if remaining_members: + member_notification = make_envelope( + T_PARTED, src=self.hub.identity.hash, room=r, body=[peer_hash] + ) + member_notification_payload = encode(member_notification) + for member_link in remaining_members: + self.hub.message_helper.queue_payload( + outgoing, member_link, member_notification_payload + ) + + # Send PARTED message with current member list to the parting user parted_body = None if self.hub.config.include_joined_member_list: members: list[bytes] = []