Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 
 
 
 

468 řádky
17 KiB

  1. # -*- coding: utf-8 -*-
  2. # Copyright 2015, 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 itertools
  16. import logging
  17. from typing import Iterable
  18. from unpaddedbase64 import decode_base64, encode_base64
  19. from synapse.api.constants import EventTypes, Membership
  20. from synapse.api.errors import NotFoundError, SynapseError
  21. from synapse.api.filtering import Filter
  22. from synapse.storage.state import StateFilter
  23. from synapse.visibility import filter_events_for_client
  24. from ._base import BaseHandler
  25. logger = logging.getLogger(__name__)
  26. class SearchHandler(BaseHandler):
  27. def __init__(self, hs):
  28. super(SearchHandler, self).__init__(hs)
  29. self._event_serializer = hs.get_event_client_serializer()
  30. self.storage = hs.get_storage()
  31. self.state_store = self.storage.state
  32. self.auth = hs.get_auth()
  33. async def get_old_rooms_from_upgraded_room(self, room_id: str) -> Iterable[str]:
  34. """Retrieves room IDs of old rooms in the history of an upgraded room.
  35. We do so by checking the m.room.create event of the room for a
  36. `predecessor` key. If it exists, we add the room ID to our return
  37. list and then check that room for a m.room.create event and so on
  38. until we can no longer find any more previous rooms.
  39. The full list of all found rooms in then returned.
  40. Args:
  41. room_id: id of the room to search through.
  42. Returns:
  43. Predecessor room ids
  44. """
  45. historical_room_ids = []
  46. # The initial room must have been known for us to get this far
  47. predecessor = await self.store.get_room_predecessor(room_id)
  48. while True:
  49. if not predecessor:
  50. # We have reached the end of the chain of predecessors
  51. break
  52. if not isinstance(predecessor.get("room_id"), str):
  53. # This predecessor object is malformed. Exit here
  54. break
  55. predecessor_room_id = predecessor["room_id"]
  56. # Don't add it to the list until we have checked that we are in the room
  57. try:
  58. next_predecessor_room = await self.store.get_room_predecessor(
  59. predecessor_room_id
  60. )
  61. except NotFoundError:
  62. # The predecessor is not a known room, so we are done here
  63. break
  64. historical_room_ids.append(predecessor_room_id)
  65. # And repeat
  66. predecessor = next_predecessor_room
  67. return historical_room_ids
  68. async def search(self, user, content, batch=None):
  69. """Performs a full text search for a user.
  70. Args:
  71. user (UserID)
  72. content (dict): Search parameters
  73. batch (str): The next_batch parameter. Used for pagination.
  74. Returns:
  75. dict to be returned to the client with results of search
  76. """
  77. if not self.hs.config.enable_search:
  78. raise SynapseError(400, "Search is disabled on this homeserver")
  79. batch_group = None
  80. batch_group_key = None
  81. batch_token = None
  82. if batch:
  83. try:
  84. b = decode_base64(batch).decode("ascii")
  85. batch_group, batch_group_key, batch_token = b.split("\n")
  86. assert batch_group is not None
  87. assert batch_group_key is not None
  88. assert batch_token is not None
  89. except Exception:
  90. raise SynapseError(400, "Invalid batch")
  91. logger.info(
  92. "Search batch properties: %r, %r, %r",
  93. batch_group,
  94. batch_group_key,
  95. batch_token,
  96. )
  97. logger.info("Search content: %s", content)
  98. try:
  99. room_cat = content["search_categories"]["room_events"]
  100. # The actual thing to query in FTS
  101. search_term = room_cat["search_term"]
  102. # Which "keys" to search over in FTS query
  103. keys = room_cat.get(
  104. "keys", ["content.body", "content.name", "content.topic"]
  105. )
  106. # Filter to apply to results
  107. filter_dict = room_cat.get("filter", {})
  108. # What to order results by (impacts whether pagination can be doen)
  109. order_by = room_cat.get("order_by", "rank")
  110. # Return the current state of the rooms?
  111. include_state = room_cat.get("include_state", False)
  112. # Include context around each event?
  113. event_context = room_cat.get("event_context", None)
  114. # Group results together? May allow clients to paginate within a
  115. # group
  116. group_by = room_cat.get("groupings", {}).get("group_by", {})
  117. group_keys = [g["key"] for g in group_by]
  118. if event_context is not None:
  119. before_limit = int(event_context.get("before_limit", 5))
  120. after_limit = int(event_context.get("after_limit", 5))
  121. # Return the historic display name and avatar for the senders
  122. # of the events?
  123. include_profile = bool(event_context.get("include_profile", False))
  124. except KeyError:
  125. raise SynapseError(400, "Invalid search query")
  126. if order_by not in ("rank", "recent"):
  127. raise SynapseError(400, "Invalid order by: %r" % (order_by,))
  128. if set(group_keys) - {"room_id", "sender"}:
  129. raise SynapseError(
  130. 400,
  131. "Invalid group by keys: %r"
  132. % (set(group_keys) - {"room_id", "sender"},),
  133. )
  134. search_filter = Filter(filter_dict)
  135. # TODO: Search through left rooms too
  136. rooms = await self.store.get_rooms_for_local_user_where_membership_is(
  137. user.to_string(),
  138. membership_list=[Membership.JOIN],
  139. # membership_list=[Membership.JOIN, Membership.LEAVE, Membership.Ban],
  140. )
  141. room_ids = {r.room_id for r in rooms}
  142. # If doing a subset of all rooms seearch, check if any of the rooms
  143. # are from an upgraded room, and search their contents as well
  144. if search_filter.rooms:
  145. historical_room_ids = []
  146. for room_id in search_filter.rooms:
  147. # Add any previous rooms to the search if they exist
  148. ids = await self.get_old_rooms_from_upgraded_room(room_id)
  149. historical_room_ids += ids
  150. # Prevent any historical events from being filtered
  151. search_filter = search_filter.with_room_ids(historical_room_ids)
  152. room_ids = search_filter.filter_rooms(room_ids)
  153. if batch_group == "room_id":
  154. room_ids.intersection_update({batch_group_key})
  155. if not room_ids:
  156. return {
  157. "search_categories": {
  158. "room_events": {"results": [], "count": 0, "highlights": []}
  159. }
  160. }
  161. rank_map = {} # event_id -> rank of event
  162. allowed_events = []
  163. room_groups = {} # Holds result of grouping by room, if applicable
  164. sender_group = {} # Holds result of grouping by sender, if applicable
  165. # Holds the next_batch for the entire result set if one of those exists
  166. global_next_batch = None
  167. highlights = set()
  168. count = None
  169. if order_by == "rank":
  170. search_result = await self.store.search_msgs(room_ids, search_term, keys)
  171. count = search_result["count"]
  172. if search_result["highlights"]:
  173. highlights.update(search_result["highlights"])
  174. results = search_result["results"]
  175. results_map = {r["event"].event_id: r for r in results}
  176. rank_map.update({r["event"].event_id: r["rank"] for r in results})
  177. filtered_events = search_filter.filter([r["event"] for r in results])
  178. events = await filter_events_for_client(
  179. self.storage, user.to_string(), filtered_events
  180. )
  181. events.sort(key=lambda e: -rank_map[e.event_id])
  182. allowed_events = events[: search_filter.limit()]
  183. for e in allowed_events:
  184. rm = room_groups.setdefault(
  185. e.room_id, {"results": [], "order": rank_map[e.event_id]}
  186. )
  187. rm["results"].append(e.event_id)
  188. s = sender_group.setdefault(
  189. e.sender, {"results": [], "order": rank_map[e.event_id]}
  190. )
  191. s["results"].append(e.event_id)
  192. elif order_by == "recent":
  193. room_events = []
  194. i = 0
  195. pagination_token = batch_token
  196. # We keep looping and we keep filtering until we reach the limit
  197. # or we run out of things.
  198. # But only go around 5 times since otherwise synapse will be sad.
  199. while len(room_events) < search_filter.limit() and i < 5:
  200. i += 1
  201. search_result = await self.store.search_rooms(
  202. room_ids,
  203. search_term,
  204. keys,
  205. search_filter.limit() * 2,
  206. pagination_token=pagination_token,
  207. )
  208. if search_result["highlights"]:
  209. highlights.update(search_result["highlights"])
  210. count = search_result["count"]
  211. results = search_result["results"]
  212. results_map = {r["event"].event_id: r for r in results}
  213. rank_map.update({r["event"].event_id: r["rank"] for r in results})
  214. filtered_events = search_filter.filter([r["event"] for r in results])
  215. events = await filter_events_for_client(
  216. self.storage, user.to_string(), filtered_events
  217. )
  218. room_events.extend(events)
  219. room_events = room_events[: search_filter.limit()]
  220. if len(results) < search_filter.limit() * 2:
  221. pagination_token = None
  222. break
  223. else:
  224. pagination_token = results[-1]["pagination_token"]
  225. for event in room_events:
  226. group = room_groups.setdefault(event.room_id, {"results": []})
  227. group["results"].append(event.event_id)
  228. if room_events and len(room_events) >= search_filter.limit():
  229. last_event_id = room_events[-1].event_id
  230. pagination_token = results_map[last_event_id]["pagination_token"]
  231. # We want to respect the given batch group and group keys so
  232. # that if people blindly use the top level `next_batch` token
  233. # it returns more from the same group (if applicable) rather
  234. # than reverting to searching all results again.
  235. if batch_group and batch_group_key:
  236. global_next_batch = encode_base64(
  237. (
  238. "%s\n%s\n%s"
  239. % (batch_group, batch_group_key, pagination_token)
  240. ).encode("ascii")
  241. )
  242. else:
  243. global_next_batch = encode_base64(
  244. ("%s\n%s\n%s" % ("all", "", pagination_token)).encode("ascii")
  245. )
  246. for room_id, group in room_groups.items():
  247. group["next_batch"] = encode_base64(
  248. ("%s\n%s\n%s" % ("room_id", room_id, pagination_token)).encode(
  249. "ascii"
  250. )
  251. )
  252. allowed_events.extend(room_events)
  253. else:
  254. # We should never get here due to the guard earlier.
  255. raise NotImplementedError()
  256. logger.info("Found %d events to return", len(allowed_events))
  257. # If client has asked for "context" for each event (i.e. some surrounding
  258. # events and state), fetch that
  259. if event_context is not None:
  260. now_token = await self.hs.get_event_sources().get_current_token()
  261. contexts = {}
  262. for event in allowed_events:
  263. res = await self.store.get_events_around(
  264. event.room_id, event.event_id, before_limit, after_limit
  265. )
  266. logger.info(
  267. "Context for search returned %d and %d events",
  268. len(res["events_before"]),
  269. len(res["events_after"]),
  270. )
  271. res["events_before"] = await filter_events_for_client(
  272. self.storage, user.to_string(), res["events_before"]
  273. )
  274. res["events_after"] = await filter_events_for_client(
  275. self.storage, user.to_string(), res["events_after"]
  276. )
  277. res["start"] = now_token.copy_and_replace(
  278. "room_key", res["start"]
  279. ).to_string()
  280. res["end"] = now_token.copy_and_replace(
  281. "room_key", res["end"]
  282. ).to_string()
  283. if include_profile:
  284. senders = {
  285. ev.sender
  286. for ev in itertools.chain(
  287. res["events_before"], [event], res["events_after"]
  288. )
  289. }
  290. if res["events_after"]:
  291. last_event_id = res["events_after"][-1].event_id
  292. else:
  293. last_event_id = event.event_id
  294. state_filter = StateFilter.from_types(
  295. [(EventTypes.Member, sender) for sender in senders]
  296. )
  297. state = await self.state_store.get_state_for_event(
  298. last_event_id, state_filter
  299. )
  300. res["profile_info"] = {
  301. s.state_key: {
  302. "displayname": s.content.get("displayname", None),
  303. "avatar_url": s.content.get("avatar_url", None),
  304. }
  305. for s in state.values()
  306. if s.type == EventTypes.Member and s.state_key in senders
  307. }
  308. contexts[event.event_id] = res
  309. else:
  310. contexts = {}
  311. # TODO: Add a limit
  312. time_now = self.clock.time_msec()
  313. for context in contexts.values():
  314. context["events_before"] = await self._event_serializer.serialize_events(
  315. context["events_before"], time_now
  316. )
  317. context["events_after"] = await self._event_serializer.serialize_events(
  318. context["events_after"], time_now
  319. )
  320. state_results = {}
  321. if include_state:
  322. rooms = {e.room_id for e in allowed_events}
  323. for room_id in rooms:
  324. state = await self.state_handler.get_current_state(room_id)
  325. state_results[room_id] = list(state.values())
  326. state_results.values()
  327. # We're now about to serialize the events. We should not make any
  328. # blocking calls after this. Otherwise the 'age' will be wrong
  329. results = []
  330. for e in allowed_events:
  331. results.append(
  332. {
  333. "rank": rank_map[e.event_id],
  334. "result": (
  335. await self._event_serializer.serialize_event(e, time_now)
  336. ),
  337. "context": contexts.get(e.event_id, {}),
  338. }
  339. )
  340. rooms_cat_res = {
  341. "results": results,
  342. "count": count,
  343. "highlights": list(highlights),
  344. }
  345. if state_results:
  346. s = {}
  347. for room_id, state in state_results.items():
  348. s[room_id] = await self._event_serializer.serialize_events(
  349. state, time_now
  350. )
  351. rooms_cat_res["state"] = s
  352. if room_groups and "room_id" in group_keys:
  353. rooms_cat_res.setdefault("groups", {})["room_id"] = room_groups
  354. if sender_group and "sender" in group_keys:
  355. rooms_cat_res.setdefault("groups", {})["sender"] = sender_group
  356. if global_next_batch:
  357. rooms_cat_res["next_batch"] = global_next_batch
  358. return {"search_categories": {"room_events": rooms_cat_res}}