From 3900e666ff875ce3da4c4cf34b4aeb467181cbdf Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Sat, 2 Apr 2022 23:16:53 -0400 Subject: [PATCH] Update mautrix-python & copy latest goodies from mautrix-facebook --- matrix_appservice_kakaotalk/db/message.py | 5 +- matrix_appservice_kakaotalk/db/reaction.py | 10 +++ .../example-config.yaml | 3 + matrix_appservice_kakaotalk/matrix.py | 74 ---------------- matrix_appservice_kakaotalk/portal.py | 84 ++++++++++++------- matrix_appservice_kakaotalk/puppet.py | 6 +- matrix_appservice_kakaotalk/user.py | 14 ++++ requirements.txt | 2 +- setup.py | 2 +- 9 files changed, 91 insertions(+), 109 deletions(-) diff --git a/matrix_appservice_kakaotalk/db/message.py b/matrix_appservice_kakaotalk/db/message.py index 6a33d67..ace4ed1 100644 --- a/matrix_appservice_kakaotalk/db/message.py +++ b/matrix_appservice_kakaotalk/db/message.py @@ -110,9 +110,9 @@ class Message: event_ids: list[EventID], timestamp: int, mx_room: RoomID, - ) -> None: + ) -> list[Message]: if not event_ids: - return + return [] columns = [col.strip('"') for col in cls.columns.split(", ")] records = [ (mxid, mx_room, ktid, index, kt_chat, kt_receiver, timestamp) @@ -123,6 +123,7 @@ class Message: await conn.copy_records_to_table("message", records=records, columns=columns) else: await conn.executemany(cls._insert_query, records) + return [Message(*record) for record in records] async def insert(self) -> None: diff --git a/matrix_appservice_kakaotalk/db/reaction.py b/matrix_appservice_kakaotalk/db/reaction.py index c7b3bd1..714458e 100644 --- a/matrix_appservice_kakaotalk/db/reaction.py +++ b/matrix_appservice_kakaotalk/db/reaction.py @@ -43,6 +43,16 @@ class Reaction: return None return cls(**row) + @classmethod + async def get_by_message_ktid(cls, kt_msgid: str, kt_receiver: int) -> dict[int, Reaction]: + q = ( + "SELECT mxid, mx_room, kt_msgid, kt_receiver, kt_sender, reaction " + "FROM reaction WHERE kt_msgid=$1 AND kt_receiver=$2" + ) + rows = await cls.db.fetch(q, kt_msgid, kt_receiver) + row_gen = (cls._from_row(row) for row in rows) + return {react.kt_sender: react for react in row_gen} + @classmethod async def get_by_ktid(cls, kt_msgid: str, kt_receiver: int, kt_sender: int) -> Reaction | None: q = ( diff --git a/matrix_appservice_kakaotalk/example-config.yaml b/matrix_appservice_kakaotalk/example-config.yaml index d7a86fd..632344d 100644 --- a/matrix_appservice_kakaotalk/example-config.yaml +++ b/matrix_appservice_kakaotalk/example-config.yaml @@ -19,6 +19,9 @@ homeserver: status_endpoint: null # Endpoint for reporting per-message status. message_send_checkpoint_endpoint: null + # Whether asynchronous uploads via MSC2246 should be enabled for media. + # Requires a media repo that supports MSC2246. + async_media: false # Application service host/registration related details # Changing these values requires regeneration of the registration. diff --git a/matrix_appservice_kakaotalk/matrix.py b/matrix_appservice_kakaotalk/matrix.py index f9938c7..b336a13 100644 --- a/matrix_appservice_kakaotalk/matrix.py +++ b/matrix_appservice_kakaotalk/matrix.py @@ -64,80 +64,6 @@ class MatrixHandler(BaseMatrixHandler): room_id, "This room has been marked as your KakaoTalk bridge notice room." ) - async def handle_puppet_invite( - self, room_id: RoomID, puppet: pu.Puppet, invited_by: u.User, event_id: EventID - ) -> None: - intent = puppet.default_mxid_intent - self.log.debug(f"{invited_by.mxid} invited puppet for {puppet.ktid} to {room_id}") - if not await invited_by.is_logged_in(): - await intent.error_and_leave( - room_id, - text="Please log in before inviting KakaoTalk puppets to private chats.", - ) - return - - portal = await po.Portal.get_by_mxid(room_id) - if portal: - if portal.is_direct: - await intent.error_and_leave( - room_id, text="You can not invite additional users to private chats." - ) - return - # TODO add KakaoTalk inviting - # await portal.invite_kakaotalk(inviter, puppet) - # await intent.join_room(room_id) - return - await intent.join_room(room_id) - try: - members = await intent.get_room_members(room_id) - except MatrixError: - self.log.exception(f"Failed to get member list after joining {room_id}") - await intent.leave_room(room_id) - return - if len(members) > 2: - # TODO add KakaoTalk group creating - await intent.send_notice( - room_id, "You can not invite KakaoTalk puppets to multi-user rooms." - ) - await intent.leave_room(room_id) - return - portal = await po.Portal.get_by_ktid( - puppet.ktid, fb_receiver=invited_by.ktid # TODO kt_type=?? - ) - if portal.mxid: - try: - await intent.invite_user(portal.mxid, invited_by.mxid, check_cache=False) - await intent.send_notice( - room_id, - text=f"You already have a private chat with me in room {portal.mxid}", - html=( - "You already have a private chat with me: " - f"Link to room" - ), - ) - await intent.leave_room(room_id) - return - except MatrixError: - pass - portal.mxid = room_id - e2be_ok = await portal.check_dm_encryption() - await portal.save() - if e2be_ok is True: - evt_type, content = await self.e2ee.encrypt( - room_id, - EventType.ROOM_MESSAGE, - TextMessageEventContent( - msgtype=MessageType.NOTICE, - body="Portal to private chat created and end-to-bridge encryption enabled.", - ), - ) - await intent.send_message_event(room_id, evt_type, content) - else: - message = "Portal to private chat created." - if e2be_ok is False: - message += "\n\nWarning: Failed to enable end-to-bridge encryption" - await intent.send_notice(room_id, message) - async def handle_invite( self, room_id: RoomID, user_id: UserID, invited_by: u.User, event_id: EventID ) -> None: diff --git a/matrix_appservice_kakaotalk/portal.py b/matrix_appservice_kakaotalk/portal.py index 13e9ef1..d31abd8 100644 --- a/matrix_appservice_kakaotalk/portal.py +++ b/matrix_appservice_kakaotalk/portal.py @@ -217,9 +217,14 @@ class Portal(DBPortal, BasePortal): async def delete(self) -> None: if self.mxid: await DBMessage.delete_all_by_room(self.mxid) + self.by_mxid.pop(self.mxid, None) self.by_ktid.pop(self._ktid_full, None) - self.by_mxid.pop(self.mxid, None) - await super().delete() + self.mxid = None + self.name_set = False + self.avatar_set = False + self.relay_user_id = None + self.encrypted = False + await super().save() # endregion # region Properties @@ -265,6 +270,11 @@ class Portal(DBPortal, BasePortal): ) return self._main_intent + async def get_dm_puppet(self) -> p.Puppet | None: + if not self.is_direct: + return None + return await p.Puppet.get_by_ktid(self.kt_sender) + # endregion # region Chat info updating @@ -362,7 +372,12 @@ class Portal(DBPortal, BasePortal): data, decryption_info = encrypt_attachment(data) upload_mime_type = "application/octet-stream" filename = None - url = await intent.upload_media(data, mime_type=upload_mime_type, filename=filename) + url = await intent.upload_media( + data, + mime_type=upload_mime_type, + filename=filename, + async_upload=cls.config["homeserver.async_media"], + ) if decryption_info: decryption_info.url = url return url, info, decryption_info @@ -436,6 +451,15 @@ class Portal(DBPortal, BasePortal): self.avatar_set = False return True + async def update_info_from_puppet(self, puppet: p.Puppet | None = None) -> bool: + if not self.is_direct: + return False + if not puppet: + puppet = await self.get_dm_puppet() + changed = await self._update_name(puppet.name) + changed = await self._update_photo_from_puppet(puppet) or changed + return changed + """ async def sync_per_room_nick(self, puppet: p.Puppet, name: str) -> None: intent = puppet.intent_for(self) @@ -457,31 +481,41 @@ class Portal(DBPortal, BasePortal): ) """ + async def _update_participant( + self, source: u.User, participant: UserInfoUnion + ) -> bool: + # TODO nick map? + self.log.trace("Syncing participant %s", participant.id) + puppet = await p.Puppet.get_by_ktid(participant.userId) + await puppet.update_info_from_participant(source, participant) + changed = False + if self.is_direct and self._kt_sender == puppet.ktid and self.encrypted: + changed = await self._update_info_from_puppet(puppet.name) or changed + if self.mxid: + if puppet.ktid != self.kt_receiver or puppet.is_real_user: + await puppet.intent_for(self).ensure_joined(self.mxid, bot=self.main_intent) + #if puppet.ktid in nick_map: + # await self.sync_per_room_nick(puppet, nick_map[puppet.ktid]) + return changed + + async def _update_participants(self, source: u.User, participants: list[UserInfoUnion] | None = None) -> bool: + # TODO nick map? if participants is None: self.log.debug("Called _update_participants with no participants, fetching them now...") participants = await source.client.get_participants(self.channel_props) - changed = False if not self._main_intent: assert self.is_direct, "_main_intent for non-direct chat portal should have been set already" self._kt_sender = participants[ 0 if self.kt_type == KnownChannelType.MemoChat or participants[0].userId != source.ktid else 1 ].userId - self._main_intent = (await p.Puppet.get_by_ktid(self._kt_sender)).default_mxid_intent + self._main_intent = (await self.get_dm_puppet()).default_mxid_intent else: self._kt_sender = (await p.Puppet.get_by_mxid(self._main_intent.mxid)).ktid if self.is_direct else None - # TODO nick_map? - for participant in participants: - puppet = await p.Puppet.get_by_ktid(participant.userId) - await puppet.update_info_from_participant(source, participant) - if self.is_direct and self._kt_sender == puppet.ktid and self.encrypted: - changed = await self._update_name(puppet.name) or changed - changed = await self._update_photo_from_puppet(puppet) or changed - if self.mxid: - if puppet.ktid != self.kt_receiver or puppet.is_real_user: - await puppet.intent_for(self).ensure_joined(self.mxid, bot=self.main_intent) - #if puppet.ktid in nick_map: - # await self.sync_per_room_nick(puppet, nick_map[puppet.ktid]) + sync_tasks = [ + self._update_participant(source, pcp) for pcp in participants + ] + changed = any(await asyncio.gather(*sync_tasks)) return changed # endregion @@ -833,7 +867,7 @@ class Portal(DBPortal, BasePortal): if message.relates_to.rel_type == RelationType.REPLY: reply_to_msg = await DBMessage.get_by_mxid(message.relates_to.event_id, self.mxid) if reply_to_msg: - reply_to = reply_to_msg.fbid + reply_to = reply_to_msg.ktid else: self.log.warning( f"Couldn't find reply target {message.relates_to.event_id}" @@ -928,16 +962,6 @@ class Portal(DBPortal, BasePortal): ) self._typing = users - async def enable_dm_encryption(self) -> bool: - ok = await super().enable_dm_encryption() - if ok: - try: - puppet = await p.Puppet.get_by_ktid(self.ktid) - await self.main_intent.set_room_name(self.mxid, puppet.name) - except Exception: - self.log.warning(f"Failed to set room name", exc_info=True) - return ok - # endregion # region KakaoTalk event handling @@ -976,6 +1000,7 @@ class Portal(DBPortal, BasePortal): sender: p.Puppet, message: Chatlog, ) -> None: + # TODO Backfill!! This avoids timing conflicts on startup sync self.log.debug(f"Handling KakaoTalk event {message.logId}") if not self.mxid: mxid = await self.create_matrix_room(source) @@ -1015,6 +1040,7 @@ class Portal(DBPortal, BasePortal): self.log.warning(f"Unhandled KakaoTalk message {message.logId}") return self.log.debug(f"Handled KakaoTalk message {message.logId} -> {event_ids}") + # TODO Might have to handle remote reactions on messages created by bulk_create await DBMessage.bulk_create( ktid=message.logId, kt_chat=self.ktid, @@ -1232,7 +1258,7 @@ class Portal(DBPortal, BasePortal): messages = await source.client.get_chats( self.channel_props, after_log_id, - limit + limit, ) if not messages: self.log.debug("Didn't get any messages from server") diff --git a/matrix_appservice_kakaotalk/puppet.py b/matrix_appservice_kakaotalk/puppet.py index deba6c5..48f0c68 100644 --- a/matrix_appservice_kakaotalk/puppet.py +++ b/matrix_appservice_kakaotalk/puppet.py @@ -204,8 +204,8 @@ class Puppet(DBPuppet, BasePuppet): return True return False - @staticmethod async def reupload_avatar( + self, source: u.User, intent: IntentAPI, url: str, @@ -214,7 +214,9 @@ class Puppet(DBPuppet, BasePuppet): async with source.client.get(url) as resp: data = await resp.read() mime = magic.mimetype(data) - return await intent.upload_media(data, mime_type=mime) + return await intent.upload_media( + data, mime_type=mime, async_upload=self.config["homeserver.async_media"] + ) async def _update_photo(self, source: u.User, photo_id: str) -> bool: if photo_id != self.photo_id or not self.avatar_set: diff --git a/matrix_appservice_kakaotalk/user.py b/matrix_appservice_kakaotalk/user.py index 2b6bd35..d0e2b49 100644 --- a/matrix_appservice_kakaotalk/user.py +++ b/matrix_appservice_kakaotalk/user.py @@ -555,6 +555,20 @@ class User(DBUser, BaseUser): return None return await pu.Puppet.get_by_ktid(self.ktid) + async def get_portal_with(self, puppet: pu.Puppet, create: bool = True) -> po.Portal | None: + # TODO + return None + """ + if not self.ktid or not self.client: + return None + return await po.Portal.get_by_ktid( + await self.client.get_dm_channel_id_for(puppet.ktid), + kt_receiver=self.ktid, + create=create, + kt_type=KnownChannelType.DirectChat if puppet.ktid != self.ktid else KnownChannelType.MemoChat + ) + """ + # region KakaoTalk event handling async def on_connect(self) -> None: diff --git a/requirements.txt b/requirements.txt index ca5978a..437507c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ aiohttp>=3,<4 asyncpg>=0.20,<0.26 commonmark>=0.8,<0.10 -mautrix==0.15.0rc4 +mautrix>=0.15.4,<0.16 pycryptodome>=3,<4 python-magic>=0.4,<0.5 ruamel.yaml>=0.15.94,<0.18 diff --git a/setup.py b/setup.py index 77de715..66eef8c 100644 --- a/setup.py +++ b/setup.py @@ -48,7 +48,7 @@ setuptools.setup( install_requires=install_requires, extras_require=extras_require, - python_requires="~=3.7", + python_requires="~=3.8", classifiers=[ "Development Status :: 1 - Planning",