You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

382 lines
14 KiB

  1. # Copyright 2018 New Vector Ltd
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import logging
  15. from typing import TYPE_CHECKING, Optional
  16. from synapse.api.constants import EventTypes, Membership, RoomCreationPreset
  17. from synapse.events import EventBase
  18. from synapse.types import JsonDict, Requester, StreamKeyType, UserID, create_requester
  19. from synapse.util.caches.descriptors import cached
  20. if TYPE_CHECKING:
  21. from synapse.server import HomeServer
  22. logger = logging.getLogger(__name__)
  23. SERVER_NOTICE_ROOM_TAG = "m.server_notice"
  24. class ServerNoticesManager:
  25. def __init__(self, hs: "HomeServer"):
  26. self._store = hs.get_datastores().main
  27. self._config = hs.config
  28. self._account_data_handler = hs.get_account_data_handler()
  29. self._room_creation_handler = hs.get_room_creation_handler()
  30. self._room_member_handler = hs.get_room_member_handler()
  31. self._event_creation_handler = hs.get_event_creation_handler()
  32. self._message_handler = hs.get_message_handler()
  33. self._storage_controllers = hs.get_storage_controllers()
  34. self._is_mine_id = hs.is_mine_id
  35. self._server_name = hs.hostname
  36. self._notifier = hs.get_notifier()
  37. self.server_notices_mxid = self._config.servernotices.server_notices_mxid
  38. def is_enabled(self) -> bool:
  39. """Checks if server notices are enabled on this server."""
  40. return self.server_notices_mxid is not None
  41. async def send_notice(
  42. self,
  43. user_id: str,
  44. event_content: dict,
  45. type: str = EventTypes.Message,
  46. state_key: Optional[str] = None,
  47. txn_id: Optional[str] = None,
  48. ) -> EventBase:
  49. """Send a notice to the given user
  50. Creates the server notices room, if none exists.
  51. Args:
  52. user_id: mxid of user to send event to.
  53. event_content: content of event to send
  54. type: type of event
  55. is_state_event: Is the event a state event
  56. txn_id: The transaction ID.
  57. """
  58. room_id = await self.get_or_create_notice_room_for_user(user_id)
  59. await self.maybe_invite_user_to_room(user_id, room_id)
  60. assert self.server_notices_mxid is not None
  61. requester = create_requester(
  62. self.server_notices_mxid, authenticated_entity=self._server_name
  63. )
  64. logger.info("Sending server notice to %s", user_id)
  65. event_dict = {
  66. "type": type,
  67. "room_id": room_id,
  68. "sender": self.server_notices_mxid,
  69. "content": event_content,
  70. }
  71. if state_key is not None:
  72. event_dict["state_key"] = state_key
  73. event, _ = await self._event_creation_handler.create_and_send_nonmember_event(
  74. requester, event_dict, ratelimit=False, txn_id=txn_id
  75. )
  76. return event
  77. @cached()
  78. async def maybe_get_notice_room_for_user(self, user_id: str) -> Optional[str]:
  79. """Try to look up the server notice room for this user if it exists.
  80. Does not create one if none can be found.
  81. Args:
  82. user_id: the user we want a server notice room for.
  83. Returns:
  84. The room's ID, or None if no room could be found.
  85. """
  86. # If there is no server notices MXID, then there is no server notices room
  87. if self.server_notices_mxid is None:
  88. return None
  89. rooms = await self._store.get_rooms_for_local_user_where_membership_is(
  90. user_id, [Membership.INVITE, Membership.JOIN]
  91. )
  92. for room in rooms:
  93. # it's worth noting that there is an asymmetry here in that we
  94. # expect the user to be invited or joined, but the system user must
  95. # be joined. This is kinda deliberate, in that if somebody somehow
  96. # manages to invite the system user to a room, that doesn't make it
  97. # the server notices room.
  98. is_server_notices_room = await self._store.check_local_user_in_room(
  99. user_id=self.server_notices_mxid, room_id=room.room_id
  100. )
  101. if is_server_notices_room:
  102. # we found a room which our user shares with the system notice
  103. # user
  104. return room.room_id
  105. return None
  106. @cached()
  107. async def get_or_create_notice_room_for_user(self, user_id: str) -> str:
  108. """Get the room for notices for a given user
  109. If we have not yet created a notice room for this user, create it, but don't
  110. invite the user to it.
  111. Args:
  112. user_id: complete user id for the user we want a room for
  113. Returns:
  114. room id of notice room.
  115. """
  116. if self.server_notices_mxid is None:
  117. raise Exception("Server notices not enabled")
  118. assert self._is_mine_id(user_id), "Cannot send server notices to remote users"
  119. requester = create_requester(
  120. self.server_notices_mxid, authenticated_entity=self._server_name
  121. )
  122. room_id = await self.maybe_get_notice_room_for_user(user_id)
  123. if room_id is not None:
  124. logger.info(
  125. "Using existing server notices room %s for user %s",
  126. room_id,
  127. user_id,
  128. )
  129. await self._update_notice_user_profile_if_changed(
  130. requester,
  131. room_id,
  132. self._config.servernotices.server_notices_mxid_display_name,
  133. self._config.servernotices.server_notices_mxid_avatar_url,
  134. )
  135. await self._update_room_info(
  136. requester,
  137. room_id,
  138. EventTypes.Name,
  139. "name",
  140. self._config.servernotices.server_notices_room_name,
  141. )
  142. await self._update_room_info(
  143. requester,
  144. room_id,
  145. EventTypes.RoomAvatar,
  146. "url",
  147. self._config.servernotices.server_notices_room_avatar_url,
  148. )
  149. await self._update_room_info(
  150. requester,
  151. room_id,
  152. EventTypes.Topic,
  153. "topic",
  154. self._config.servernotices.server_notices_room_topic,
  155. )
  156. return room_id
  157. # apparently no existing notice room: create a new one
  158. logger.info("Creating server notices room for %s", user_id)
  159. # see if we want to override the profile info for the server user.
  160. # note that if we want to override either the display name or the
  161. # avatar, we have to use both.
  162. join_profile = None
  163. if (
  164. self._config.servernotices.server_notices_mxid_display_name is not None
  165. or self._config.servernotices.server_notices_mxid_avatar_url is not None
  166. ):
  167. join_profile = {
  168. "displayname": self._config.servernotices.server_notices_mxid_display_name,
  169. "avatar_url": self._config.servernotices.server_notices_mxid_avatar_url,
  170. }
  171. room_config: JsonDict = {
  172. "preset": RoomCreationPreset.PRIVATE_CHAT,
  173. "power_level_content_override": {"users_default": -10},
  174. }
  175. if self._config.servernotices.server_notices_room_name:
  176. room_config["name"] = self._config.servernotices.server_notices_room_name
  177. if self._config.servernotices.server_notices_room_topic:
  178. room_config["topic"] = self._config.servernotices.server_notices_room_topic
  179. if self._config.servernotices.server_notices_room_avatar_url:
  180. room_config["initial_state"] = [
  181. {
  182. "type": EventTypes.RoomAvatar,
  183. "state_key": "",
  184. "content": {
  185. "url": self._config.servernotices.server_notices_room_avatar_url,
  186. },
  187. }
  188. ]
  189. # `ignore_forced_encryption` is used to bypass `encryption_enabled_by_default_for_room_type`
  190. # setting if it set, since the server notices will not be encrypted anyway.
  191. room_id, _, _ = await self._room_creation_handler.create_room(
  192. requester,
  193. config=room_config,
  194. ratelimit=False,
  195. creator_join_profile=join_profile,
  196. ignore_forced_encryption=True,
  197. )
  198. self.maybe_get_notice_room_for_user.invalidate((user_id,))
  199. max_id = await self._account_data_handler.add_tag_to_room(
  200. user_id, room_id, SERVER_NOTICE_ROOM_TAG, {}
  201. )
  202. self._notifier.on_new_event(StreamKeyType.ACCOUNT_DATA, max_id, users=[user_id])
  203. logger.info("Created server notices room %s for %s", room_id, user_id)
  204. return room_id
  205. async def maybe_invite_user_to_room(self, user_id: str, room_id: str) -> None:
  206. """Invite the given user to the given server room, unless the user has already
  207. joined or been invited to it.
  208. Args:
  209. user_id: The ID of the user to invite.
  210. room_id: The ID of the room to invite the user to.
  211. """
  212. assert self.server_notices_mxid is not None
  213. requester = create_requester(
  214. self.server_notices_mxid, authenticated_entity=self._server_name
  215. )
  216. # Check whether the user has already joined or been invited to this room. If
  217. # that's the case, there is no need to re-invite them.
  218. joined_rooms = await self._store.get_rooms_for_local_user_where_membership_is(
  219. user_id, [Membership.INVITE, Membership.JOIN]
  220. )
  221. for room in joined_rooms:
  222. if room.room_id == room_id:
  223. return
  224. user_id_obj = UserID.from_string(user_id)
  225. await self._room_member_handler.update_membership(
  226. requester=requester,
  227. target=user_id_obj,
  228. room_id=room_id,
  229. action="invite",
  230. ratelimit=False,
  231. )
  232. if self._config.servernotices.server_notices_auto_join:
  233. user_requester = create_requester(
  234. user_id, authenticated_entity=self._server_name
  235. )
  236. await self._room_member_handler.update_membership(
  237. requester=user_requester,
  238. target=user_id_obj,
  239. room_id=room_id,
  240. action="join",
  241. ratelimit=False,
  242. )
  243. async def _update_notice_user_profile_if_changed(
  244. self,
  245. requester: Requester,
  246. room_id: str,
  247. display_name: Optional[str],
  248. avatar_url: Optional[str],
  249. ) -> None:
  250. """
  251. Updates the notice user's profile if it's different from what is in the room.
  252. Args:
  253. requester: The user who is performing the update.
  254. room_id: The ID of the server notice room
  255. display_name: The displayname of the server notice user
  256. avatar_url: The avatar url of the server notice user
  257. """
  258. logger.debug("Checking whether notice user profile has changed for %s", room_id)
  259. assert self.server_notices_mxid is not None
  260. notice_user_data_in_room = (
  261. await self._storage_controllers.state.get_current_state_event(
  262. room_id,
  263. EventTypes.Member,
  264. self.server_notices_mxid,
  265. )
  266. )
  267. assert notice_user_data_in_room is not None
  268. notice_user_profile_changed = (
  269. display_name != notice_user_data_in_room.content.get("displayname")
  270. or avatar_url != notice_user_data_in_room.content.get("avatar_url")
  271. )
  272. if notice_user_profile_changed:
  273. logger.info("Updating notice user profile in room %s", room_id)
  274. await self._room_member_handler.update_membership(
  275. requester=requester,
  276. target=UserID.from_string(self.server_notices_mxid),
  277. room_id=room_id,
  278. action="join",
  279. ratelimit=False,
  280. content={"displayname": display_name, "avatar_url": avatar_url},
  281. )
  282. async def _update_room_info(
  283. self,
  284. requester: Requester,
  285. room_id: str,
  286. info_event_type: str,
  287. info_content_key: str,
  288. info_value: Optional[str],
  289. ) -> None:
  290. """
  291. Updates a specific notice room's info if it's different from what is set.
  292. Args:
  293. requester: The user who is performing the update.
  294. room_id: The ID of the server notice room
  295. info_event_type: The event type holding the specific info
  296. info_content_key: The key containing the specific info in the event's content
  297. info_value: The expected value for the specific info
  298. """
  299. room_info_event = await self._storage_controllers.state.get_current_state_event(
  300. room_id,
  301. info_event_type,
  302. "",
  303. )
  304. existing_info_value = None
  305. if room_info_event:
  306. existing_info_value = room_info_event.get(info_content_key)
  307. if existing_info_value == info_value:
  308. return
  309. if not existing_info_value and not info_value:
  310. # A missing `info_value` can either be represented by a None
  311. # or an empty string, so we assume that if they're both falsey
  312. # they're equivalent.
  313. return
  314. if info_value is None:
  315. info_value = ""
  316. room_info_event_dict = {
  317. "type": info_event_type,
  318. "room_id": room_id,
  319. "sender": requester.user.to_string(),
  320. "state_key": "",
  321. "content": {
  322. info_content_key: info_value,
  323. },
  324. }
  325. event, _ = await self._event_creation_handler.create_and_send_nonmember_event(
  326. requester, room_info_event_dict, ratelimit=False
  327. )