diff --git a/platypush/message/event/matrix.py b/platypush/message/event/matrix.py index 71032e3945..04ed49769a 100644 --- a/platypush/message/event/matrix.py +++ b/platypush/message/event/matrix.py @@ -217,3 +217,34 @@ class MatrixRoomTopicChangedEvent(MatrixEvent): :param topic: New room topic. """ super().__init__(*args, topic=topic, **kwargs) + + +class MatrixRoomTypingStartEvent(MatrixEvent): + """ + Event triggered when a user in a room starts typing. + """ + + +class MatrixRoomTypingStopEvent(MatrixEvent): + """ + Event triggered when a user in a room stops typing. + """ + + +class MatrixRoomSeenReceiptEvent(MatrixEvent): + """ + Event triggered when the last message seen by a user in a room is updated. + """ + + +class MatrixUserPresenceEvent(MatrixEvent): + """ + Event triggered when a user comes online or goes offline. + """ + + def __init__(self, *args, is_active: bool, last_active: datetime | None, **kwargs): + """ + :param is_active: True if the user is currently online. + :param topic: When the user was last active. + """ + super().__init__(*args, is_active=is_active, last_active=last_active, **kwargs) diff --git a/platypush/plugins/matrix/__init__.py b/platypush/plugins/matrix/__init__.py index ee8a252d1d..d31bce4d66 100644 --- a/platypush/plugins/matrix/__init__.py +++ b/platypush/plugins/matrix/__init__.py @@ -66,6 +66,8 @@ from nio.client.async_client import client_session from nio.client.base_client import logged_in from nio.crypto import decrypt_attachment from nio.crypto.device import OlmDevice +from nio.events.ephemeral import ReceiptEvent, TypingNoticeEvent +from nio.events.presence import PresenceEvent from nio.exceptions import OlmUnverifiedDeviceError from nio.responses import DownloadResponse, RoomMessagesResponse @@ -86,8 +88,12 @@ from platypush.message.event.matrix import ( MatrixRoomInviteEvent, MatrixRoomJoinEvent, MatrixRoomLeaveEvent, + MatrixRoomSeenReceiptEvent, MatrixRoomTopicChangedEvent, + MatrixRoomTypingStartEvent, + MatrixRoomTypingStopEvent, MatrixSyncEvent, + MatrixUserPresenceEvent, ) from platypush.plugins import AsyncRunnablePlugin, action @@ -165,6 +171,7 @@ class MatrixClient(AsyncClient): self._autotrust_users_whitelist = autotrust_users_whitelist or set() self._first_sync_performed = asyncio.Event() self._last_batches_by_room = {} + self._typing_users_by_room = {} self._encrypted_attachments_keystore_path = os.path.join( store_path, 'attachment_keys.json' @@ -417,6 +424,9 @@ class MatrixClient(AsyncClient): self.add_to_device_callback(self._on_key_verification_key, KeyVerificationKey) # type: ignore self.add_to_device_callback(self._on_key_verification_mac, KeyVerificationMac) # type: ignore self.add_to_device_callback(self._on_key_verification_accept, KeyVerificationAccept) # type: ignore + self.add_ephemeral_callback(self._on_typing, TypingNoticeEvent) # type: ignore + self.add_ephemeral_callback(self._on_receipt, ReceiptEvent) # type: ignore + self.add_presence_callback(self._on_presence, PresenceEvent) # type: ignore if self._autojoin_on_invite: self.add_event_callback(self._autojoin_room_callback, InviteEvent) # type: ignore @@ -498,9 +508,9 @@ class MatrixClient(AsyncClient): return response async def _event_base_args( - self, room: MatrixRoom, event: Event | None = None + self, room: MatrixRoom | None, event: Event | None = None ) -> dict: - sender_id = event.sender if event else None + sender_id = getattr(event, 'sender', None) sender = ( await self.get_profile(sender_id) if sender_id else None # type: ignore ) @@ -510,9 +520,15 @@ class MatrixClient(AsyncClient): 'sender_id': sender_id, 'sender_display_name': sender.displayname if sender else None, 'sender_avatar_url': sender.avatar_url if sender else None, - 'room_id': room.room_id, - 'room_name': room.name, - 'room_topic': room.topic, + **( + { + 'room_id': room.room_id, + 'room_name': room.name, + 'room_topic': room.topic, + } + if room + else {} + ), 'server_timestamp': ( datetime.datetime.fromtimestamp(event.server_timestamp / 1000) if event and getattr(event, 'server_timestamp', None) @@ -738,16 +754,72 @@ class MatrixClient(AsyncClient): await self.room_leave(room.room_id) await self.join(event.replacement_room) + async def _on_typing(self, room: MatrixRoom, event: TypingNoticeEvent): + users = set(event.users) + typing_users = self._typing_users_by_room.get(room.room_id, set()) + start_typing_users = users.difference(typing_users) + stop_typing_users = typing_users.difference(users) + + for user in start_typing_users: + event.sender = user # type: ignore + get_bus().post( + MatrixRoomTypingStartEvent( + **(await self._event_base_args(room, event)), # type: ignore + sender=user, + ) + ) + + for user in stop_typing_users: + event.sender = user # type: ignore + get_bus().post( + MatrixRoomTypingStopEvent( + **(await self._event_base_args(room, event)), # type: ignore + ) + ) + + self._typing_users_by_room[room.room_id] = users + + async def _on_receipt(self, room: MatrixRoom, event: ReceiptEvent): + if self._first_sync_performed.is_set(): + for receipt in event.receipts: + event.sender = receipt.user_id # type: ignore + get_bus().post( + MatrixRoomSeenReceiptEvent( + **(await self._event_base_args(room, event)), # type: ignore + ) + ) + + async def _on_presence(self, event: PresenceEvent): + if self._first_sync_performed.is_set(): + last_active = ( + ( + datetime.datetime.now() + - datetime.timedelta(seconds=event.last_active_ago / 1000) + ) + if event.last_active_ago + else None + ) + + event.sender = event.user_id # type: ignore + get_bus().post( + MatrixUserPresenceEvent( + **(await self._event_base_args(None, event)), # type: ignore + is_active=event.currently_active or False, + last_active=last_active, + ) + ) + async def _on_unknown_encrypted_event( self, room: MatrixRoom, event: UnknownEncryptedEvent | MegolmEvent ): - body = getattr(event, 'ciphertext', '') - get_bus().post( - MatrixEncryptedMessageEvent( - body=body, - **(await self._event_base_args(room, event)), + if self._first_sync_performed.is_set(): + body = getattr(event, 'ciphertext', '') + get_bus().post( + MatrixEncryptedMessageEvent( + body=body, + **(await self._event_base_args(room, event)), + ) ) - ) async def _on_unknown_event(self, room: MatrixRoom, event: UnknownEvent): evt = None @@ -875,6 +947,14 @@ class MatrixPlugin(AsyncRunnablePlugin): * :class:`platypush.message.event.matrix.MatrixEncryptedMessageEvent`: when a message is received but the client doesn't have the E2E keys to decrypt it, or encryption has not been enabled. + * :class:`platypush.message.event.matrix.MatrixRoomTypingStartEvent`: + when a user in a room starts typing. + * :class:`platypush.message.event.matrix.MatrixRoomTypingStopEvent`: + when a user in a room stops typing. + * :class:`platypush.message.event.matrix.MatrixRoomSeenReceiptEvent`: + when the last message seen by a user in a room is updated. + * :class:`platypush.message.event.matrix.MatrixUserPresenceEvent`: + when a user comes online or goes offline. """ @@ -1001,6 +1081,8 @@ class MatrixPlugin(AsyncRunnablePlugin): pass except Exception as e: self.logger.exception(e) + self.logger.info('Waiting 10 seconds before reconnecting') + await asyncio.sleep(10) finally: try: await self.client.close() @@ -1185,7 +1267,7 @@ class MatrixPlugin(AsyncRunnablePlugin): first returned message will be the oldest and messages will be returned in ascending order. :param limit: Maximum number of messages to be returned (default: 10). - # :return: .. schema:: matrix.MatrixMessagesResponseSchema + :return: .. schema:: matrix.MatrixMessagesResponseSchema """ response = self._loop_execute( self.client.room_messages( @@ -1570,5 +1652,23 @@ class MatrixPlugin(AsyncRunnablePlugin): """ self._loop_execute(self.client.room_forget(room_id)) + @action + def set_display_name(self, display_name: str): + """ + Set/change the display name for the current user. + + :param display_name: New display name. + """ + self._loop_execute(self.client.set_displayname(display_name)) + + @action + def set_avatar(self, url: str): + """ + Set/change the avatar URL for the current user. + + :param url: New avatar URL. It must be a valid ``mxc://`` link. + """ + self._loop_execute(self.client.set_avatar(url)) + # vim:sw=4:ts=4:et: diff --git a/platypush/plugins/matrix/manifest.yaml b/platypush/plugins/matrix/manifest.yaml index 1b8b2fc920..b348205275 100644 --- a/platypush/plugins/matrix/manifest.yaml +++ b/platypush/plugins/matrix/manifest.yaml @@ -1,6 +1,7 @@ manifest: events: - platypush.message.event.matrix.MatrixMessageEvent: when a message is received. + platypush.message.event.matrix.MatrixMessageEvent: when a message is + received. platypush.message.event.matrix.MatrixMessageImageEvent: when a message containing an image is received. platypush.message.event.matrix.MatrixMessageAudioEvent: when a message @@ -11,17 +12,33 @@ manifest: containing a generic file is received. platypush.message.event.matrix.MatrixSyncEvent: when the startup synchronization has been completed and the plugin is ready to use. - platypush.message.event.matrix.MatrixRoomCreatedEvent: when a room is created. - platypush.message.event.matrix.MatrixRoomJoinEvent: when a user joins a room. - platypush.message.event.matrix.MatrixRoomLeaveEvent: when a user leaves a room. - platypush.message.event.matrix.MatrixRoomInviteEvent: when the user is invited to a room. - platypush.message.event.matrix.MatrixRoomTopicChangedEvent: when the topic/title of a room changes. - platypush.message.event.matrix.MatrixCallInviteEvent: when the user is invited to a call. - platypush.message.event.matrix.MatrixCallAnswerEvent: when a called user wishes to pick the call. - platypush.message.event.matrix.MatrixCallHangupEvent: when a called user exits the call. - platypush.message.event.matrix.MatrixEncryptedMessageEvent: | - when a message is received but the client doesn't - have the E2E keys to decrypt it, or encryption has not been enabled. + platypush.message.event.matrix.MatrixRoomCreatedEvent: when a room is + created. + platypush.message.event.matrix.MatrixRoomJoinEvent: when a user joins a + room. + platypush.message.event.matrix.MatrixRoomLeaveEvent: when a user leaves a + room. + platypush.message.event.matrix.MatrixRoomInviteEvent: when the user is + invited to a room. + platypush.message.event.matrix.MatrixRoomTopicChangedEvent: when the + topic/title of a room changes. + platypush.message.event.matrix.MatrixCallInviteEvent: when the user is + invited to a call. + platypush.message.event.matrix.MatrixCallAnswerEvent: when a called user + wishes to pick the call. + platypush.message.event.matrix.MatrixCallHangupEvent: when a called user + exits the call. + platypush.message.event.matrix.MatrixEncryptedMessageEvent: when a message + is received but the client doesn't have the E2E keys to decrypt it, or + encryption has not been enabled. + platypush.message.event.matrix.MatrixRoomTypingStartEvent: when a user in a + room starts typing. + platypush.message.event.matrix.MatrixRoomTypingStopEvent: when a user in a + room stops typing. + platypush.message.event.matrix.MatrixRoomSeenReceiptEvent: when the last + message seen by a user in a room is updated. + platypush.message.event.matrix.MatrixUserPresenceEvent: when a user comes + online or goes offline. apt: - libolm-devel pacman: