Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 
 
 

343 lignes
13 KiB

  1. # Copyright 2015, 2016 OpenMarket 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, Iterable, List, Optional, Sequence, Tuple
  16. from synapse.api.constants import EduTypes, ReceiptTypes
  17. from synapse.appservice import ApplicationService
  18. from synapse.streams import EventSource
  19. from synapse.types import (
  20. JsonDict,
  21. JsonMapping,
  22. ReadReceipt,
  23. StreamKeyType,
  24. UserID,
  25. get_domain_from_id,
  26. )
  27. if TYPE_CHECKING:
  28. from synapse.server import HomeServer
  29. logger = logging.getLogger(__name__)
  30. class ReceiptsHandler:
  31. def __init__(self, hs: "HomeServer"):
  32. self.notifier = hs.get_notifier()
  33. self.server_name = hs.config.server.server_name
  34. self.store = hs.get_datastores().main
  35. self.event_auth_handler = hs.get_event_auth_handler()
  36. self.event_handler = hs.get_event_handler()
  37. self._storage_controllers = hs.get_storage_controllers()
  38. self.hs = hs
  39. # We only need to poke the federation sender explicitly if its on the
  40. # same instance. Other federation sender instances will get notified by
  41. # `synapse.app.generic_worker.FederationSenderHandler` when it sees it
  42. # in the receipts stream.
  43. self.federation_sender = None
  44. if hs.should_send_federation():
  45. self.federation_sender = hs.get_federation_sender()
  46. # If we can handle the receipt EDUs we do so, otherwise we route them
  47. # to the appropriate worker.
  48. if hs.get_instance_name() in hs.config.worker.writers.receipts:
  49. hs.get_federation_registry().register_edu_handler(
  50. EduTypes.RECEIPT, self._received_remote_receipt
  51. )
  52. else:
  53. hs.get_federation_registry().register_instances_for_edu(
  54. EduTypes.RECEIPT,
  55. hs.config.worker.writers.receipts,
  56. )
  57. self.clock = self.hs.get_clock()
  58. self.state = hs.get_state_handler()
  59. async def _received_remote_receipt(self, origin: str, content: JsonDict) -> None:
  60. """Called when we receive an EDU of type m.receipt from a remote HS."""
  61. receipts = []
  62. for room_id, room_values in content.items():
  63. # If we're not in the room just ditch the event entirely. This is
  64. # probably an old server that has come back and thinks we're still in
  65. # the room (or we've been rejoined to the room by a state reset).
  66. is_in_room = await self.event_auth_handler.is_host_in_room(
  67. room_id, self.server_name
  68. )
  69. if not is_in_room:
  70. logger.info(
  71. "Ignoring receipt for room %r from server %s as we're not in the room",
  72. room_id,
  73. origin,
  74. )
  75. continue
  76. # Let's check that the origin server is in the room before accepting the receipt.
  77. # We don't want to block waiting on a partial state so take an
  78. # approximation if needed.
  79. domains = await self._storage_controllers.state.get_current_hosts_in_room_or_partial_state_approximation(
  80. room_id
  81. )
  82. if origin not in domains:
  83. logger.info(
  84. "Ignoring receipt for room %r from server %s as they're not in the room",
  85. room_id,
  86. origin,
  87. )
  88. continue
  89. for receipt_type, users in room_values.items():
  90. for user_id, user_values in users.items():
  91. if get_domain_from_id(user_id) != origin:
  92. logger.info(
  93. "Received receipt for user %r from server %s, ignoring",
  94. user_id,
  95. origin,
  96. )
  97. continue
  98. # Check if these receipts apply to a thread.
  99. data = user_values.get("data", {})
  100. thread_id = data.get("thread_id")
  101. # If the thread ID is invalid, consider it missing.
  102. if not isinstance(thread_id, str):
  103. thread_id = None
  104. receipts.append(
  105. ReadReceipt(
  106. room_id=room_id,
  107. receipt_type=receipt_type,
  108. user_id=user_id,
  109. event_ids=user_values["event_ids"],
  110. thread_id=thread_id,
  111. data=data,
  112. )
  113. )
  114. await self._handle_new_receipts(receipts)
  115. async def _handle_new_receipts(self, receipts: List[ReadReceipt]) -> bool:
  116. """Takes a list of receipts, stores them and informs the notifier."""
  117. min_batch_id: Optional[int] = None
  118. max_batch_id: Optional[int] = None
  119. for receipt in receipts:
  120. res = await self.store.insert_receipt(
  121. receipt.room_id,
  122. receipt.receipt_type,
  123. receipt.user_id,
  124. receipt.event_ids,
  125. receipt.thread_id,
  126. receipt.data,
  127. )
  128. if not res:
  129. # res will be None if this receipt is 'old'
  130. continue
  131. stream_id, max_persisted_id = res
  132. if min_batch_id is None or stream_id < min_batch_id:
  133. min_batch_id = stream_id
  134. if max_batch_id is None or max_persisted_id > max_batch_id:
  135. max_batch_id = max_persisted_id
  136. # Either both of these should be None or neither.
  137. if min_batch_id is None or max_batch_id is None:
  138. # no new receipts
  139. return False
  140. affected_room_ids = list({r.room_id for r in receipts})
  141. self.notifier.on_new_event(
  142. StreamKeyType.RECEIPT, max_batch_id, rooms=affected_room_ids
  143. )
  144. # Note that the min here shouldn't be relied upon to be accurate.
  145. await self.hs.get_pusherpool().on_new_receipts(
  146. min_batch_id, max_batch_id, affected_room_ids
  147. )
  148. return True
  149. async def received_client_receipt(
  150. self,
  151. room_id: str,
  152. receipt_type: str,
  153. user_id: UserID,
  154. event_id: str,
  155. thread_id: Optional[str],
  156. ) -> None:
  157. """Called when a client tells us a local user has read up to the given
  158. event_id in the room.
  159. """
  160. # Ensure the room/event exists, this will raise an error if the user
  161. # cannot view the event.
  162. if not await self.event_handler.get_event(user_id, room_id, event_id):
  163. return
  164. receipt = ReadReceipt(
  165. room_id=room_id,
  166. receipt_type=receipt_type,
  167. user_id=user_id.to_string(),
  168. event_ids=[event_id],
  169. thread_id=thread_id,
  170. data={"ts": int(self.clock.time_msec())},
  171. )
  172. is_new = await self._handle_new_receipts([receipt])
  173. if not is_new:
  174. return
  175. if self.federation_sender and receipt_type != ReceiptTypes.READ_PRIVATE:
  176. await self.federation_sender.send_read_receipt(receipt)
  177. class ReceiptEventSource(EventSource[int, JsonMapping]):
  178. def __init__(self, hs: "HomeServer"):
  179. self.store = hs.get_datastores().main
  180. self.config = hs.config
  181. @staticmethod
  182. def filter_out_private_receipts(
  183. rooms: Sequence[JsonMapping], user_id: str
  184. ) -> List[JsonMapping]:
  185. """
  186. Filters a list of serialized receipts (as returned by /sync and /initialSync)
  187. and removes private read receipts of other users.
  188. This operates on the return value of get_linearized_receipts_for_rooms(),
  189. which is wrapped in a cache. Care must be taken to ensure that the input
  190. values are not modified.
  191. Args:
  192. rooms: A list of mappings, each mapping has a `content` field, which
  193. is a map of event ID -> receipt type -> user ID -> receipt information.
  194. Returns:
  195. The same as rooms, but filtered.
  196. """
  197. result: List[JsonMapping] = []
  198. # Iterate through each room's receipt content.
  199. for room in rooms:
  200. # The receipt content with other user's private read receipts removed.
  201. content = {}
  202. # Iterate over each event ID / receipts for that event.
  203. for event_id, orig_event_content in room.get("content", {}).items():
  204. event_content = orig_event_content
  205. # If there are private read receipts, additional logic is necessary.
  206. if ReceiptTypes.READ_PRIVATE in event_content:
  207. # Make a copy without private read receipts to avoid leaking
  208. # other user's private read receipts..
  209. event_content = {
  210. receipt_type: receipt_value
  211. for receipt_type, receipt_value in event_content.items()
  212. if receipt_type != ReceiptTypes.READ_PRIVATE
  213. }
  214. # Copy the current user's private read receipt from the
  215. # original content, if it exists.
  216. user_private_read_receipt = orig_event_content[
  217. ReceiptTypes.READ_PRIVATE
  218. ].get(user_id, None)
  219. if user_private_read_receipt:
  220. event_content[ReceiptTypes.READ_PRIVATE] = {
  221. user_id: user_private_read_receipt
  222. }
  223. # Include the event if there is at least one non-private read
  224. # receipt or the current user has a private read receipt.
  225. if event_content:
  226. content[event_id] = event_content
  227. # Include the event if there is at least one non-private read receipt
  228. # or the current user has a private read receipt.
  229. if content:
  230. # Build a new event to avoid mutating the cache.
  231. new_room = {k: v for k, v in room.items() if k != "content"}
  232. new_room["content"] = content
  233. result.append(new_room)
  234. return result
  235. async def get_new_events(
  236. self,
  237. user: UserID,
  238. from_key: int,
  239. limit: int,
  240. room_ids: Iterable[str],
  241. is_guest: bool,
  242. explicit_room_id: Optional[str] = None,
  243. ) -> Tuple[List[JsonMapping], int]:
  244. from_key = int(from_key)
  245. to_key = self.get_current_key()
  246. if from_key == to_key:
  247. return [], to_key
  248. events = await self.store.get_linearized_receipts_for_rooms(
  249. room_ids, from_key=from_key, to_key=to_key
  250. )
  251. events = ReceiptEventSource.filter_out_private_receipts(
  252. events, user.to_string()
  253. )
  254. return events, to_key
  255. async def get_new_events_as(
  256. self, from_key: int, to_key: int, service: ApplicationService
  257. ) -> Tuple[List[JsonMapping], int]:
  258. """Returns a set of new read receipt events that an appservice
  259. may be interested in.
  260. Args:
  261. from_key: the stream position at which events should be fetched from
  262. to_key: the stream position up to which events should be fetched to
  263. service: The appservice which may be interested
  264. Returns:
  265. A two-tuple containing the following:
  266. * A list of json dictionaries derived from read receipts that the
  267. appservice may be interested in.
  268. * The current read receipt stream token.
  269. """
  270. from_key = int(from_key)
  271. if from_key == to_key:
  272. return [], to_key
  273. # Fetch all read receipts for all rooms, up to a limit of 100. This is ordered
  274. # by most recent.
  275. rooms_to_events = await self.store.get_linearized_receipts_for_all_rooms(
  276. from_key=from_key, to_key=to_key
  277. )
  278. # Then filter down to rooms that the AS can read
  279. events = []
  280. for room_id, event in rooms_to_events.items():
  281. if not await service.is_interested_in_room(room_id, self.store):
  282. continue
  283. events.append(event)
  284. return events, to_key
  285. def get_current_key(self) -> int:
  286. return self.store.get_max_receipt_stream_id()