464 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			464 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Python
		
	
	
| import logging
 | |
| from typing import TYPE_CHECKING, List, Tuple
 | |
| 
 | |
| from synapse.api.constants import EventContentFields, EventTypes
 | |
| from synapse.appservice import ApplicationService
 | |
| from synapse.http.servlet import assert_params_in_dict
 | |
| from synapse.types import JsonDict, Requester, UserID, create_requester
 | |
| from synapse.util.stringutils import random_string
 | |
| 
 | |
| if TYPE_CHECKING:
 | |
|     from synapse.server import HomeServer
 | |
| 
 | |
| logger = logging.getLogger(__name__)
 | |
| 
 | |
| 
 | |
| class RoomBatchHandler:
 | |
|     def __init__(self, hs: "HomeServer"):
 | |
|         self.hs = hs
 | |
|         self.store = hs.get_datastores().main
 | |
|         self.state_store = hs.get_storage().state
 | |
|         self.event_creation_handler = hs.get_event_creation_handler()
 | |
|         self.room_member_handler = hs.get_room_member_handler()
 | |
|         self.auth = hs.get_auth()
 | |
| 
 | |
|     async def inherit_depth_from_prev_ids(self, prev_event_ids: List[str]) -> int:
 | |
|         """Finds the depth which would sort it after the most-recent
 | |
|         prev_event_id but before the successors of those events. If no
 | |
|         successors are found, we assume it's an historical extremity part of the
 | |
|         current batch and use the same depth of the prev_event_ids.
 | |
| 
 | |
|         Args:
 | |
|             prev_event_ids: List of prev event IDs
 | |
| 
 | |
|         Returns:
 | |
|             Inherited depth
 | |
|         """
 | |
|         (
 | |
|             most_recent_prev_event_id,
 | |
|             most_recent_prev_event_depth,
 | |
|         ) = await self.store.get_max_depth_of(prev_event_ids)
 | |
| 
 | |
|         # We want to insert the historical event after the `prev_event` but before the successor event
 | |
|         #
 | |
|         # We inherit depth from the successor event instead of the `prev_event`
 | |
|         # because events returned from `/messages` are first sorted by `topological_ordering`
 | |
|         # which is just the `depth` and then tie-break with `stream_ordering`.
 | |
|         #
 | |
|         # We mark these inserted historical events as "backfilled" which gives them a
 | |
|         # negative `stream_ordering`. If we use the same depth as the `prev_event`,
 | |
|         # then our historical event will tie-break and be sorted before the `prev_event`
 | |
|         # when it should come after.
 | |
|         #
 | |
|         # We want to use the successor event depth so they appear after `prev_event` because
 | |
|         # it has a larger `depth` but before the successor event because the `stream_ordering`
 | |
|         # is negative before the successor event.
 | |
|         successor_event_ids = await self.store.get_successor_events(
 | |
|             [most_recent_prev_event_id]
 | |
|         )
 | |
| 
 | |
|         # If we can't find any successor events, then it's a forward extremity of
 | |
|         # historical messages and we can just inherit from the previous historical
 | |
|         # event which we can already assume has the correct depth where we want
 | |
|         # to insert into.
 | |
|         if not successor_event_ids:
 | |
|             depth = most_recent_prev_event_depth
 | |
|         else:
 | |
|             (
 | |
|                 _,
 | |
|                 oldest_successor_depth,
 | |
|             ) = await self.store.get_min_depth_of(successor_event_ids)
 | |
| 
 | |
|             depth = oldest_successor_depth
 | |
| 
 | |
|         return depth
 | |
| 
 | |
|     def create_insertion_event_dict(
 | |
|         self, sender: str, room_id: str, origin_server_ts: int
 | |
|     ) -> JsonDict:
 | |
|         """Creates an event dict for an "insertion" event with the proper fields
 | |
|         and a random batch ID.
 | |
| 
 | |
|         Args:
 | |
|             sender: The event author MXID
 | |
|             room_id: The room ID that the event belongs to
 | |
|             origin_server_ts: Timestamp when the event was sent
 | |
| 
 | |
|         Returns:
 | |
|             The new event dictionary to insert.
 | |
|         """
 | |
| 
 | |
|         next_batch_id = random_string(8)
 | |
