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 <noreply@anthropic.com>
This commit is contained in:
2026-09-03 20:35:09 -06:00
co-authored by Claude Sonnet 5
parent 6e889b8ea4
commit 019e10ac5c
2 changed files with 51 additions and 1 deletions
+30 -1
View File
@@ -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,
+21
View File
@@ -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")