typing.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2014-2016 OpenMarket 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. from twisted.internet import defer
  16. from ._base import BaseHandler
  17. from synapse.api.errors import SynapseError, AuthError
  18. from synapse.util.logcontext import PreserveLoggingContext
  19. from synapse.util.metrics import Measure
  20. from synapse.types import UserID
  21. import logging
  22. from collections import namedtuple
  23. import ujson as json
  24. logger = logging.getLogger(__name__)
  25. # A tiny object useful for storing a user's membership in a room, as a mapping
  26. # key
  27. RoomMember = namedtuple("RoomMember", ("room_id", "user"))
  28. class TypingNotificationHandler(BaseHandler):
  29. def __init__(self, hs):
  30. super(TypingNotificationHandler, self).__init__(hs)
  31. self.homeserver = hs
  32. self.clock = hs.get_clock()
  33. self.federation = hs.get_replication_layer()
  34. self.federation.register_edu_handler("m.typing", self._recv_edu)
  35. hs.get_distributor().observe("user_left_room", self.user_left_room)
  36. self._member_typing_until = {} # clock time we expect to stop
  37. self._member_typing_timer = {} # deferreds to manage theabove
  38. # map room IDs to serial numbers
  39. self._room_serials = {}
  40. self._latest_room_serial = 0
  41. # map room IDs to sets of users currently typing
  42. self._room_typing = {}
  43. def tearDown(self):
  44. """Cancels all the pending timers.
  45. Normally this shouldn't be needed, but it's required from unit tests
  46. to avoid a "Reactor was unclean" warning."""
  47. for t in self._member_typing_timer.values():
  48. self.clock.cancel_call_later(t)
  49. @defer.inlineCallbacks
  50. def started_typing(self, target_user, auth_user, room_id, timeout):
  51. if not self.hs.is_mine(target_user):
  52. raise SynapseError(400, "User is not hosted on this Home Server")
  53. if target_user != auth_user:
  54. raise AuthError(400, "Cannot set another user's typing state")
  55. yield self.auth.check_joined_room(room_id, target_user.to_string())
  56. logger.debug(
  57. "%s has started typing in %s", target_user.to_string(), room_id
  58. )
  59. until = self.clock.time_msec() + timeout
  60. member = RoomMember(room_id=room_id, user=target_user)
  61. was_present = member in self._member_typing_until
  62. if member in self._member_typing_timer:
  63. self.clock.cancel_call_later(self._member_typing_timer[member])
  64. def _cb():
  65. logger.debug(
  66. "%s has timed out in %s", target_user.to_string(), room_id
  67. )
  68. self._stopped_typing(member)
  69. self._member_typing_until[member] = until
  70. self._member_typing_timer[member] = self.clock.call_later(
  71. timeout / 1000.0, _cb
  72. )
  73. if was_present:
  74. # No point sending another notification
  75. defer.returnValue(None)
  76. yield self._push_update(
  77. room_id=room_id,
  78. user=target_user,
  79. typing=True,
  80. )
  81. @defer.inlineCallbacks
  82. def stopped_typing(self, target_user, auth_user, room_id):
  83. if not self.hs.is_mine(target_user):
  84. raise SynapseError(400, "User is not hosted on this Home Server")
  85. if target_user != auth_user:
  86. raise AuthError(400, "Cannot set another user's typing state")
  87. yield self.auth.check_joined_room(room_id, target_user.to_string())
  88. logger.debug(
  89. "%s has stopped typing in %s", target_user.to_string(), room_id
  90. )
  91. member = RoomMember(room_id=room_id, user=target_user)
  92. if member in self._member_typing_timer:
  93. self.clock.cancel_call_later(self._member_typing_timer[member])
  94. del self._member_typing_timer[member]
  95. yield self._stopped_typing(member)
  96. @defer.inlineCallbacks
  97. def user_left_room(self, user, room_id):
  98. if self.hs.is_mine(user):
  99. member = RoomMember(room_id=room_id, user=user)
  100. yield self._stopped_typing(member)
  101. @defer.inlineCallbacks
  102. def _stopped_typing(self, member):
  103. if member not in self._member_typing_until:
  104. # No point
  105. defer.returnValue(None)
  106. yield self._push_update(
  107. room_id=member.room_id,
  108. user=member.user,
  109. typing=False,
  110. )
  111. del self._member_typing_until[member]
  112. if member in self._member_typing_timer:
  113. # Don't cancel it - either it already expired, or the real
  114. # stopped_typing() will cancel it
  115. del self._member_typing_timer[member]
  116. @defer.inlineCallbacks
  117. def _push_update(self, room_id, user, typing):
  118. localusers = set()
  119. remotedomains = set()
  120. rm_handler = self.homeserver.get_handlers().room_member_handler
  121. yield rm_handler.fetch_room_distributions_into(
  122. room_id, localusers=localusers, remotedomains=remotedomains
  123. )
  124. if localusers:
  125. self._push_update_local(
  126. room_id=room_id,
  127. user=user,
  128. typing=typing
  129. )
  130. deferreds = []
  131. for domain in remotedomains:
  132. deferreds.append(self.federation.send_edu(
  133. destination=domain,
  134. edu_type="m.typing",
  135. content={
  136. "room_id": room_id,
  137. "user_id": user.to_string(),
  138. "typing": typing,
  139. },
  140. ))
  141. yield defer.DeferredList(deferreds, consumeErrors=True)
  142. @defer.inlineCallbacks
  143. def _recv_edu(self, origin, content):
  144. room_id = content["room_id"]
  145. user = UserID.from_string(content["user_id"])
  146. localusers = set()
  147. rm_handler = self.homeserver.get_handlers().room_member_handler
  148. yield rm_handler.fetch_room_distributions_into(
  149. room_id, localusers=localusers
  150. )
  151. if localusers:
  152. self._push_update_local(
  153. room_id=room_id,
  154. user=user,
  155. typing=content["typing"]
  156. )
  157. def _push_update_local(self, room_id, user, typing):
  158. room_set = self._room_typing.setdefault(room_id, set())
  159. if typing:
  160. room_set.add(user)
  161. else:
  162. room_set.discard(user)
  163. self._latest_room_serial += 1
  164. self._room_serials[room_id] = self._latest_room_serial
  165. with PreserveLoggingContext():
  166. self.notifier.on_new_event(
  167. "typing_key", self._latest_room_serial, rooms=[room_id]
  168. )
  169. def get_all_typing_updates(self, last_id, current_id):
  170. # TODO: Work out a way to do this without scanning the entire state.
  171. rows = []
  172. for room_id, serial in self._room_serials.items():
  173. if last_id < serial and serial <= current_id:
  174. typing = self._room_typing[room_id]
  175. typing_bytes = json.dumps([
  176. u.to_string() for u in typing
  177. ], ensure_ascii=False)
  178. rows.append((serial, room_id, typing_bytes))
  179. rows.sort()
  180. return rows
  181. class TypingNotificationEventSource(object):
  182. def __init__(self, hs):
  183. self.hs = hs
  184. self.clock = hs.get_clock()
  185. self._handler = None
  186. self._room_member_handler = None
  187. def handler(self):
  188. # Avoid cyclic dependency in handler setup
  189. if not self._handler:
  190. self._handler = self.hs.get_handlers().typing_notification_handler
  191. return self._handler
  192. def room_member_handler(self):
  193. if not self._room_member_handler:
  194. self._room_member_handler = self.hs.get_handlers().room_member_handler
  195. return self._room_member_handler
  196. def _make_event_for(self, room_id):
  197. typing = self.handler()._room_typing[room_id]
  198. return {
  199. "type": "m.typing",
  200. "room_id": room_id,
  201. "content": {
  202. "user_ids": [u.to_string() for u in typing],
  203. },
  204. }
  205. def get_new_events(self, from_key, room_ids, **kwargs):
  206. with Measure(self.clock, "typing.get_new_events"):
  207. from_key = int(from_key)
  208. handler = self.handler()
  209. events = []
  210. for room_id in room_ids:
  211. if room_id not in handler._room_serials:
  212. continue
  213. if handler._room_serials[room_id] <= from_key:
  214. continue
  215. events.append(self._make_event_for(room_id))
  216. return events, handler._latest_room_serial
  217. def get_current_key(self):
  218. return self.handler()._latest_room_serial
  219. def get_pagination_rows(self, user, pagination_config, key):
  220. return ([], pagination_config.from_key)