Example #1
0
    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)
Example #2
0
 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)
Example #3
0
 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 {}
Example #4
0
 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)
Example #5
0
 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)
Example #6
0
    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
Example #7
0
 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)
Example #8
0
    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)
Example #9
0
    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)))
Example #10
0
    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)
Example #11
0
 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)
Example #12
0
    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
Example #13
0
    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
Example #14
0
    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
Example #15
0
    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
Example #16
0
    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
Example #17
0
 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)