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.
 
 
 
 
 
 

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