@@ -0,0 +1 @@ | |||||
Add config options to set the avatar and the topic of the server notices room. |
@@ -44,14 +44,16 @@ section, which should look like this: | |||||
server_notices: | server_notices: | ||||
system_mxid_localpart: server | system_mxid_localpart: server | ||||
system_mxid_display_name: "Server 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_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 | auto_join: true | ||||
``` | ``` | ||||
The only compulsory setting is `system_mxid_localpart`, which defines the user | 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 | 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 | `system_mxid_display_name` and `system_mxid_avatar_url` can be used to set the | ||||
displayname and avatar of the Server Notices user. | displayname and avatar of the Server Notices user. | ||||
@@ -3837,16 +3837,22 @@ Sub-options for this setting include: | |||||
* `system_mxid_display_name`: set the display name of the "notices" user | * `system_mxid_display_name`: set the display name of the "notices" user | ||||
* `system_mxid_avatar_url`: set the avatar for 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_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. | * `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._ | 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: | Example configuration: | ||||
```yaml | ```yaml | ||||
server_notices: | server_notices: | ||||
system_mxid_localpart: notices | system_mxid_localpart: notices | ||||
system_mxid_display_name: "Server 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_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 | auto_join: true | ||||
``` | ``` | ||||
--- | --- | ||||
@@ -38,6 +38,14 @@ class ServerNoticesConfig(Config): | |||||
server_notices_room_name (str|None): | server_notices_room_name (str|None): | ||||
The name to use for the server notices room. | The name to use for the server notices room. | ||||
None if server notices are not enabled. | 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" | section = "servernotices" | ||||
@@ -48,6 +56,8 @@ class ServerNoticesConfig(Config): | |||||
self.server_notices_mxid_display_name: Optional[str] = None | self.server_notices_mxid_display_name: Optional[str] = None | ||||
self.server_notices_mxid_avatar_url: Optional[str] = None | self.server_notices_mxid_avatar_url: Optional[str] = None | ||||
self.server_notices_room_name: 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 | self.server_notices_auto_join: bool = False | ||||
def read_config(self, config: JsonDict, **kwargs: Any) -> None: | 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) | self.server_notices_mxid_avatar_url = c.get("system_mxid_avatar_url", None) | ||||
# todo: i18n | # todo: i18n | ||||
self.server_notices_room_name = c.get("room_name", "Server Notices") | 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) | self.server_notices_auto_join = c.get("auto_join", False) |
@@ -16,7 +16,7 @@ from typing import TYPE_CHECKING, Optional | |||||
from synapse.api.constants import EventTypes, Membership, RoomCreationPreset | from synapse.api.constants import EventTypes, Membership, RoomCreationPreset | ||||
from synapse.events import EventBase | 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 | from synapse.util.caches.descriptors import cached | ||||
if TYPE_CHECKING: | if TYPE_CHECKING: | ||||
@@ -36,6 +36,7 @@ class ServerNoticesManager: | |||||
self._room_member_handler = hs.get_room_member_handler() | self._room_member_handler = hs.get_room_member_handler() | ||||
self._event_creation_handler = hs.get_event_creation_handler() | self._event_creation_handler = hs.get_event_creation_handler() | ||||
self._message_handler = hs.get_message_handler() | self._message_handler = hs.get_message_handler() | ||||
self._storage_controllers = hs.get_storage_controllers() | |||||
self._is_mine_id = hs.is_mine_id | self._is_mine_id = hs.is_mine_id | ||||
self._server_name = hs.hostname | 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_display_name, | ||||
self._config.servernotices.server_notices_mxid_avatar_url, | 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 | return room_id | ||||
# apparently no existing notice room: create a new one | # 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, | "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` | # `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. | # setting if it set, since the server notices will not be encrypted anyway. | ||||
room_id, _, _ = await self._room_creation_handler.create_room( | room_id, _, _ = await self._room_creation_handler.create_room( | ||||
requester, | 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, | ratelimit=False, | ||||
creator_join_profile=join_profile, | creator_join_profile=join_profile, | ||||
ignore_forced_encryption=True, | ignore_forced_encryption=True, | ||||
@@ -265,11 +303,12 @@ class ServerNoticesManager: | |||||
assert self.server_notices_mxid is not None | 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 | assert notice_user_data_in_room is not None | ||||
@@ -288,3 +327,55 @@ class ServerNoticesManager: | |||||
ratelimit=False, | ratelimit=False, | ||||
content={"displayname": display_name, "avatar_url": avatar_url}, | 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 | |||||
) |
@@ -596,6 +596,115 @@ class ServerNoticeTestCase(unittest.HomeserverTestCase): | |||||
) | ) | ||||
self.assertEqual(notice_user_state["avatar_url"], new_avatar_url) | 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( | def _check_invite_and_join_status( | ||||
self, user_id: str, expected_invites: int, expected_memberships: int | self, user_id: str, expected_invites: int, expected_memberships: int | ||||
) -> Sequence[RoomsForUser]: | ) -> Sequence[RoomsForUser]: | ||||