room_list.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  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. import logging
  16. from collections import namedtuple
  17. from six import iteritems
  18. from six.moves import range
  19. import msgpack
  20. from unpaddedbase64 import decode_base64, encode_base64
  21. from twisted.internet import defer
  22. from synapse.api.constants import EventTypes, JoinRules
  23. from synapse.types import ThirdPartyInstanceID
  24. from synapse.util.async import concurrently_execute
  25. from synapse.util.caches.descriptors import cachedInlineCallbacks
  26. from synapse.util.caches.response_cache import ResponseCache
  27. from ._base import BaseHandler
  28. logger = logging.getLogger(__name__)
  29. REMOTE_ROOM_LIST_POLL_INTERVAL = 60 * 1000
  30. # This is used to indicate we should only return rooms published to the main list.
  31. EMPTY_THIRD_PARTY_ID = ThirdPartyInstanceID(None, None)
  32. class RoomListHandler(BaseHandler):
  33. def __init__(self, hs):
  34. super(RoomListHandler, self).__init__(hs)
  35. self.response_cache = ResponseCache(hs, "room_list")
  36. self.remote_response_cache = ResponseCache(hs, "remote_room_list",
  37. timeout_ms=30 * 1000)
  38. def get_local_public_room_list(self, limit=None, since_token=None,
  39. search_filter=None,
  40. network_tuple=EMPTY_THIRD_PARTY_ID,):
  41. """Generate a local public room list.
  42. There are multiple different lists: the main one plus one per third
  43. party network. A client can ask for a specific list or to return all.
  44. Args:
  45. limit (int)
  46. since_token (str)
  47. search_filter (dict)
  48. network_tuple (ThirdPartyInstanceID): Which public list to use.
  49. This can be (None, None) to indicate the main list, or a particular
  50. appservice and network id to use an appservice specific one.
  51. Setting to None returns all public rooms across all lists.
  52. """
  53. logger.info(
  54. "Getting public room list: limit=%r, since=%r, search=%r, network=%r",
  55. limit, since_token, bool(search_filter), network_tuple,
  56. )
  57. if search_filter:
  58. # We explicitly don't bother caching searches or requests for
  59. # appservice specific lists.
  60. logger.info("Bypassing cache as search request.")
  61. return self._get_public_room_list(
  62. limit, since_token, search_filter, network_tuple=network_tuple,
  63. )
  64. key = (limit, since_token, network_tuple)
  65. return self.response_cache.wrap(
  66. key,
  67. self._get_public_room_list,
  68. limit, since_token, network_tuple=network_tuple,
  69. )
  70. @defer.inlineCallbacks
  71. def _get_public_room_list(self, limit=None, since_token=None,
  72. search_filter=None,
  73. network_tuple=EMPTY_THIRD_PARTY_ID,):
  74. if since_token and since_token != "END":
  75. since_token = RoomListNextBatch.from_token(since_token)
  76. else:
  77. since_token = None
  78. rooms_to_order_value = {}
  79. rooms_to_num_joined = {}
  80. newly_visible = []
  81. newly_unpublished = []
  82. if since_token:
  83. stream_token = since_token.stream_ordering
  84. current_public_id = yield self.store.get_current_public_room_stream_id()
  85. public_room_stream_id = since_token.public_room_stream_id
  86. newly_visible, newly_unpublished = yield self.store.get_public_room_changes(
  87. public_room_stream_id, current_public_id,
  88. network_tuple=network_tuple,
  89. )
  90. else:
  91. stream_token = yield self.store.get_room_max_stream_ordering()
  92. public_room_stream_id = yield self.store.get_current_public_room_stream_id()
  93. room_ids = yield self.store.get_public_room_ids_at_stream_id(
  94. public_room_stream_id, network_tuple=network_tuple,
  95. )
  96. # We want to return rooms in a particular order: the number of joined
  97. # users. We then arbitrarily use the room_id as a tie breaker.
  98. @defer.inlineCallbacks
  99. def get_order_for_room(room_id):
  100. # Most of the rooms won't have changed between the since token and
  101. # now (especially if the since token is "now"). So, we can ask what
  102. # the current users are in a room (that will hit a cache) and then
  103. # check if the room has changed since the since token. (We have to
  104. # do it in that order to avoid races).
  105. # If things have changed then fall back to getting the current state
  106. # at the since token.
  107. joined_users = yield self.store.get_users_in_room(room_id)
  108. if self.store.has_room_changed_since(room_id, stream_token):
  109. latest_event_ids = yield self.store.get_forward_extremeties_for_room(
  110. room_id, stream_token
  111. )
  112. if not latest_event_ids:
  113. return
  114. joined_users = yield self.state_handler.get_current_user_in_room(
  115. room_id, latest_event_ids,
  116. )
  117. num_joined_users = len(joined_users)
  118. rooms_to_num_joined[room_id] = num_joined_users
  119. if num_joined_users == 0:
  120. return
  121. # We want larger rooms to be first, hence negating num_joined_users
  122. rooms_to_order_value[room_id] = (-num_joined_users, room_id)
  123. logger.info("Getting ordering for %i rooms since %s",
  124. len(room_ids), stream_token)
  125. yield concurrently_execute(get_order_for_room, room_ids, 10)
  126. sorted_entries = sorted(rooms_to_order_value.items(), key=lambda e: e[1])
  127. sorted_rooms = [room_id for room_id, _ in sorted_entries]
  128. # `sorted_rooms` should now be a list of all public room ids that is
  129. # stable across pagination. Therefore, we can use indices into this
  130. # list as our pagination tokens.
  131. # Filter out rooms that we don't want to return
  132. rooms_to_scan = [
  133. r for r in sorted_rooms
  134. if r not in newly_unpublished and rooms_to_num_joined[room_id] > 0
  135. ]
  136. total_room_count = len(rooms_to_scan)
  137. if since_token:
  138. # Filter out rooms we've already returned previously
  139. # `since_token.current_limit` is the index of the last room we
  140. # sent down, so we exclude it and everything before/after it.
  141. if since_token.direction_is_forward:
  142. rooms_to_scan = rooms_to_scan[since_token.current_limit + 1:]
  143. else:
  144. rooms_to_scan = rooms_to_scan[:since_token.current_limit]
  145. rooms_to_scan.reverse()
  146. logger.info("After sorting and filtering, %i rooms remain",
  147. len(rooms_to_scan))
  148. # _append_room_entry_to_chunk will append to chunk but will stop if
  149. # len(chunk) > limit
  150. #
  151. # Normally we will generate enough results on the first iteration here,
  152. # but if there is a search filter, _append_room_entry_to_chunk may
  153. # filter some results out, in which case we loop again.
  154. #
  155. # We don't want to scan over the entire range either as that
  156. # would potentially waste a lot of work.
  157. #
  158. # XXX if there is no limit, we may end up DoSing the server with
  159. # calls to get_current_state_ids for every single room on the
  160. # server. Surely we should cap this somehow?
  161. #
  162. if limit:
  163. step = limit + 1
  164. else:
  165. # step cannot be zero
  166. step = len(rooms_to_scan) if len(rooms_to_scan) != 0 else 1
  167. chunk = []
  168. for i in range(0, len(rooms_to_scan), step):
  169. batch = rooms_to_scan[i:i + step]
  170. logger.info("Processing %i rooms for result", len(batch))
  171. yield concurrently_execute(
  172. lambda r: self._append_room_entry_to_chunk(
  173. r, rooms_to_num_joined[r],
  174. chunk, limit, search_filter
  175. ),
  176. batch, 5,
  177. )
  178. logger.info("Now %i rooms in result", len(chunk))
  179. if len(chunk) >= limit + 1:
  180. break
  181. chunk.sort(key=lambda e: (-e["num_joined_members"], e["room_id"]))
  182. # Work out the new limit of the batch for pagination, or None if we
  183. # know there are no more results that would be returned.
  184. # i.e., [since_token.current_limit..new_limit] is the batch of rooms
  185. # we've returned (or the reverse if we paginated backwards)
  186. # We tried to pull out limit + 1 rooms above, so if we have <= limit
  187. # then we know there are no more results to return
  188. new_limit = None
  189. if chunk and (not limit or len(chunk) > limit):
  190. if not since_token or since_token.direction_is_forward:
  191. if limit:
  192. chunk = chunk[:limit]
  193. last_room_id = chunk[-1]["room_id"]
  194. else:
  195. if limit:
  196. chunk = chunk[-limit:]
  197. last_room_id = chunk[0]["room_id"]
  198. new_limit = sorted_rooms.index(last_room_id)
  199. results = {
  200. "chunk": chunk,
  201. "total_room_count_estimate": total_room_count,
  202. }
  203. if since_token:
  204. results["new_rooms"] = bool(newly_visible)
  205. if not since_token or since_token.direction_is_forward:
  206. if new_limit is not None:
  207. results["next_batch"] = RoomListNextBatch(
  208. stream_ordering=stream_token,
  209. public_room_stream_id=public_room_stream_id,
  210. current_limit=new_limit,
  211. direction_is_forward=True,
  212. ).to_token()
  213. if since_token:
  214. results["prev_batch"] = since_token.copy_and_replace(
  215. direction_is_forward=False,
  216. current_limit=since_token.current_limit + 1,
  217. ).to_token()
  218. else:
  219. if new_limit is not None:
  220. results["prev_batch"] = RoomListNextBatch(
  221. stream_ordering=stream_token,
  222. public_room_stream_id=public_room_stream_id,
  223. current_limit=new_limit,
  224. direction_is_forward=False,
  225. ).to_token()
  226. if since_token:
  227. results["next_batch"] = since_token.copy_and_replace(
  228. direction_is_forward=True,
  229. current_limit=since_token.current_limit - 1,
  230. ).to_token()
  231. defer.returnValue(results)
  232. @defer.inlineCallbacks
  233. def _append_room_entry_to_chunk(self, room_id, num_joined_users, chunk, limit,
  234. search_filter):
  235. """Generate the entry for a room in the public room list and append it
  236. to the `chunk` if it matches the search filter
  237. """
  238. if limit and len(chunk) > limit + 1:
  239. # We've already got enough, so lets just drop it.
  240. return
  241. result = yield self.generate_room_entry(room_id, num_joined_users)
  242. if result and _matches_room_entry(result, search_filter):
  243. chunk.append(result)
  244. @cachedInlineCallbacks(num_args=1, cache_context=True)
  245. def generate_room_entry(self, room_id, num_joined_users, cache_context,
  246. with_alias=True, allow_private=False):
  247. """Returns the entry for a room
  248. """
  249. result = {
  250. "room_id": room_id,
  251. "num_joined_members": num_joined_users,
  252. }
  253. current_state_ids = yield self.store.get_current_state_ids(
  254. room_id, on_invalidate=cache_context.invalidate,
  255. )
  256. event_map = yield self.store.get_events([
  257. event_id for key, event_id in iteritems(current_state_ids)
  258. if key[0] in (
  259. EventTypes.JoinRules,
  260. EventTypes.Name,
  261. EventTypes.Topic,
  262. EventTypes.CanonicalAlias,
  263. EventTypes.RoomHistoryVisibility,
  264. EventTypes.GuestAccess,
  265. "m.room.avatar",
  266. )
  267. ])
  268. current_state = {
  269. (ev.type, ev.state_key): ev
  270. for ev in event_map.values()
  271. }
  272. # Double check that this is actually a public room.
  273. join_rules_event = current_state.get((EventTypes.JoinRules, ""))
  274. if join_rules_event:
  275. join_rule = join_rules_event.content.get("join_rule", None)
  276. if not allow_private and join_rule and join_rule != JoinRules.PUBLIC:
  277. defer.returnValue(None)
  278. if with_alias:
  279. aliases = yield self.store.get_aliases_for_room(
  280. room_id, on_invalidate=cache_context.invalidate
  281. )
  282. if aliases:
  283. result["aliases"] = aliases
  284. name_event = yield current_state.get((EventTypes.Name, ""))
  285. if name_event:
  286. name = name_event.content.get("name", None)
  287. if name:
  288. result["name"] = name
  289. topic_event = current_state.get((EventTypes.Topic, ""))
  290. if topic_event:
  291. topic = topic_event.content.get("topic", None)
  292. if topic:
  293. result["topic"] = topic
  294. canonical_event = current_state.get((EventTypes.CanonicalAlias, ""))
  295. if canonical_event:
  296. canonical_alias = canonical_event.content.get("alias", None)
  297. if canonical_alias:
  298. result["canonical_alias"] = canonical_alias
  299. visibility_event = current_state.get((EventTypes.RoomHistoryVisibility, ""))
  300. visibility = None
  301. if visibility_event:
  302. visibility = visibility_event.content.get("history_visibility", None)
  303. result["world_readable"] = visibility == "world_readable"
  304. guest_event = current_state.get((EventTypes.GuestAccess, ""))
  305. guest = None
  306. if guest_event:
  307. guest = guest_event.content.get("guest_access", None)
  308. result["guest_can_join"] = guest == "can_join"
  309. avatar_event = current_state.get(("m.room.avatar", ""))
  310. if avatar_event:
  311. avatar_url = avatar_event.content.get("url", None)
  312. if avatar_url:
  313. result["avatar_url"] = avatar_url
  314. defer.returnValue(result)
  315. @defer.inlineCallbacks
  316. def get_remote_public_room_list(self, server_name, limit=None, since_token=None,
  317. search_filter=None, include_all_networks=False,
  318. third_party_instance_id=None,):
  319. if search_filter:
  320. # We currently don't support searching across federation, so we have
  321. # to do it manually without pagination
  322. limit = None
  323. since_token = None
  324. res = yield self._get_remote_list_cached(
  325. server_name, limit=limit, since_token=since_token,
  326. include_all_networks=include_all_networks,
  327. third_party_instance_id=third_party_instance_id,
  328. )
  329. if search_filter:
  330. res = {"chunk": [
  331. entry
  332. for entry in list(res.get("chunk", []))
  333. if _matches_room_entry(entry, search_filter)
  334. ]}
  335. defer.returnValue(res)
  336. def _get_remote_list_cached(self, server_name, limit=None, since_token=None,
  337. search_filter=None, include_all_networks=False,
  338. third_party_instance_id=None,):
  339. repl_layer = self.hs.get_federation_client()
  340. if search_filter:
  341. # We can't cache when asking for search
  342. return repl_layer.get_public_rooms(
  343. server_name, limit=limit, since_token=since_token,
  344. search_filter=search_filter, include_all_networks=include_all_networks,
  345. third_party_instance_id=third_party_instance_id,
  346. )
  347. key = (
  348. server_name, limit, since_token, include_all_networks,
  349. third_party_instance_id,
  350. )
  351. return self.remote_response_cache.wrap(
  352. key,
  353. repl_layer.get_public_rooms,
  354. server_name, limit=limit, since_token=since_token,
  355. search_filter=search_filter,
  356. include_all_networks=include_all_networks,
  357. third_party_instance_id=third_party_instance_id,
  358. )
  359. class RoomListNextBatch(namedtuple("RoomListNextBatch", (
  360. "stream_ordering", # stream_ordering of the first public room list
  361. "public_room_stream_id", # public room stream id for first public room list
  362. "current_limit", # The number of previous rooms returned
  363. "direction_is_forward", # Bool if this is a next_batch, false if prev_batch
  364. ))):
  365. KEY_DICT = {
  366. "stream_ordering": "s",
  367. "public_room_stream_id": "p",
  368. "current_limit": "n",
  369. "direction_is_forward": "d",
  370. }
  371. REVERSE_KEY_DICT = {v: k for k, v in KEY_DICT.items()}
  372. @classmethod
  373. def from_token(cls, token):
  374. return RoomListNextBatch(**{
  375. cls.REVERSE_KEY_DICT[key]: val
  376. for key, val in msgpack.loads(decode_base64(token)).items()
  377. })
  378. def to_token(self):
  379. return encode_base64(msgpack.dumps({
  380. self.KEY_DICT[key]: val
  381. for key, val in self._asdict().items()
  382. }))
  383. def copy_and_replace(self, **kwds):
  384. return self._replace(
  385. **kwds
  386. )
  387. def _matches_room_entry(room_entry, search_filter):
  388. if search_filter and search_filter.get("generic_search_term", None):
  389. generic_search_term = search_filter["generic_search_term"].upper()
  390. if generic_search_term in room_entry.get("name", "").upper():
  391. return True
  392. elif generic_search_term in room_entry.get("topic", "").upper():
  393. return True
  394. elif generic_search_term in room_entry.get("canonical_alias", "").upper():
  395. return True
  396. else:
  397. return True
  398. return False