Skip to content

[Server Announcements] [Create/Configure Rooms] Configure announcement rooms and reconcile them on startup - #20372

Open
anoadragon453 wants to merge 3 commits into
developfrom
anoa/server_announcements_config_rooms
Open

anoadragon453 wants to merge 3 commits into
developfrom
anoa/server_announcements_config_rooms

Conversation

@anoadragon453

@anoadragon453 anoadragon453 commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

Epic ticket: #20240
Part 1 of implementing #20344

This PR implements configuring server announcement rooms. Server admins can configure one or more announcement rooms and decide who is allowed to send events in it. The homeserver will then actually create the room, and configure it. The reasoning behind the choices in state events are in #20344.

Preventing room members from modifying state will come in a follow-up PR.

Recommended to review commit-by-commit.

Pull Request Checklist

  • Pull request is based on the develop branch
  • Pull request includes a changelog file. The entry should:
    • Be a short description of your change which makes sense to users. "Fixed a bug that prevented receiving messages from other servers." instead of "Moved X method from EventStore to EventWorkerStore.".
    • Use markdown where necessary, mostly for code blocks.
    • End with either a period (.) or an exclamation mark (!).
    • Start with a capital letter.
    • Feel free to credit yourself, by adding a sentence "Contributed by @github_username." or "Contributed by [Your Name]." to the end of the entry.
  • Code style is correct (run the linters)

@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Schema Diff

Please check that this looks as expected!

common (sqlite)

Unchanged

common (postgres)

Unchanged

main (sqlite)

--- /tmp/schema_diff_yujy0ztl/before/main/full_schemas/9999/full.sql.sqlite	2026-10-19 11:27:10.659194123 +0000
+++ /tmp/schema_diff_yujy0ztl/after/main/full_schemas/9999/full.sql.sqlite	2026-10-19 11:26:55.652875229 +0000
@@ -1219,20 +1219,27 @@
     -- all subsequent sticky events for the same room with higher
     -- `stream_id`s are also unsent.
     --
     -- Not a foreign key because we must still support expiration of sticky events.
     sticky_events_stream_position INTEGER NOT NULL,
 
     -- It's enough to track one position per (destination, room_id) pair.
     PRIMARY KEY (destination, room_id)
 );
 CREATE INDEX destination_rooms_sticky_events_backlog_room_id ON destination_rooms_sticky_events_backlog (room_id);
+CREATE TABLE server_announcement_rooms (
+    id TEXT PRIMARY KEY NOT NULL,
+    room_id TEXT UNIQUE NOT NULL,
+    avatar_path TEXT,
+    avatar_sha256 TEXT,
+    avatar_mxc TEXT
+);
 CREATE UNIQUE INDEX receipts_graph_unique_index ON receipts_graph (room_id, receipt_type, user_id) WHERE thread_id IS NULL;
 CREATE UNIQUE INDEX receipts_linearized_unique_index ON receipts_linearized (room_id, receipt_type, user_id) WHERE thread_id IS NULL;
 CREATE INDEX device_lists_changes_in_room_by_room_idx ON device_lists_changes_in_room (room_id, stream_id) ;
 CREATE INDEX events_jump_to_date_idx ON events (room_id, origin_server_ts) WHERE NOT outlier;
 CREATE INDEX room_membership_user_room_idx ON room_memberships (user_id, room_id) ;
 CREATE UNIQUE INDEX profiles_full_user_id_key ON profiles (full_user_id) ;
 CREATE UNIQUE INDEX full_users_unique_idx ON user_filters (full_user_id, filter_id) ;
 CREATE INDEX event_push_actions_stream_highlight_index ON event_push_actions (highlight, stream_ordering) WHERE highlight=0;
 CREATE INDEX current_state_events_stream_ordering_idx ON current_state_events (event_stream_ordering) ;
 CREATE INDEX local_current_membership_stream_ordering_idx ON local_current_membership (event_stream_ordering) ;

main (postgres)

--- /tmp/schema_diff_yujy0ztl/before/main/full_schemas/9999/full.sql.postgres	2026-10-19 11:27:11.379209414 +0000
+++ /tmp/schema_diff_yujy0ztl/after/main/full_schemas/9999/full.sql.postgres	2026-10-19 11:26:56.384890799 +0000
@@ -1022,20 +1022,27 @@
 CREATE TABLE scheduled_tasks (
     id text NOT NULL,
     action text NOT NULL,
     status text NOT NULL,
     "timestamp" bigint NOT NULL,
     resource_id text,
     params text,
     result text,
     error text
 );
