summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--changelog.d/16679.feature1
-rw-r--r--docs/server_notices.md6
-rw-r--r--docs/usage/configuration/config_documentation.md8
-rw-r--r--synapse/config/server_notices.py12
-rw-r--r--synapse/server_notices/server_notices_manager.py113
-rw-r--r--tests/rest/admin/test_server_notice.py109
6 files changed, 235 insertions, 14 deletions
diff --git a/changelog.d/16679.feature b/changelog.d/16679.feature
new file mode 100644
index 0000000000..85af837ae1
--- /dev/null
+++ b/changelog.d/16679.feature
@@ -0,0 +1 @@
+Add config options to set the avatar and the topic of the server notices room.
diff --git a/docs/server_notices.md b/docs/server_notices.md
index aae25d23b8..33b2f4c9ee 100644
--- a/docs/server_notices.md
+++ b/docs/server_notices.md
@@ -44,14 +44,16 @@ section, which should look like this:
 server_notices:
    system_mxid_localpart: server
    system_mxid_display_name: "Server Notices"
-   system_mxid_avatar_url: "mxc://server.com/oumMVlgDnLYFaPVkExemNVVZ"
+   system_mxid_avatar_url: "mxc://example.com/oumMVlgDnLYFaPVkExemNVVZ"
    room_name: "Server Notices"
+   room_avatar_url: "mxc://example.com/oumMVlgDnLYFaPVkExemNVVZ"
+   room_topic: "Room used by your server admin to notice you of important information"
    auto_join: true
 ```
 
 The only compulsory setting is `system_mxid_localpart`, which defines the user
 id of the Server Notices user, as above. `room_name` defines the name of the
-room which will be created.
+room which will be created, `room_avatar_url` its avatar and `room_topic` its topic.
 
 `system_mxid_display_name` and `system_mxid_avatar_url` can be used to set the
 displayname and avatar of the Server Notices user.
diff --git a/docs/usage/configuration/config_documentation.md b/docs/usage/configuration/config_documentation.md
index 560cd65977..90ea8d093f 100644
--- a/docs/usage/configuration/config_documentation.md
+++ b/docs/usage/configuration/config_documentation.md
@@ -3837,16 +3837,22 @@ Sub-options for this setting include:
 * `system_mxid_display_name`: set the display name of the "notices" user
 * `system_mxid_avatar_url`: set the avatar for the "notices" user
 * `room_name`: set the room name of the server notices room
+* `room_avatar_url`: optional string. The room avatar to use for server notice rooms. If set to the empty string `""`, notice rooms will not be given an avatar. Defaults to the empty string. _Added in Synapse 1.99.0._
+* `room_topic`: optional string. The topic to use for server notice rooms. If set to the empty string `""`, notice rooms will not be given a topic. Defaults to the empty string.  _Added in Synapse 1.99.0._
 * `auto_join`: boolean. If true, the user will be automatically joined to the room instead of being invited.
   Defaults to false. _Added in Synapse 1.98.0._
 
+Note that the name, topic and avatar of existing server notice rooms will only be updated when a new notice event is sent.
+
 Example configuration:
 ```yaml
 server_notices:
   system_mxid_localpart: notices
   system_mxid_display_name: "Server Notices"
-  system_mxid_avatar_url: "mxc://server.com/oumMVlgDnLYFaPVkExemNVVZ"
+  system_mxid_avatar_url: "mxc://example.com/oumMVlgDnLYFaPVkExemNVVZ"
   room_name: "Server Notices"
