def dispatch_event(self, event: Event, source: SyncStream) -> List[asyncio.Task]: """ Send the given event to all applicable event handlers. Args: event: The event to send. source: The sync stream the event was received in. """ if isinstance(event.content, BaseMessageEventContentFuncs): event.content.trim_reply_fallback() if getattr(event, "state_key", None) is not None: event.type = event.type.with_class(EventType.Class.STATE) elif source & SyncStream.EPHEMERAL: event.type = event.type.with_class(EventType.Class.EPHEMERAL) elif source & SyncStream.ACCOUNT_DATA: event.type = event.type.with_class(EventType.Class.ACCOUNT_DATA) elif source & SyncStream.TO_DEVICE: event.type = event.type.with_class(EventType.Class.TO_DEVICE) else: event.type = event.type.with_class(EventType.Class.MESSAGE) setattr(event, "source", source) return self.dispatch_manual_event(event.type, event, include_global_handlers=True)
def dispatch_event(self, event: Event, source: SyncStream) -> List[asyncio.Task]: if isinstance(event, MessageEvent): event = MaubotMessageEvent(event, self) elif source != SyncStream.INTERNAL: event.client = self return super().dispatch_event(event, source)
async def handle_transaction( self, txn_id: str, *, events: list[JSON], extra_data: JSON, ephemeral: list[JSON] | None = None, device_otk_count: dict[UserID, DeviceOTKCount] | None = None, device_lists: DeviceLists | None = None, ) -> JSON: for raw_edu in ephemeral or []: try: edu = EphemeralEvent.deserialize(raw_edu) except SerializerError: self.log.exception("Failed to deserialize ephemeral event %s", raw_edu) else: self.handle_matrix_event(edu) for raw_event in events: try: self._fix_prev_content(raw_event) event = Event.deserialize(raw_event) except SerializerError: self.log.exception("Failed to deserialize event %s", raw_event) else: self.handle_matrix_event(event) return {}
async def dispatch_event(self, event: Event, source: SyncStream = SyncStream.INTERNAL) -> None: if isinstance(event, MessageEvent): event = MaubotMessageEvent(event, self) elif source != SyncStream.INTERNAL: event.client = self return await super().dispatch_event(event, source)
async def handle_transaction(self, txn_id: str, events: List[JSON]) -> None: for raw_event in events: try: self._fix_prev_content(raw_event) event = Event.deserialize(raw_event) except SerializerError: self.log.exception("Failed to deserialize event %s", raw_event) else: self.handle_matrix_event(event)
async def get_messages( self, room_id: RoomID, direction: PaginationDirection, from_token: SyncToken, to_token: Optional[SyncToken] = None, limit: Optional[int] = None, filter_json: Optional[str] = None) -> PaginatedMessages: """ Get a list of message and state events for a room. Pagination parameters are used to paginate history in the room. See also: `/messages API reference`_ Args: room_id: The ID of the room to get events from. direction: The direction to return events from. from_token: The token to start returning events from. This token can be obtained from a ``prev_batch`` token returned for each room by the `sync endpoint`_, or from a ``start`` or ``end`` token returned by a previous request to this endpoint. to_token: The token to stop returning events at. limit: The maximum number of events to return. Defaults to 10. filter_json: A JSON RoomEventFilter_ to filter returned events with. Returns: .. _RoomEventFilter: https://matrix.org/docs/spec/client_server/r0.5.0#filtering .. _sync endpoint: https://matrix.org/docs/spec/client_server/r0.5.0#get-matrix-client-r0-sync .. _/messages API reference: https://matrix.org/docs/spec/client_server/r0.5.0#get-matrix-client-r0-rooms-roomid-messages """ query_params = { "from": from_token, "dir": direction.value, } if to_token: query_params["to"] = to_token if limit: query_params["limit"] = str(limit) if filter: query_params["filter"] = filter_json content = await self.api.request(Method.GET, Path.rooms[room_id].messages, query_params=query_params) try: return PaginatedMessages( content["start"], content["end"], [Event.deserialize(event) for event in content["chunk"]]) except KeyError: if "start" not in content: raise MatrixResponseError("`start` not in response.") elif "end" not in content: raise MatrixResponseError("`start` not in response.") raise MatrixResponseError("`content` not in response.") except SerializerError as e: raise MatrixResponseError("Invalid events in response") from e
def decrypt(self, event: EncryptedEvent) -> MessageEvent: serialized = event.serialize() event = self.client.decrypt_event( NioEvent.parse_encrypted_event(serialized)) try: event.source["content"]["m.relates_to"] = serialized["content"][ "m.relates_to"] except KeyError: pass return Event.deserialize(event.source)
async def handle_event(self, event: Event) -> None: self.log.debug("Handle event") domain = self.config['homeserver.domain'] namespace = self.config['appservice.namespace'] event_type: str = event.get("type", "m.unknown") room_id: Optional[RoomID] = event.get("room_id", None) event_id: Optional[EventID] = event.get("event_id", None) sender: Optional[UserID] = event.get("sender", None) content: Dict = event.get("content", {}) self.log.debug(f"Event {event}") self.log.debug(f"Event type: {event.type}") self.log.debug(f"Event room_id: {room_id}") self.log.debug(f"Event sender: {sender}") self.log.debug(f"Event content: {content}") if event.type == EventType.ROOM_MEMBER: event: StateEvent prev_content = event.unsigned.prev_content or MemberStateEventContent( ) prev_membership = prev_content.membership if prev_content else Membership.JOIN if event.content.membership == Membership.LEAVE: if event.sender == event.state_key: await self.sl.matrix_user_left(UserID(event.state_key), event.room_id, event.event_id) elif event.content.membership == Membership.JOIN: if prev_membership != Membership.JOIN: await self.sl.matrix_user_joined(UserID(event.state_key), event.room_id, event.event_id) elif event.type in (EventType.ROOM_MESSAGE, EventType.STICKER): event: MessageEvent if event.type != EventType.ROOM_MESSAGE: event.content.msgtype = MessageType(str(event.type)) await self.handle_message(event.room_id, event.sender, event.content, event.event_id)
def _handle_sync(self, sync_resp: dict) -> None: # Get events from rooms -> join -> [room_id] -> ephemeral -> events (array) ephemeral_events = ( event for room_id, data in sync_resp.get("rooms", {}).get("join", {}).items() for event in self._filter_events(room_id, data.get("ephemeral", {}).get("events", [])) ) # Get events from presence -> events (array) presence_events = sync_resp.get("presence", {}).get("events", []) # Deserialize and handle all events for event in chain(ephemeral_events, presence_events): asyncio.create_task(self.mx.try_handle_sync_event(Event.deserialize(event)))
def handle_sync(self, sync_resp: Dict) -> None: # Get events from rooms -> join -> [room_id] -> ephemeral -> events (array) ephemeral_events = (event for room_id, data in sync_resp.get( "rooms", {}).get("join", {}).items() for event in self._filter_events( room_id, data.get("ephemeral", {}).get("events", []))) # Get events from presence -> events (array) presence_events = sync_resp.get("presence", {}).get("events", []) # Deserialize and handle all events coro = asyncio.gather(*[ self.mx.try_handle_event(Event.deserialize(event)) for event in chain(ephemeral_events, presence_events) ], loop=self.loop) asyncio.ensure_future(coro, loop=self.loop)
async def handle_transaction( self, txn_id: str, events: List[JSON], ephemeral: Optional[List[JSON]] = None) -> None: for raw_edu in ephemeral or []: try: edu = EphemeralEvent.deserialize(raw_edu) except SerializerError: self.log.exception("Failed to deserialize ephemeral event %s", raw_edu) else: self.handle_matrix_event(edu) for raw_event in events: try: self._fix_prev_content(raw_event) event = Event.deserialize(raw_event) except SerializerError: self.log.exception("Failed to deserialize event %s", raw_event) else: self.handle_matrix_event(event)
async def get_event(self, room_id: RoomID, event_id: EventID) -> Event: """ Get a single event based on ``room_id``/``event_id``. You must have permission to retrieve this event e.g. by being a member in the room for this event. See also: `API reference <https://spec.matrix.org/v1.1/client-server-api/#get_matrixclientv3roomsroomideventeventid>`__ Args: room_id: The ID of the room the event is in. event_id: The event ID to get. Returns: The event. """ content = await self.api.request( Method.GET, Path.v3.rooms[room_id].event[event_id], metrics_method="getEvent") try: return Event.deserialize(content) except SerializerError as e: raise MatrixResponseError("Invalid event in response") from e
async def get_event(self, room_id: RoomID, event_id: EventID) -> Event: """ Get a single event based on ``room_id``/``event_id``. You must have permission to retrieve this event e.g. by being a member in the room for this event. See also: `/event/{eventId} API reference`_ Args: room_id: The ID of the room the event is in. event_id: The event ID to get. Returns: The event. .. _/event/{eventId} API reference: https://matrix.org/docs/spec/client_server/r0.5.0#get-matrix-client-r0-rooms-roomid-event-eventid """ content = await self.api.request(Method.GET, Path.rooms[room_id].event[event_id]) try: return Event.deserialize(content) except SerializerError as e: raise MatrixResponseError("Invalid event in response") from e
async def decrypt_megolm_event(self, evt: EncryptedEvent) -> Event: """ Decrypt an event that was encrypted using Megolm. Args: evt: The whole encrypted event. Returns: The decrypted event, including some unencrypted metadata from the input event. Raises: DecryptionError: If decryption failed. """ if not isinstance(evt.content, EncryptedMegolmEventContent): raise DecryptionError("Unsupported event content class") elif evt.content.algorithm != EncryptionAlgorithm.MEGOLM_V1: raise DecryptionError("Unsupported event encryption algorithm") session = await self.crypto_store.get_group_session(evt.room_id, evt.content.sender_key, evt.content.session_id) if session is None: # TODO check if olm session is wedged raise DecryptionError("Failed to decrypt megolm event: no session with given ID found") try: plaintext, index = session.decrypt(evt.content.ciphertext) except olm.OlmGroupSessionError as e: raise DecryptionError("Failed to decrypt megolm event") from e if not await self.crypto_store.validate_message_index(evt.content.sender_key, evt.content.session_id, evt.event_id, index, evt.timestamp): raise DecryptionError("Duplicate message index") verified = False if ((evt.content.device_id == self.client.device_id and session.signing_key == self.account.signing_key and evt.content.sender_key == self.account.identity_key)): verified = True else: device = await self.crypto_store.get_device(evt.sender, evt.content.device_id) if device and device.trust == TrustState.VERIFIED and not session.forwarding_chain: if ((device.signing_key != session.signing_key or device.identity_key != evt.content.sender_key)): raise DecryptionError("Device keys in event and verified device info " "do not match") verified = True # else: TODO query device keys? try: data = json.loads(plaintext) room_id = data["room_id"] event_type = data["type"] content = data["content"] except json.JSONDecodeError as e: raise DecryptionError("Failed to parse megolm payload") from e except KeyError as e: raise DecryptionError("Megolm payload is missing fields") from e if room_id != evt.room_id: raise DecryptionError("Encrypted megolm event is not intended for this room") result = Event.deserialize({ "room_id": evt.room_id, "event_id": evt.event_id, "sender": evt.sender, "origin_server_ts": evt.timestamp, "type": event_type, "content": content, }) result.unsigned = evt.unsigned if evt.content.relates_to: if hasattr(result.content, "relates_to"): if not result.content.relates_to: result.content.relates_to = evt.content.relates_to elif "m.relates_to" not in result.content: result.content["m.relates_to"] = evt.content.relates_to.serialize() result.type = result.type.with_class(evt.type.t_class) result["mautrix"] = { "verified": verified, } return result
async def get_messages( self, room_id: RoomID, direction: PaginationDirection, from_token: SyncToken, to_token: SyncToken | None = None, limit: int | None = None, filter_json: str | dict | RoomEventFilter | None = None, ) -> PaginatedMessages: """ Get a list of message and state events for a room. Pagination parameters are used to paginate history in the room. See also: `API reference <https://spec.matrix.org/v1.1/client-server-api/#get_matrixclientv3roomsroomidmessages>`__ Args: room_id: The ID of the room to get events from. direction: The direction to return events from. from_token: The token to start returning events from. This token can be obtained from a ``prev_batch`` token returned for each room by the `sync endpoint`_, or from a ``start`` or ``end`` token returned by a previous request to this endpoint. to_token: The token to stop returning events at. limit: The maximum number of events to return. Defaults to 10. filter_json: A JSON RoomEventFilter_ to filter returned events with. Returns: .. _RoomEventFilter: https://spec.matrix.org/v1.1/client-server-api/#filtering .. _sync endpoint: https://spec.matrix.org/v1.1/client-server-api/#get_matrixclientv3sync """ if isinstance(filter_json, Serializable): filter_json = filter_json.json() elif isinstance(filter_json, dict): filter_json = json.dumps(filter_json) query_params = { "from": from_token, "dir": direction.value, "to": to_token, "limit": str(limit) if limit else None, "filter": filter_json, } content = await self.api.request( Method.GET, Path.v3.rooms[room_id].messages, query_params=query_params, metrics_method="getMessages", ) try: return PaginatedMessages( content["start"], content["end"], [Event.deserialize(event) for event in content["chunk"]], ) except KeyError: if "start" not in content: raise MatrixResponseError("`start` not in response.") elif "end" not in content: raise MatrixResponseError("`start` not in response.") raise MatrixResponseError("`content` not in response.") except SerializerError as e: raise MatrixResponseError("Invalid events in response") from e
def handle_sync(self, data: JSON) -> List[asyncio.Task]: """ Handle a /sync object. Args: data: The data from a /sync request. """ tasks = [] otk_count = data.get("device_one_time_keys_count", {}) tasks += self.dispatch_internal_event( InternalEventType.DEVICE_OTK_COUNT, custom_type=DeviceOTKCount(curve25519=otk_count.get("curve25519", 0), signed_curve25519=otk_count.get("signed_curve25519", 0))) device_lists = data.get("device_lists", {}) tasks += self.dispatch_internal_event(InternalEventType.DEVICE_LISTS, custom_type=DeviceLists( changed=device_lists.get("changed", []), left=device_lists.get("left", []))) for raw_event in data.get("account_data", {}).get("events", []): tasks += self.dispatch_event(AccountDataEvent.deserialize(raw_event), source=SyncStream.ACCOUNT_DATA) for raw_event in data.get("ephemeral", {}).get("events", []): tasks += self.dispatch_event(EphemeralEvent.deserialize(raw_event), source=SyncStream.EPHEMERAL) for raw_event in data.get("to_device", {}).get("events", []): tasks += self.dispatch_event(ToDeviceEvent.deserialize(raw_event), source=SyncStream.TO_DEVICE) rooms = data.get("rooms", {}) for room_id, room_data in rooms.get("join", {}).items(): for raw_event in room_data.get("state", {}).get("events", []): raw_event["room_id"] = room_id tasks += self.dispatch_event(StateEvent.deserialize(raw_event), source=SyncStream.JOINED_ROOM | SyncStream.STATE) for raw_event in room_data.get("timeline", {}).get("events", []): raw_event["room_id"] = room_id tasks += self.dispatch_event(Event.deserialize(raw_event), source=SyncStream.JOINED_ROOM | SyncStream.TIMELINE) for room_id, room_data in rooms.get("invite", {}).items(): events: List[Dict[str, Any]] = room_data.get("invite_state", {}).get("events", []) for raw_event in events: raw_event["room_id"] = room_id raw_invite = next(raw_event for raw_event in events if raw_event.get("type", "") == "m.room.member" and raw_event.get("state_key", "") == self.mxid) # These aren't required by the spec, so make sure they're set raw_invite.setdefault("event_id", None) raw_invite.setdefault("origin_server_ts", int(time() * 1000)) raw_invite.setdefault("unsigned", {}) invite = StateEvent.deserialize(raw_invite) invite.unsigned.invite_room_state = [StrippedStateEvent.deserialize(raw_event) for raw_event in events if raw_event != raw_invite] tasks += self.dispatch_event(invite, source=SyncStream.INVITED_ROOM | SyncStream.STATE) for room_id, room_data in rooms.get("leave", {}).items(): for raw_event in room_data.get("timeline", {}).get("events", []): if "state_key" in raw_event: raw_event["room_id"] = room_id tasks += self.dispatch_event(StateEvent.deserialize(raw_event), source=SyncStream.LEFT_ROOM | SyncStream.TIMELINE) return tasks
async def call_handlers(self, event: Event) -> None: if isinstance(event, MessageEvent): event = MaubotMessageEvent(event, self) else: event.client = self return await super().call_handlers(event)