|         insertion_event = {
 | |
|             "type": EventTypes.MSC2716_INSERTION,
 | |
|             "sender": sender,
 | |
|             "room_id": room_id,
 | |
|             "content": {
 | |
|                 EventContentFields.MSC2716_NEXT_BATCH_ID: next_batch_id,
 | |
|                 EventContentFields.MSC2716_HISTORICAL: True,
 | |
|             },
 | |
|             "origin_server_ts": origin_server_ts,
 | |
|         }
 | |
| 
 | |
|         return insertion_event
 | |
| 
 | |
|     async def create_requester_for_user_id_from_app_service(
 | |
|         self, user_id: str, app_service: ApplicationService
 | |
|     ) -> Requester:
 | |
|         """Creates a new requester for the given user_id
 | |
|         and validates that the app service is allowed to control
 | |
|         the given user.
 | |
| 
 | |
|         Args:
 | |
|             user_id: The author MXID that the app service is controlling
 | |
|             app_service: The app service that controls the user
 | |
| 
 | |
|         Returns:
 | |
|             Requester object
 | |
|         """
 | |
| 
 | |
|         await self.auth.validate_appservice_can_control_user_id(app_service, user_id)
 | |
| 
 | |
|         return create_requester(user_id, app_service=app_service)
 | |
| 
 | |
|     async def get_most_recent_full_state_ids_from_event_id_list(
 | |
|         self, event_ids: List[str]
 | |
|     ) -> List[str]:
 | |
|         """Find the most recent event_id and grab the full state at that event.
 | |
|         We will use this as a base to auth our historical messages against.
 | |
| 
 | |
|         Args:
 | |
|             event_ids: List of event ID's to look at
 | |
| 
 | |
|         Returns:
 | |
|             List of event ID's
 | |
|         """
 | |
| 
 | |
|         (
 | |
|             most_recent_event_id,
 | |
|             _,
 | |
|         ) = await self.store.get_max_depth_of(event_ids)
 | |
|         # mapping from (type, state_key) -> state_event_id
 | |
|         prev_state_map = await self.state_store.get_state_ids_for_event(
 | |
|             most_recent_event_id
 | |
|         )
 | |
|         # List of state event ID's
 | |
|         full_state_ids = list(prev_state_map.values())
 | |
| 
 | |
|         return full_state_ids
 | |
| 
 | |
|     async def persist_state_events_at_start(
 | |
|         self,
 | |
|         state_events_at_start: List[JsonDict],
 | |
|         room_id: str,
 | |
|         initial_state_event_ids: List[str],
 | |
|         app_service_requester: Requester,
 | |
|     ) -> List[str]:
 | |
|         """Takes all `state_events_at_start` event dictionaries and creates/persists
 | |
|         them in a floating state event chain which don't resolve into the current room
 | |
|         state. They are floating because they reference no prev_events which disconnects
 | |
|         them from the normal DAG.
 | |
| 
 | |
|         Args:
 | |
|             state_events_at_start:
 | |
|             room_id: Room where you want the events persisted in.
 | |
|             initial_state_event_ids:
 | |
|                 The base set of state for the historical batch which the floating
 | |
|                 state chain will derive from. This should probably be the state
 | |
|                 from the `prev_event` defined by `/batch_send?prev_event_id=$abc`.
 | |
|             app_service_requester: The requester of an application service.
 | |
| 
 | |
|         Returns:
 | |
|             List of state event ID's we just persisted
 | |
|         """
 | |
|         assert app_service_requester.app_service
 | |
| 
 | |
|         state_event_ids_at_start = []
 | |
|         state_event_ids = initial_state_event_ids.copy()
 | |
| 
 | |
|         # Make the state events float off on their own by specifying no
 | |
|         # prev_events for the first one in the chain so we don't have a bunch of
 | |
|         # `@mxid joined the room` noise between each batch.
 | |
|         prev_event_ids_for_state_chain: List[str] = []
 | |
| 
 | |
|         for index, state_event in enumerate(state_events_at_start):
 | |
|             assert_params_in_dict(
 | |
|                 state_event, ["type", "origin_server_ts", "content", "sender"]
 | |
|             )
 | |
| 
 | |
|             logger.debug(
 | |
|                 "RoomBatchSendEventRestServlet inserting state_event=%s", state_event
 | |
|             )
 | |
| 
 | |
|             event_dict = {
 | |
|                 "type": state_event["type"],
 | |
|                 "origin_server_ts": state_event["origin_server_ts"],
 | |
|                 "content": state_event["content"],
 | |
|                 "room_id": room_id,
 | |
|                 "sender": state_event["sender"],
 | |
|                 "state_key": state_event["state_key"],
 | |
|             }
 | |
| 
 | |
|             # Mark all events as historical
 | |
|             event_dict["content"][EventContentFields.MSC2716_HISTORICAL] = True
 | |
| 
 | |
|             # TODO: This is pretty much the same as some other code to handle inserting state in this file
 | |
|             if event_dict["type"] == EventTypes.Member:
 | |
|                 membership = event_dict["content"].get("membership", None)
 | |
|                 event_id, _ = await self.room_member_handler.update_membership(
 | |
|                     await self.create_requester_for_user_id_from_app_service(
 | |
|                         state_event["sender"], app_service_requester.app_service
 | |
|                     ),
 | |
|                     target=UserID.from_string(event_dict["state_key"]),
 | |
|                     room_id=room_id,
 | |
|                     action=membership,
 | |
|                     content=event_dict["content"],
 | |
|                     historical=True,
 | |
|                     # Only the first event in the state chain should be floating.
 | |
|                     # The rest should hang off each other in a chain.
 | |
|                     allow_no_prev_events=index == 0,
 | |
|                     prev_event_ids=prev_event_ids_for_state_chain,
 | |
|                     # The first event in the state chain is floating with no
 | |
|                     # `prev_events` which means it can't derive state from
 | |
|                     # anywhere automatically. So we need to set some state
 | |
|                     # explicitly.
 | |
|                     #
 | |
|                     # Make sure to use a copy of this list because we modify it
 | |
|                     # later in the loop here. Otherwise it will be the same
 | |
|                     # reference and also update in the event when we append
 | |
|                     # later.
 | |
|                     state_event_ids=state_event_ids.copy(),
 | |
|                 )
 | |
|             else:
 | |
|                 (
 | |
|                     event,
 | |
|                     _,
 | |
|                 ) = await self.event_creation_handler.create_and_send_nonmember_event(
 | |
|                     await self.create_requester_for_user_id_from_app_service(
 | |
|                         state_event["sender"], app_service_requester.app_service
 | |
|                     ),
 | |
|                     event_dict,
 | |
|                     historical=True,
 | |
|                     # Only the first event in the state chain should be floating.
 | |
|                     # The rest should hang off each other in a chain.
 | |
|                     allow_no_prev_events=index == 0,
 | |
|                     prev_event_ids=prev_event_ids_for_state_chain,
 | |
|                     # The first event in the state chain is floating with no
 | |
|                     # `prev_events` which means it can't derive state from
 | |
|                     # anywhere automatically. So we need to set some state
 | |
|                     # explicitly.
 | |
|                     #
 | |
|                     # Make sure to use a copy of this list because we modify it
 | |
|                     # later in the loop here. Otherwise it will be the same
 | |
|                     # reference and also update in the event when we append later.
 | |
|                     state_event_ids=state_event_ids.copy(),
 | |
|                 )
 | |
|                 event_id = event.event_id
 | |
| 
 | |
|             state_event_ids_at_start.append(event_id)
 | |
|             state_event_ids.append(event_id)
 | |
|             # Connect all the state in a floating chain
 | |
|             prev_event_ids_for_state_chain = [event_id]
 | |
| 
 | |
|         return state_event_ids_at_start
 | |
| 
 | |
|     async def persist_historical_events(
 | |
|         self,
 | |
|         events_to_create: List[JsonDict],
 | |
|         room_id: str,
 | |
|         inherited_depth: int,
 | |
|         initial_state_event_ids: List[str],
 | |
|         app_service_requester: Requester,
 | |
|     ) -> List[str]:
 | |
|         """Create and persists all events provided sequentially. Handles the
 | |
|         complexity of creating events in chronological order so they can
 | |
|         reference each other by prev_event but still persists in
 | |
|         reverse-chronoloical order so they have the correct
 | |
|         (topological_ordering, stream_ordering) and sort correctly from
 | |
|         /messages.
 | |
| 
 | |
|         Args:
 | |
|             events_to_create: List of historical events to create in JSON
 | |
|                 dictionary format.
 | |
|             room_id: Room where you want the events persisted in.
 | |
|             inherited_depth: The depth to create the events at (you will
 | |
|                 probably by calling inherit_depth_from_prev_ids(...)).
 | |
|             initial_state_event_ids:
 | |
|                 This is used to set explicit state for the insertion event at
 | |
|                 the start of the historical batch since it's floating with no
 | |
|                 prev_events to derive state from automatically.
 | |
|             app_service_requester: The requester of an application service.
 | |
| 
 | |
|         Returns:
 | |
|             List of persisted event IDs
 | |
|         """
 | |
|         assert app_service_requester.app_service
 | |
| 
 | |
|         # We expect the first event in a historical batch to be an insertion event
 | |
|         assert events_to_create[0]["type"] == EventTypes.MSC2716_INSERTION
 | |
|         # We expect the last event in a historical batch to be an batch event
 | |
|         assert events_to_create[-1]["type"] == EventTypes.MSC2716_BATCH
 | |
| 
 | |
|         # Make the historical event chain float off on its own by specifying no
 | |
|         # prev_events for the first event in the chain which causes the HS to
 | |
|         # ask for the state at the start of the batch later.
 | |
|         prev_event_ids: List[str] = []
 | |
| 
 | |
|         event_ids = []
 | |
|         events_to_persist = []
 | |
|         for index, ev in enumerate(events_to_create):
 | |
|             assert_params_in_dict(ev, ["type", "origin_server_ts", "content", "sender"])
 | |
| 
 | |
|             assert self.hs.is_mine_id(ev["sender"]), "User must be our own: %s" % (
 | |
|                 ev["sender"],
 | |
|             )
 | |
| 
 | |
|             event_dict = {
 | |
|                 "type": ev["type"],
 | |
|                 "origin_server_ts": ev["origin_server_ts"],
 | |
|                 "content": ev["content"],
 | |
|                 "room_id": room_id,
 | |
|                 "sender": ev["sender"],  # requester.user.to_string(),
 | |
|                 "prev_events": prev_event_ids.copy(),
 | |
|             }
 | |
| 
 | |
|             # Mark all events as historical
 | |
|             event_dict["content"][EventContentFields.MSC2716_HISTORICAL] = True
 | |
| 
 | |
|             event, context = await self.event_creation_handler.create_event(
 | |
|                 await self.create_requester_for_user_id_from_app_service(
 | |
|                     ev["sender"], app_service_requester.app_service
 | |
|                 ),
 | |
|                 event_dict,
 | |
|                 # Only the first event (which is the insertion event) in the
 | |
|                 # chain should be floating. The rest should hang off each other
 | |
|                 # in a chain.
 | |
|                 allow_no_prev_events=index == 0,
 | |
|                 prev_event_ids=event_dict.get("prev_events"),
 | |
|                 # Since the first event (which is the insertion event) in the
 | |
|                 # chain is floating with no `prev_events`, it can't derive state
 | |
|                 # from anywhere automatically. So we need to set some state
 | |
|                 # explicitly.
 | |
|                 state_event_ids=initial_state_event_ids if index == 0 else None,
 | |
|                 historical=True,
 | |
|                 depth=inherited_depth,
 | |
|             )
 | |
| 
 | |
|             assert context._state_group
 | |
| 
 | |
|             # Normally this is done when persisting the event but we have to
 | |
|             # pre-emptively do it here because we create all the events first,
 | |
|             # then persist them in another pass below. And we want to share
 | |
|             # state_groups across the whole batch so this lookup needs to work
 | |
|             # for the next event in the batch in this loop.
 | |
|             await self.store.store_state_group_id_for_event_id(
 | |
|                 event_id=event.event_id,
 | |
|                 state_group_id=context._state_group,
 | |
|             )
 | |
| 
 | |
|             logger.debug(
 | |
|                 "RoomBatchSendEventRestServlet inserting event=%s, prev_event_ids=%s",
 | |
|                 event,
 | |
|                 prev_event_ids,
 | |
|             )
 | |
| 
 | |
|             events_to_persist.append((event, context))
 | |
|             event_id = event.event_id
 | |
| 
 | |
|             event_ids.append(event_id)
 | |
|             prev_event_ids = [event_id]
 | |
| 
 | |
|         # Persist events in reverse-chronological order so they have the
 | |
|         # correct stream_ordering as they are backfilled (which decrements).
 | |
|         # Events are sorted by (topological_ordering, stream_ordering)
 | |
|         # where topological_ordering is just depth.
 | |
|         for (event, context) in reversed(events_to_persist):
 | |