+  room_avatar_url: "mxc://example.com/oumMVlgDnLYFaPVkExemNVVZ"
+  room_topic: "Room used by your server admin to notice you of important information"
   auto_join: true
 ```
 ---
diff --git a/synapse/config/server_notices.py b/synapse/config/server_notices.py
index a8badba0f8..79f365cad5 100644
--- a/synapse/config/server_notices.py
+++ b/synapse/config/server_notices.py
@@ -38,6 +38,14 @@ class ServerNoticesConfig(Config):
         server_notices_room_name (str|None):
             The name to use for the server notices room.
             None if server notices are not enabled.
+
+        server_notices_room_avatar_url (str|None):
+            The avatar URL to use for the server notices room.
+            None if server notices are not enabled.
+
+        server_notices_room_topic (str|None):
+            The topic to use for the server notices room.
+            None if server notices are not enabled.
     """
 
     section = "servernotices"
@@ -48,6 +56,8 @@ class ServerNoticesConfig(Config):
         self.server_notices_mxid_display_name: Optional[str] = None
         self.server_notices_mxid_avatar_url: Optional[str] = None
         self.server_notices_room_name: Optional[str] = None
+        self.server_notices_room_avatar_url: Optional[str] = None
+        self.server_notices_room_topic: Optional[str] = None
         self.server_notices_auto_join: bool = False
 
     def read_config(self, config: JsonDict, **kwargs: Any) -> None:
@@ -63,4 +73,6 @@ class ServerNoticesConfig(Config):
         self.server_notices_mxid_avatar_url = c.get("system_mxid_avatar_url", None)
         # todo: i18n
         self.server_notices_room_name = c.get("room_name", "Server Notices")
+        self.server_notices_room_avatar_url = c.get("room_avatar_url", None)
+        self.server_notices_room_topic = c.get("room_topic", None)
         self.server_notices_auto_join = c.get("auto_join", False)
diff --git a/synapse/server_notices/server_notices_manager.py b/synapse/server_notices/server_notices_manager.py
index 2353b5d47f..39a54362d8 100644
--- a/synapse/server_notices/server_notices_manager.py
+++ b/synapse/server_notices/server_notices_manager.py
@@ -16,7 +16,7 @@ from typing import TYPE_CHECKING, Optional
 
 from synapse.api.constants import EventTypes, Membership, RoomCreationPreset
 from synapse.events import EventBase
-from synapse.types import Requester, StreamKeyType, UserID, create_requester
+from synapse.types import JsonDict, Requester, StreamKeyType, UserID, create_requester
 from synapse.util.caches.descriptors import cached
 
 if TYPE_CHECKING:
@@ -36,6 +36,7 @@ class ServerNoticesManager:
         self._room_member_handler = hs.get_room_member_handler()
         self._event_creation_handler = hs.get_event_creation_handler()
         self._message_handler = hs.get_message_handler()
+        self._storage_controllers = hs.get_storage_controllers()
         self._is_mine_id = hs.is_mine_id
         self._server_name = hs.hostname
 
@@ -160,6 +161,27 @@ class ServerNoticesManager:
                 self._config.servernotices.server_notices_mxid_display_name,
                 self._config.servernotices.server_notices_mxid_avatar_url,
             )
+            await self._update_room_info(
+                requester,
+                room_id,
+                EventTypes.Name,
+                "name",
+                self._config.servernotices.server_notices_room_name,
+            )
+            await self._update_room_info(
+                requester,
+                room_id,
+                EventTypes.RoomAvatar,
+                "url",
+                self._config.servernotices.server_notices_room_avatar_url,
+            )
+            await self._update_room_info(
+                requester,
+                room_id,
+                EventTypes.Topic,
+                "topic",
+                self._config.servernotices.server_notices_room_topic,
+            )
             return room_id
 
         # apparently no existing notice room: create a new one
@@ -178,15 +200,31 @@ class ServerNoticesManager:
                 "avatar_url": self._config.servernotices.server_notices_mxid_avatar_url,
             }
 
+        room_config: JsonDict = {
+            "preset": RoomCreationPreset.PRIVATE_CHAT,
+            "power_level_content_override": {"users_default": -10},
+        }
+
+        if self._config.servernotices.server_notices_room_name:
+            room_config["name"] = self._config.servernotices.server_notices_room_name
+        if self._config.servernotices.server_notices_room_topic:
+            room_config["topic"] = self._config.servernotices.server_notices_room_topic
+        if self._config.servernotices.server_notices_room_avatar_url:
+            room_config["initial_state"] = [
+                {
+                    "type": EventTypes.RoomAvatar,
+                    "state_key": "",
+                    "content": {
+                        "url": self._config.servernotices.server_notices_room_avatar_url,
+                    },
+                }
+            ]
+
         # `ignore_forced_encryption` is used to bypass `encryption_enabled_by_default_for_room_type`
         # setting if it set, since the server notices will not be encrypted anyway.
         room_id, _, _ = await self._room_creation_handler.create_room(
             requester,
-            config={
-                "preset": RoomCreationPreset.PRIVATE_CHAT,
-                "name": self._config.servernotices.server_notices_room_name,
-                "power_level_content_override": {"users_default": -10},
-            },
+            config=room_config,
             ratelimit=False,
             creator_join_profile=join_profile,
             ignore_forced_encryption=True,
@@ -265,11 +303,12 @@ class ServerNoticesManager:
 
         assert self.server_notices_mxid is not None
 
-        notice_user_data_in_room = await self._message_handler.get_room_data(
-            create_requester(self.server_notices_mxid),
-            room_id,
-            EventTypes.Member,
-            self.server_notices_mxid,
+        notice_user_data_in_room = (
+            await self._storage_controllers.state.get_current_state_event(
+                room_id,
+                EventTypes.Member,
+                self.server_notices_mxid,
+            )
         )
 
         assert notice_user_data_in_room is not None
@@ -288,3 +327,55 @@ class ServerNoticesManager:
                 ratelimit=False,
                 content={"displayname": display_name, "avatar_url": avatar_url},
             )
+
+    async def _update_room_info(
+        self,
+        requester: Requester,
+        room_id: str,
+        info_event_type: str,
+        info_content_key: str,
+        info_value: Optional[str],
+    ) -> None:
+        """
+        Updates a specific notice room's info if it's different from what is set.
+
+        Args:
+            requester: The user who is performing the update.
+            room_id: The ID of the server notice room
+            info_event_type: The event type holding the specific info
+            info_content_key: The key containing the specific info in the event's content
+            info_value: The expected value for the specific info
+        """
+        room_info_event = await self._storage_controllers.state.get_current_state_event(
+            room_id,
+            info_event_type,
+            "",
+        )
+
+        existing_info_value = None
+        if room_info_event:
+            existing_info_value = room_info_event.get(info_content_key)
+        if existing_info_value == info_value:
+            return
+        if not existing_info_value and not info_value:
+            # A missing `info_value` can either be represented by a None
+            # or an empty string, so we assume that if they're both falsey
+            # they're equivalent.
+            return
+
+        if info_value is None:
+            info_value = ""
+
+        room_info_event_dict = {
+            "type": info_event_type,
+            "room_id": room_id,
+            "sender": requester.user.to_string(),
+            "state_key": "",
+            "content": {
+                info_content_key: info_value,
+            },
+        }
+
+        event, _ = await self._event_creation_handler.create_and_send_nonmember_event(
+            requester, room_info_event_dict, ratelimit=False
+        )
diff --git a/tests/rest/admin/test_server_notice.py b/tests/rest/admin/test_server_notice.py
index 2398bc503a..e1d4ceb698 100644
--- a/tests/rest/admin/test_server_notice.py
+++ b/tests/rest/admin/test_server_notice.py
@@ -596,6 +596,115 @@ class ServerNoticeTestCase(unittest.HomeserverTestCase):
         )
         self.assertEqual(notice_user_state["avatar_url"], new_avatar_url)
 
+    @override_config(
+        {
+            "server_notices": {
+                "system_mxid_localpart": "notices",
+                "room_avatar_url": "test/url",
+                "room_topic": "Test Topic",
+            }
+        }
+    )
+    def test_notice_room_avatar_and_topic(self) -> None:
+        """
+        Tests that using `room_avatar_url` and `room_topic` config properly sets
+        those properties for the created notice rooms.
+        """
+        server_notice_request_content = {
+            "user_id": self.other_user,
+            "content": {"msgtype": "m.text", "body": "test msg one"},
+        }
+
+        self.make_request(
+            "POST",
+            self.url,
+            access_token=self.admin_user_tok,
+            content=server_notice_request_content,
+        )
+
+        invited_rooms = self._check_invite_and_join_status(self.other_user, 1, 0)
+        notice_room_id = invited_rooms[0].room_id
+        self.helper.join(
+            room=notice_room_id, user=self.other_user, tok=self.other_user_token
+        )
+
+        room_avatar_state = self.helper.get_state(
+            notice_room_id,
+            "m.room.avatar",
+            self.other_user_token,
+            state_key="",
+        )
+        self.assertEqual(room_avatar_state["url"], "test/url")
+
+        room_topic_state = self.helper.get_state(
+            notice_room_id,
+            "m.room.topic",
+            self.other_user_token,
+            state_key="",
+        )
+        self.assertEqual(room_topic_state["topic"], "Test Topic")
+
+    @override_config(
+        {
+            "server_notices": {
+                "system_mxid_localpart": "notices",
+                "room_avatar_url": "test/url",
+            }
+        }
+    )
+    def test_update_room_avatar_when_changed(self) -> None:
+        """
+        Tests that existing server notices room avatar is updated when it is
+        different from the one in homeserver config.
+        """
+        server_notice_request_content = {
+            "user_id": self.other_user,
+            "content": {"msgtype": "m.text", "body": "test msg one"},
+        }
+
+        self.make_request(
+            "POST",
+            self.url,
+            access_token=self.admin_user_tok,
+            content=server_notice_request_content,
+        )
+
+        invited_rooms = self._check_invite_and_join_status(self.other_user, 1, 0)
+        notice_room_id = invited_rooms[0].room_id
+        self.helper.join(
+            room=notice_room_id, user=self.other_user, tok=self.other_user_token
+        )
+
+        room_avatar_state = self.helper.get_state(
+            notice_room_id,
+            "m.room.avatar",
+            self.other_user_token,
+            state_key="",
+        )
+        self.assertEqual(room_avatar_state["url"], "test/url")
+
+        # simulate a change in server config after a server restart.
+        new_avatar_url = "test/new-url"
+        self.server_notices_manager._config.servernotices.server_notices_room_avatar_url = (
+            new_avatar_url
+        )
+        self.server_notices_manager.get_or_create_notice_room_for_user.cache.invalidate_all()
+
+        self.make_request(
+            "POST",
+            self.url,
+            access_token=self.admin_user_tok,
+            content=server_notice_request_content,
+        )
+
+        room_avatar_state = self.helper.get_state(
+            notice_room_id,
+            "m.room.avatar",
+            self.other_user_token,
+            state_key="",
+        )
+        self.assertEqual(room_avatar_state["url"], new_avatar_url)
+
     def _check_invite_and_join_status(
         self, user_id: str, expected_invites: int, expected_memberships: int
     ) -> Sequence[RoomsForUser]: