From 019e10ac5c8976edc8ca15b2c02dadd42b8bedb9 Mon Sep 17 00:00:00 2001 From: Keith Smith Date: Thu, 3 Sep 2026 20:35:09 -0600 Subject: [PATCH] Post a welcome message when an admin adds someone to a room (#72) Attributed to the admin doing the adding rather than a new system/bot sender concept, since every message today requires a real user_id and the admin is already a real, in-scope user for the request. Broadcasts room_added before the welcome message itself, so the new member's client learns the room exists before it sees an unread update for it. Co-Authored-By: Claude Sonnet 5 --- backend/app/routers/rooms.py | 31 ++++++++++++++++++++++++++++++- backend/tests/test_rooms.py | 21 +++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/backend/app/routers/rooms.py b/backend/app/routers/rooms.py index a36d1e5..50aa069 100644 --- a/backend/app/routers/rooms.py +++ b/backend/app/routers/rooms.py @@ -39,8 +39,9 @@ from app.schemas.webhook import ( WebhookIncomingRead, ) from app.services.link_preview_service import get_link_previews_for_urls -from app.services.message_events import broadcast_room_added +from app.services.message_events import broadcast_new_message, broadcast_room_added from app.services.message_service import ( + create_message, get_reactions_for_messages, list_recent_messages, list_room_attachments, @@ -655,7 +656,35 @@ async def add_member_endpoint( raise HTTPException(status_code=404, detail="No user with that ID") except AlreadyMemberError: raise HTTPException(status_code=409, detail="That user is already a member") + + # room_added first -- the new member's client needs to know this room + # exists before it can make sense of an unread_update for it, which the + # welcome message below would otherwise trigger out of order. await broadcast_room_added(request.app.state.broadcaster, data.user_id, room) + + # #72: attributed to the admin doing the adding, not a new system/bot + # sender concept -- they're already a real, in-scope user for this + # request, and every Message row requires a real user_id today. + welcome_name = membership.user.display_name or membership.user.username + welcome_message = await create_message( + db, room.id, current_user.id, f"Welcome to #{room.name}, {welcome_name}!" + ) + # Same "sending implies having seen the room" reasoning as ws/chat.py's + # own live-message path -- without it, the admin's own client would show + # this room as unread from a message they effectively just sent. + await mark_room_read(db, room.id, current_user.id) + await broadcast_new_message( + db, + request.app.state.broadcaster, + request.app.state.presence, + request.app.state.focus_presence, + request.app.state.global_presence, + str(request.base_url), + room.id, + welcome_message, + current_user, + ) + online_ids = await request.app.state.global_presence.online_user_ids([membership.user_id]) return RoomMemberRead( user_id=membership.user_id, diff --git a/backend/tests/test_rooms.py b/backend/tests/test_rooms.py index 529ef44..e7fa6c8 100644 --- a/backend/tests/test_rooms.py +++ b/backend/tests/test_rooms.py @@ -484,6 +484,27 @@ async def test_add_member_directly(client, db_session, monkeypatch): assert "added" in calls[0]["subject"].lower() +async def test_add_member_posts_welcome_message(client, db_session, monkeypatch): + # #72: attributed to the admin who added them, since there's no + # system/bot sender concept -- mirrors test_add_member_directly's setup. + _fake_send_email(monkeypatch) + alice = await register_and_login(client, db_session, username="alice") + room_id = (await client.post("/api/rooms", json={"name": "general"})).json()["id"] + + await client.post("/api/auth/logout") + bob = await register_and_login(client, db_session, username="bob") + + await client.post("/api/auth/logout") + await login_as(client, "alice") + resp = await client.post(f"/api/rooms/{room_id}/members", json={"user_id": bob["id"]}) + assert resp.status_code == 201, resp.text + + history = (await client.get(f"/api/rooms/{room_id}/messages")).json() + welcome_messages = [m for m in history if m["username"] == "alice"] + assert len(welcome_messages) == 1 + assert welcome_messages[0]["content"] == "Welcome to #general, bob!" + + async def test_add_member_requires_admin_role(client, db_session, monkeypatch): _fake_send_email(monkeypatch) await register_and_login(client, db_session, username="alice")