|             await self.event_creation_handler.handle_new_client_event(
 | |
|                 await self.create_requester_for_user_id_from_app_service(
 | |
|                     event.sender, app_service_requester.app_service
 | |
|                 ),
 | |
|                 event=event,
 | |
|                 context=context,
 | |
|             )
 | |
| 
 | |
|         return event_ids
 | |
| 
 | |
|     async def handle_batch_of_events(
 | |
|         self,
 | |
|         events_to_create: List[JsonDict],
 | |
|         room_id: str,
 | |
|         batch_id_to_connect_to: str,
 | |
|         inherited_depth: int,
 | |
|         initial_state_event_ids: List[str],
 | |
|         app_service_requester: Requester,
 | |
|     ) -> Tuple[List[str], str]:
 | |
|         """
 | |
|         Handles creating and persisting all of the historical events as well as
 | |
|         insertion and batch meta events to make the batch navigable in the DAG.
 | |
| 
 | |
|         Args:
 | |
|             events_to_create: List of historical events to create in JSON
 | |
|                 dictionary format.
 | |
|             room_id: Room where you want the events created in.
 | |
|             batch_id_to_connect_to: The batch_id from the insertion event you
 | |
|                 want this batch to connect to.
 | |
|             inherited_depth: The depth to create the events at (you will
 | |
|                 probably by calling inherit_depth_from_prev_ids(...)).
 | |
|             initial_state_event_ids:
 | |
|                 This is used to set explicit state for the insertion event at
 | |
|                 the start of the historical batch since it's floating with no
 | |
|                 prev_events to derive state from automatically. This should
 | |
|                 probably be the state from the `prev_event` defined by
 | |
|                 `/batch_send?prev_event_id=$abc` plus the outcome of
 | |
|                 `persist_state_events_at_start`
 | |
|             app_service_requester: The requester of an application service.
 | |
| 
 | |
|         Returns:
 | |
|             Tuple containing a list of created events and the next_batch_id
 | |
|         """
 | |
| 
 | |
|         # Connect this current batch to the insertion event from the previous batch
 | |
|         last_event_in_batch = events_to_create[-1]
 | |
|         batch_event = {
 | |
|             "type": EventTypes.MSC2716_BATCH,
 | |
|             "sender": app_service_requester.user.to_string(),
 | |
|             "room_id": room_id,
 | |
|             "content": {
 | |
|                 EventContentFields.MSC2716_BATCH_ID: batch_id_to_connect_to,
 | |
|                 EventContentFields.MSC2716_HISTORICAL: True,
 | |
|             },
 | |
|             # Since the batch event is put at the end of the batch,
 | |
|             # where the newest-in-time event is, copy the origin_server_ts from
 | |
|             # the last event we're inserting
 | |
|             "origin_server_ts": last_event_in_batch["origin_server_ts"],
 | |
|         }
 | |
|         # Add the batch event to the end of the batch (newest-in-time)
 | |
|         events_to_create.append(batch_event)
 | |
| 
 | |
|         # Add an "insertion" event to the start of each batch (next to the oldest-in-time
 | |
|         # event in the batch) so the next batch can be connected to this one.
 | |
|         insertion_event = self.create_insertion_event_dict(
 | |
|             sender=app_service_requester.user.to_string(),
 | |
|             room_id=room_id,
 | |
|             # Since the insertion event is put at the start of the batch,
 | |
|             # where the oldest-in-time event is, copy the origin_server_ts from
 | |
|             # the first event we're inserting
 | |
|             origin_server_ts=events_to_create[0]["origin_server_ts"],
 | |
|         )
 | |
|         next_batch_id = insertion_event["content"][
 | |
|             EventContentFields.MSC2716_NEXT_BATCH_ID
 | |
|         ]
 | |
|         # Prepend the insertion event to the start of the batch (oldest-in-time)
 | |
|         events_to_create = [insertion_event] + events_to_create
 | |
| 
 | |
|         # Create and persist all of the historical events
 | |
|         event_ids = await self.persist_historical_events(
 | |
|             events_to_create=events_to_create,
 | |
|             room_id=room_id,
 | |
|             inherited_depth=inherited_depth,
 | |
|             initial_state_event_ids=initial_state_event_ids,
 | |
|             app_service_requester=app_service_requester,
 | |
|         )
 | |
| 
 | |
|         return event_ids, next_batch_id
 |