+CREATE TABLE server_announcement_rooms (
+    id text NOT NULL,
+    room_id text NOT NULL,
+    avatar_path text,
+    avatar_sha256 text,
+    avatar_mxc text
+);
 CREATE TABLE server_keys_json (
     server_name text NOT NULL,
     key_id text NOT NULL,
     from_server text NOT NULL,
     ts_added_ms bigint NOT NULL,
     ts_valid_until_ms bigint NOT NULL,
     key_json bytea NOT NULL
 );
 CREATE TABLE server_signature_keys (
     server_name text,
@@ -1572,20 +1579,24 @@
 ALTER TABLE ONLY room_stats_current
     ADD CONSTRAINT room_stats_current_pkey PRIMARY KEY (room_id);
 ALTER TABLE ONLY room_tags_revisions
     ADD CONSTRAINT room_tag_revisions_uniqueness UNIQUE (user_id, room_id);
 ALTER TABLE ONLY room_tags
     ADD CONSTRAINT room_tag_uniqueness UNIQUE (user_id, room_id, tag);
 ALTER TABLE ONLY rooms
     ADD CONSTRAINT rooms_pkey PRIMARY KEY (room_id);
 ALTER TABLE ONLY scheduled_tasks
     ADD CONSTRAINT scheduled_tasks_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY server_announcement_rooms
+    ADD CONSTRAINT server_announcement_rooms_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY server_announcement_rooms
+    ADD CONSTRAINT server_announcement_rooms_room_id_key UNIQUE (room_id);
 ALTER TABLE ONLY server_keys_json
     ADD CONSTRAINT server_keys_json_uniqueness UNIQUE (server_name, key_id, from_server);
 ALTER TABLE ONLY server_signature_keys
     ADD CONSTRAINT server_signature_keys_server_name_key_id_key UNIQUE (server_name, key_id);
 ALTER TABLE ONLY sessions
     ADD CONSTRAINT sessions_session_type_session_id_key UNIQUE (session_type, session_id);
 ALTER TABLE ONLY sliding_sync_connection_positions
     ADD CONSTRAINT sliding_sync_connection_positions_pkey PRIMARY KEY (connection_position);
 ALTER TABLE ONLY sliding_sync_connection_required_state
     ADD CONSTRAINT sliding_sync_connection_required_state_pkey PRIMARY KEY (required_state_id);

state (sqlite)

Unchanged

state (postgres)

Unchanged

@anoadragon453
anoadragon453 force-pushed the anoa/server_announcements_config_rooms branch 3 times, most recently from 8190e6f to e638bf5 Compare October 16, 2026 20:48
@anoadragon453
anoadragon453 force-pushed the anoa/server_announcements_config_rooms branch from e638bf5 to 9a307d4 Compare October 16, 2026 21:00
Allow storing mappings between server announcement room config and
configured Matrix rooms.
@anoadragon453
anoadragon453 force-pushed the anoa/server_announcements_config_rooms branch from 9a307d4 to 326f828 Compare October 19, 2026 10:55
@anoadragon453
anoadragon453 marked this pull request as ready for review October 19, 2026 10:55
@anoadragon453
anoadragon453 requested a review from a team as a code owner October 19, 2026 10:55
@anoadragon453
anoadragon453 requested review from barodeur and sandhose and removed request for a team and sandhose October 19, 2026 10:55
Create and update the room state of server announcement rooms that are
configured. Rooms that are removed from configuration are kept, but no
longer given special treatment.

To remove old announcement rooms, server admins can purge them in the
usual manner. Purging these rooms also removes the announcement `id` ->
matrix room ID mapping in the `server_announcement_rooms` table.
@anoadragon453
anoadragon453 force-pushed the anoa/server_announcements_config_rooms branch from 326f828 to ffdc35f Compare October 19, 2026 11:24
@MadLittleMods MadLittleMods added the A-Server-Announcements Message all users on a homeserver at once (separate from "Server Notices") label Oct 19, 2026

@barodeur barodeur left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥

None.
"""

async def get_server_announcement_room(self, announcement_id: str) -> str | None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: because this is only internal naming

I found it confusing when reviewing that the "Announcement Room Id" was announcement_id. I understand we cannot use room_id either.
Maybe something like config_id would make things less confusing

Comment on lines +171 to +176
# We don't want announcement room events to federate to other homeservers.
# If this room is somehow federating, then we shouldn't use it.
raise ConfigError(
f'Announcement room {room.id} ({room_id}) has "m.federate": true '
"in its create event. Please create another room (with a new id)",
("experimental_features", "server_announcements", "rooms", room.id),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🥲 not sure how this can happen, but the safeguard is nice

# Check that all configured announcement sending users exist.
for user_id in room.allowed_user_ids:
user = await self._store.get_user_by_id(user_id)
if user is None or user.is_deactivated:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems reasonable to handle deactivated account.

I wonder if we should just warn instead of raising an error in that case.
Being forced to reconfigure after the account is de-activated could create some friction between user management and ops.

Comment on lines +130 to +139
is_server_announcements_room = (
await self._store.get_server_announcement_id(room.room_id) is not None
)

if not is_server_notices_room or is_server_announcements_room:
continue

# we found a room which our user shares with the system notice
# user
return room.room_id

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To prevent calling self._store.get_server_announcement_id for every room, we could instead:

Suggested change
is_server_announcements_room = (
await self._store.get_server_announcement_id(room.room_id) is not None
)
if not is_server_notices_room or is_server_announcements_room:
continue
# we found a room which our user shares with the system notice
# user
return room.room_id
if (
is_server_notices_room
and await self._store.get_server_announcement_id(room.room_id) is None
):
# we found a room which our user shares with the system notice
# user
return room.room_id

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Server-Announcements Message all users on a homeserver at once (separate from "Server Notices")

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants