resource_limits_server_notices.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2018 New Vector Ltd
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import logging
  16. from typing import List, Tuple
  17. from synapse.api.constants import (
  18. EventTypes,
  19. LimitBlockingTypes,
  20. ServerNoticeLimitReached,
  21. ServerNoticeMsgType,
  22. )
  23. from synapse.api.errors import AuthError, ResourceLimitError, SynapseError
  24. from synapse.server_notices.server_notices_manager import SERVER_NOTICE_ROOM_TAG
  25. logger = logging.getLogger(__name__)
  26. class ResourceLimitsServerNotices:
  27. """Keeps track of whether the server has reached it's resource limit and
  28. ensures that the client is kept up to date.
  29. """
  30. def __init__(self, hs):
  31. """
  32. Args:
  33. hs (synapse.server.HomeServer):
  34. """
  35. self._server_notices_manager = hs.get_server_notices_manager()
  36. self._store = hs.get_datastore()
  37. self._auth = hs.get_auth()
  38. self._config = hs.config
  39. self._resouce_limited = False
  40. self._account_data_handler = hs.get_account_data_handler()
  41. self._message_handler = hs.get_message_handler()
  42. self._state = hs.get_state_handler()
  43. self._notifier = hs.get_notifier()
  44. self._enabled = (
  45. hs.config.limit_usage_by_mau
  46. and self._server_notices_manager.is_enabled()
  47. and not hs.config.hs_disabled
  48. )
  49. async def maybe_send_server_notice_to_user(self, user_id: str) -> None:
  50. """Check if we need to send a notice to this user, this will be true in
  51. two cases.
  52. 1. The server has reached its limit does not reflect this
  53. 2. The room state indicates that the server has reached its limit when
  54. actually the server is fine
  55. Args:
  56. user_id: user to check
  57. """
  58. if not self._enabled:
  59. return
  60. timestamp = await self._store.user_last_seen_monthly_active(user_id)
  61. if timestamp is None:
  62. # This user will be blocked from receiving the notice anyway.
  63. # In practice, not sure we can ever get here
  64. return
  65. room_id = await self._server_notices_manager.get_or_create_notice_room_for_user(
  66. user_id
  67. )
  68. if not room_id:
  69. logger.warning("Failed to get server notices room")
  70. return
  71. await self._check_and_set_tags(user_id, room_id)
  72. # Determine current state of room
  73. currently_blocked, ref_events = await self._is_room_currently_blocked(room_id)
  74. limit_msg = None
  75. limit_type = None
  76. try:
  77. # Normally should always pass in user_id to check_auth_blocking
  78. # if you have it, but in this case are checking what would happen
  79. # to other users if they were to arrive.
  80. await self._auth.check_auth_blocking()
  81. except ResourceLimitError as e:
  82. limit_msg = e.msg
  83. limit_type = e.limit_type
  84. try:
  85. if (
  86. limit_type == LimitBlockingTypes.MONTHLY_ACTIVE_USER
  87. and not self._config.mau_limit_alerting
  88. ):
  89. # We have hit the MAU limit, but MAU alerting is disabled:
  90. # reset room if necessary and return
  91. if currently_blocked:
  92. await self._remove_limit_block_notification(user_id, ref_events)
  93. return
  94. if currently_blocked and not limit_msg:
  95. # Room is notifying of a block, when it ought not to be.
  96. await self._remove_limit_block_notification(user_id, ref_events)
  97. elif not currently_blocked and limit_msg:
  98. # Room is not notifying of a block, when it ought to be.
  99. await self._apply_limit_block_notification(
  100. user_id, limit_msg, limit_type # type: ignore
  101. )
  102. except SynapseError as e:
  103. logger.error("Error sending resource limits server notice: %s", e)
  104. async def _remove_limit_block_notification(
  105. self, user_id: str, ref_events: List[str]
  106. ) -> None:
  107. """Utility method to remove limit block notifications from the server
  108. notices room.
  109. Args:
  110. user_id: user to notify
  111. ref_events: The event_ids of pinned events that are unrelated to
  112. limit blocking and need to be preserved.
  113. """
  114. content = {"pinned": ref_events}
  115. await self._server_notices_manager.send_notice(
  116. user_id, content, EventTypes.Pinned, ""
  117. )
  118. async def _apply_limit_block_notification(
  119. self, user_id: str, event_body: str, event_limit_type: str
  120. ) -> None:
  121. """Utility method to apply limit block notifications in the server
  122. notices room.
  123. Args:
  124. user_id: user to notify
  125. event_body: The human readable text that describes the block.
  126. event_limit_type: Specifies the type of block e.g. monthly active user
  127. limit has been exceeded.
  128. """
  129. content = {
  130. "body": event_body,
  131. "msgtype": ServerNoticeMsgType,
  132. "server_notice_type": ServerNoticeLimitReached,
  133. "admin_contact": self._config.admin_contact,
  134. "limit_type": event_limit_type,
  135. }
  136. event = await self._server_notices_manager.send_notice(
  137. user_id, content, EventTypes.Message
  138. )
  139. content = {"pinned": [event.event_id]}
  140. await self._server_notices_manager.send_notice(
  141. user_id, content, EventTypes.Pinned, ""
  142. )
  143. async def _check_and_set_tags(self, user_id: str, room_id: str) -> None:
  144. """
  145. Since server notices rooms were originally not with tags,
  146. important to check that tags have been set correctly
  147. Args:
  148. user_id(str): the user in question
  149. room_id(str): the server notices room for that user
  150. """
  151. tags = await self._store.get_tags_for_room(user_id, room_id)
  152. need_to_set_tag = True
  153. if tags:
  154. if SERVER_NOTICE_ROOM_TAG in tags:
  155. # tag already present, nothing to do here
  156. need_to_set_tag = False
  157. if need_to_set_tag:
  158. max_id = await self._account_data_handler.add_tag_to_room(
  159. user_id, room_id, SERVER_NOTICE_ROOM_TAG, {}
  160. )
  161. self._notifier.on_new_event("account_data_key", max_id, users=[user_id])
  162. async def _is_room_currently_blocked(self, room_id: str) -> Tuple[bool, List[str]]:
  163. """
  164. Determines if the room is currently blocked
  165. Args:
  166. room_id: The room id of the server notices room
  167. Returns:
  168. bool: Is the room currently blocked
  169. list: The list of pinned event IDs that are unrelated to limit blocking
  170. This list can be used as a convenience in the case where the block
  171. is to be lifted and the remaining pinned event references need to be
  172. preserved
  173. """
  174. currently_blocked = False
  175. pinned_state_event = None
  176. try:
  177. pinned_state_event = await self._state.get_current_state(
  178. room_id, event_type=EventTypes.Pinned
  179. )
  180. except AuthError:
  181. # The user has yet to join the server notices room
  182. pass
  183. referenced_events = [] # type: List[str]
  184. if pinned_state_event is not None:
  185. referenced_events = list(pinned_state_event.content.get("pinned", []))
  186. events = await self._store.get_events(referenced_events)
  187. for event_id, event in events.items():
  188. if event.type != EventTypes.Message:
  189. continue
  190. if event.content.get("msgtype") == ServerNoticeMsgType:
  191. currently_blocked = True
  192. # remove event in case we need to disable blocking later on.
  193. if event_id in referenced_events:
  194. referenced_events.remove(event.event_id)
  195. return currently_blocked, referenced_events