From 256c4d429a95b6a2bdc9aabeda5406f6494bf99b Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Thu, 24 Mar 2022 19:32:46 -0400 Subject: [PATCH] Inbound message attachments, starting with images --- matrix_appservice_kakaotalk/db/message.py | 26 +++- .../kt/client/client.py | 11 +- .../kt/types/chat/__init__.py | 3 +- .../types/{ => chat}/attachment/__init__.py | 22 ++- .../kt/types/chat/attachment/contact.py | 42 ++++++ .../kt/types/chat/attachment/emoticon.py | 37 +++++ .../kt/types/chat/attachment/map.py | 31 ++++ .../kt/types/chat/attachment/media.py | 93 ++++++++++++ .../kt/types/{ => chat}/attachment/mention.py | 7 +- .../kt/types/chat/attachment/openlink.py | 30 ++++ .../kt/types/chat/attachment/post.py | 141 ++++++++++++++++++ .../kt/types/chat/attachment/reply.py | 40 +++++ .../kt/types/chat/chat.py | 52 ++++++- matrix_appservice_kakaotalk/portal.py | 119 +++++++++++---- matrix_appservice_kakaotalk/user.py | 1 - 15 files changed, 613 insertions(+), 42 deletions(-) rename matrix_appservice_kakaotalk/kt/types/{ => chat}/attachment/__init__.py (74%) create mode 100644 matrix_appservice_kakaotalk/kt/types/chat/attachment/contact.py create mode 100644 matrix_appservice_kakaotalk/kt/types/chat/attachment/emoticon.py create mode 100644 matrix_appservice_kakaotalk/kt/types/chat/attachment/map.py create mode 100644 matrix_appservice_kakaotalk/kt/types/chat/attachment/media.py rename matrix_appservice_kakaotalk/kt/types/{ => chat}/attachment/mention.py (94%) create mode 100644 matrix_appservice_kakaotalk/kt/types/chat/attachment/openlink.py create mode 100644 matrix_appservice_kakaotalk/kt/types/chat/attachment/post.py create mode 100644 matrix_appservice_kakaotalk/kt/types/chat/attachment/reply.py diff --git a/matrix_appservice_kakaotalk/db/message.py b/matrix_appservice_kakaotalk/db/message.py index 30ebb0c..ed01c36 100644 --- a/matrix_appservice_kakaotalk/db/message.py +++ b/matrix_appservice_kakaotalk/db/message.py @@ -21,7 +21,7 @@ from asyncpg import Record from attr import dataclass, field from mautrix.types import EventID, RoomID -from mautrix.util.async_db import Database +from mautrix.util.async_db import Database, Scheme from ..kt.types.bson import Long @@ -101,6 +101,30 @@ class Message: "VALUES ($1, $2, $3, $4, $5, $6, $7)" ) + @classmethod + async def bulk_create( + cls, + ktid: str, + kt_chat: int, + kt_receiver: int, + event_ids: list[EventID], + timestamp: int, + mx_room: RoomID, + ) -> None: + if not event_ids: + return + columns = [col.strip('"') for col in cls.columns.split(", ")] + records = [ + (mxid, mx_room, ktid, index, kt_chat, kt_receiver, timestamp) + for index, mxid in enumerate(event_ids) + ] + async with cls.db.acquire() as conn, conn.transaction(): + if cls.db.scheme == Scheme.POSTGRES: + await conn.copy_records_to_table("message", records=records, columns=columns) + else: + await conn.executemany(cls._insert_query, records) + + async def insert(self) -> None: q = self._insert_query await self.db.execute( diff --git a/matrix_appservice_kakaotalk/kt/client/client.py b/matrix_appservice_kakaotalk/kt/client/client.py index 0e374c9..c93800c 100644 --- a/matrix_appservice_kakaotalk/kt/client/client.py +++ b/matrix_appservice_kakaotalk/kt/client/client.py @@ -23,7 +23,7 @@ with any other potential backend. from __future__ import annotations from typing import TYPE_CHECKING, cast, Type, Optional, Union - +from contextlib import asynccontextmanager import logging import urllib.request @@ -65,6 +65,12 @@ if TYPE_CHECKING: from ...user import User +@asynccontextmanager +async def sandboxed_get(url: URL) -> _RequestContextManager: + async with ClientSession() as sess, sess.get(url) as resp: + yield resp + + # TODO Consider defining an interface for this, with node/native backend as swappable implementations # TODO If no state is stored, consider using free functions instead of classmethods class Client: @@ -154,6 +160,7 @@ class Client: self, url: Union[str, URL], headers: Optional[dict[str, str]] = None, + sandbox: bool = False, **kwargs, ) -> _RequestContextManager: # TODO Is auth ever needed? @@ -163,6 +170,8 @@ class Client: **(headers or {}), } url = URL(url) + if sandbox: + return sandboxed_get(url) return self.http.get(url, headers=headers, **kwargs) # endregion diff --git a/matrix_appservice_kakaotalk/kt/types/chat/__init__.py b/matrix_appservice_kakaotalk/kt/types/chat/__init__.py index c0eff5d..b3fa5a2 100644 --- a/matrix_appservice_kakaotalk/kt/types/chat/__init__.py +++ b/matrix_appservice_kakaotalk/kt/types/chat/__init__.py @@ -13,7 +13,6 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -""" +from .attachment import * from .chat_type import * from .chat import * -""" diff --git a/matrix_appservice_kakaotalk/kt/types/attachment/__init__.py b/matrix_appservice_kakaotalk/kt/types/chat/attachment/__init__.py similarity index 74% rename from matrix_appservice_kakaotalk/kt/types/attachment/__init__.py rename to matrix_appservice_kakaotalk/kt/types/chat/attachment/__init__.py index 0b590cb..f034f85 100644 --- a/matrix_appservice_kakaotalk/kt/types/attachment/__init__.py +++ b/matrix_appservice_kakaotalk/kt/types/chat/attachment/__init__.py @@ -19,8 +19,7 @@ from attr import dataclass from mautrix.types import SerializableAttrs -from ..bson import Long -from .mention import MentionStruct +from .mention import * @dataclass(kw_only=True) @@ -28,9 +27,28 @@ class Attachment(SerializableAttrs): shout: Optional[bool] = None mentions: Optional[list[MentionStruct]] = None urls: Optional[list[str]] = None + url: Optional[str] = None # NOTE Added since this may have replaced urls @dataclass class PathAttachment(Attachment): path: str s: int + + +from .media import * +from .reply import * +from .media import * +from .emoticon import * +#from .voip import * +from .contact import * +from .map import * +from .post import * +from .openlink import * + + +__all__ = [ + "Attachment", + "PathAttachment", +] +# TODO What about the import *s? diff --git a/matrix_appservice_kakaotalk/kt/types/chat/attachment/contact.py b/matrix_appservice_kakaotalk/kt/types/chat/attachment/contact.py new file mode 100644 index 0000000..66db832 --- /dev/null +++ b/matrix_appservice_kakaotalk/kt/types/chat/attachment/contact.py @@ -0,0 +1,42 @@ +# matrix-appservice-kakaotalk - A Matrix-KakaoTalk puppeting bridge. +# Copyright (C) 2022 Tulir Asokan, Andrew Ferrazzutti +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from typing import Union + +from attr import dataclass + +from ...bson import Long +from . import Attachment + + +@dataclass +class ProfileAttachment(Attachment): + userId: Union[int, Long] + nickName: str + fullProfileImageUrl: str + profileImageUrl: str + statusMessage: str + + +@dataclass +class ContactAttachment(Attachment): + name: str + url: str + + +__all__ = [ + "ProfileAttachment", + "ContactAttachment", +] diff --git a/matrix_appservice_kakaotalk/kt/types/chat/attachment/emoticon.py b/matrix_appservice_kakaotalk/kt/types/chat/attachment/emoticon.py new file mode 100644 index 0000000..5a81518 --- /dev/null +++ b/matrix_appservice_kakaotalk/kt/types/chat/attachment/emoticon.py @@ -0,0 +1,37 @@ +# matrix-appservice-kakaotalk - A Matrix-KakaoTalk puppeting bridge. +# Copyright (C) 2022 Tulir Asokan, Andrew Ferrazzutti +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from typing import Optional + +from attr import dataclass + +from . import Attachment + + +@dataclass +class EmoticonAttachment(Attachment): + path: str + name: str + type: str + alt: str + s: Optional[int] = None + sound: Optional[str] = None + width: Optional[int] = None + height: Optional[int] = None + + +__all__ = [ + "EmoticonAttachment", +] diff --git a/matrix_appservice_kakaotalk/kt/types/chat/attachment/map.py b/matrix_appservice_kakaotalk/kt/types/chat/attachment/map.py new file mode 100644 index 0000000..6084936 --- /dev/null +++ b/matrix_appservice_kakaotalk/kt/types/chat/attachment/map.py @@ -0,0 +1,31 @@ +# matrix-appservice-kakaotalk - A Matrix-KakaoTalk puppeting bridge. +# Copyright (C) 2022 Tulir Asokan, Andrew Ferrazzutti +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from attr import dataclass + +from . import Attachment + + +@dataclass +class MapAttachment(Attachment): + lat: int + lng: int + a: str + c: bool + + +__all__ = [ + "MapAttachment", +] diff --git a/matrix_appservice_kakaotalk/kt/types/chat/attachment/media.py b/matrix_appservice_kakaotalk/kt/types/chat/attachment/media.py new file mode 100644 index 0000000..0bf831d --- /dev/null +++ b/matrix_appservice_kakaotalk/kt/types/chat/attachment/media.py @@ -0,0 +1,93 @@ +# matrix-appservice-kakaotalk - A Matrix-KakaoTalk puppeting bridge. +# Copyright (C) 2022 Tulir Asokan, Andrew Ferrazzutti +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from typing import Optional + +from attr import dataclass + +from . import Attachment + + +@dataclass +class MediaKeyAttachment(Attachment): + k: str + url: str + s: int + + +@dataclass +class PhotoAttachment(Attachment): + w: int + h: int + thumbnailUrl: str + thumbnailWidth: int + thumbnailHeight: int + cs: str + mt: str + + +@dataclass +class MultiPhotoAttachment(Attachment): + kl: list[str] + wl: list[int] + hl: list[int] + csl: list[str] + imageUrls: list[str] + thumbnailUrls: list[str] + thumbnailWidths: list[int] + thumbnailHeights: list[int] + sl: int + + +@dataclass +class VideoAttachment(Attachment): + tk: str + w: int + h: int + cs: str + d: int + + +@dataclass +class FileAttachment(Attachment): + name: str + size: int + expire: int + cs: str + + +@dataclass +class AudioAttachment(Attachment): + d: int + expire: Optional[int] = None + + +@dataclass +class LongTextAttachment(Attachment): + path: str + k: str + s: int + sd: bool + + +__all__ = [ + "MediaKeyAttachment", + "PhotoAttachment", + "MultiPhotoAttachment", + "VideoAttachment", + "FileAttachment", + "AudioAttachment", + "LongTextAttachment", +] diff --git a/matrix_appservice_kakaotalk/kt/types/attachment/mention.py b/matrix_appservice_kakaotalk/kt/types/chat/attachment/mention.py similarity index 94% rename from matrix_appservice_kakaotalk/kt/types/attachment/mention.py rename to matrix_appservice_kakaotalk/kt/types/chat/attachment/mention.py index c151b1c..1006891 100644 --- a/matrix_appservice_kakaotalk/kt/types/attachment/mention.py +++ b/matrix_appservice_kakaotalk/kt/types/chat/attachment/mention.py @@ -19,7 +19,7 @@ from attr import dataclass from mautrix.types import SerializableAttrs -from ..bson import Long +from ...bson import Long @dataclass @@ -27,3 +27,8 @@ class MentionStruct(SerializableAttrs): at: list[int] len: int user_id: Union[Long, int] + + +__all__ = [ + "MentionStruct", +] diff --git a/matrix_appservice_kakaotalk/kt/types/chat/attachment/openlink.py b/matrix_appservice_kakaotalk/kt/types/chat/attachment/openlink.py new file mode 100644 index 0000000..cead03a --- /dev/null +++ b/matrix_appservice_kakaotalk/kt/types/chat/attachment/openlink.py @@ -0,0 +1,30 @@ +# matrix-appservice-kakaotalk - A Matrix-KakaoTalk puppeting bridge. +# Copyright (C) 2022 Tulir Asokan, Andrew Ferrazzutti +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from attr import dataclass + +from .post import PostAttachment + + +@dataclass +class OpenScheduleAttachment(PostAttachment): + scheduleId: int + title: str + eventAt: int + + +__all__ = [ + "OpenScheduleAttachment", +] diff --git a/matrix_appservice_kakaotalk/kt/types/chat/attachment/post.py b/matrix_appservice_kakaotalk/kt/types/chat/attachment/post.py new file mode 100644 index 0000000..2ea9c83 --- /dev/null +++ b/matrix_appservice_kakaotalk/kt/types/chat/attachment/post.py @@ -0,0 +1,141 @@ +# matrix-appservice-kakaotalk - A Matrix-KakaoTalk puppeting bridge. +# Copyright (C) 2022 Tulir Asokan, Andrew Ferrazzutti +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from typing import Optional, Union +from enum import IntEnum + +from attr import dataclass + +from mautrix.types import SerializableAttrs + +from ...bson import Long +from . import Attachment +from .emoticon import EmoticonAttachment + + +class KnownPostItemType(IntEnum): + TEXT = 1 + FOOTER = 2 + HEADER = 3 + EMOTICON = 4 + IMAGE = 5 + VIDEO = 6 + FILE = 7 + SCHEDULE = 8 + VOTE = 9 + SCRAP = 10 + +PostItemType = Union[KnownPostItemType, int] + + +class KnownPostSubItemType(IntEnum): + pass + +PostSubItemType = Union[KnownPostSubItemType, int] + + +class KnownPostFooterStyle(IntEnum): + ARTICLE = 1 + SCHEDULE = 2 + SCHEDULE_ANSWER = 3 + VOTE = 4 + VOTE_RESULT = 5 + +PostFooterStyle = Union[KnownPostFooterStyle, int] + + +class PostItem: + @dataclass + class Unknown(SerializableAttrs): + t: PostItemType + + @dataclass(kw_only=True) + class Text(Unknown): + t = KnownPostItemType.TEXT + ct: str + jct: str + + @dataclass(kw_only=True) + class Header(Unknown): + t = KnownPostItemType.HEADER + st: int + + @dataclass + class UDict(SerializableAttrs): + id: Union[int, Long] + + u: Optional[UDict] + + @dataclass(kw_only=True) + class Image(Unknown): + t = KnownPostItemType.IMAGE + tt: Optional[str] = None + th: list[str] + g: Optional[bool] = None + + @dataclass(kw_only=True) + class Emoticon(Unknown): + t = KnownPostItemType.EMOTICON + ct: EmoticonAttachment + + @dataclass(kw_only=True) + class Vote(Unknown): + t = KnownPostItemType.VOTE + st: int + tt: str + ittpe: Optional[str] = None + its: list[dict] + + @dataclass(kw_only=True) + class Video(Unknown): + t = KnownPostItemType.VIDEO + th: str + + @dataclass(kw_only=True) + class File(Unknown): + t = KnownPostItemType.FILE + tt: str + c: int + + @dataclass(kw_only=True) + class Footer(Unknown): + t = KnownPostItemType.FOOTER + st: PostFooterStyle + url: str + + +@dataclass(kw_only=True) +class PostAttachment(Attachment): + subtype: Optional[PostSubItemType] = None + os: list[PostItem.Unknown] + + +@dataclass +class VoteAttachment(PostAttachment): + voteId: int + title: str + + +__all__ = [ + "KnownPostItemType", + "PostItemType", + "KnownPostSubItemType", + "PostSubItemType", + "KnownPostFooterStyle", + "PostFooterStyle", + "PostItem", + "PostAttachment", + "VoteAttachment", +] diff --git a/matrix_appservice_kakaotalk/kt/types/chat/attachment/reply.py b/matrix_appservice_kakaotalk/kt/types/chat/attachment/reply.py new file mode 100644 index 0000000..dbb9993 --- /dev/null +++ b/matrix_appservice_kakaotalk/kt/types/chat/attachment/reply.py @@ -0,0 +1,40 @@ +# matrix-appservice-kakaotalk - A Matrix-KakaoTalk puppeting bridge. +# Copyright (C) 2022 Tulir Asokan, Andrew Ferrazzutti +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from typing import Optional + +from attr import dataclass + +from ...bson import Long +from . import Attachment +from ..chat_type import ChatType +from .mention import MentionStruct + + +@dataclass(kw_only=True) +class ReplyAttachment(Attachment): + attach_only: bool + attach_type: int + src_linkId: Optional[Long] = None + src_logId: Long + src_mentions: list[MentionStruct] + src_message: str + src_type: ChatType + src_userId: Long + + +__all__ = [ + "ReplyAttachment", +] diff --git a/matrix_appservice_kakaotalk/kt/types/chat/chat.py b/matrix_appservice_kakaotalk/kt/types/chat/chat.py index 0f454a4..62201ed 100644 --- a/matrix_appservice_kakaotalk/kt/types/chat/chat.py +++ b/matrix_appservice_kakaotalk/kt/types/chat/chat.py @@ -13,16 +13,30 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import Optional, Union +from typing import Optional, Union, Type from attr import dataclass -from mautrix.types import SerializableAttrs +from mautrix.types import SerializableAttrs, JSON from ..bson import Long from ..user.channel_user import ChannelUser -from ..attachment import Attachment -from .chat_type import ChatType +from .attachment import ( + Attachment, + PhotoAttachment, + VideoAttachment, + ContactAttachment, + AudioAttachment, + EmoticonAttachment, + OpenScheduleAttachment, + VoteAttachment, + MapAttachment, + ProfileAttachment, + FileAttachment, + PostAttachment, + ReplyAttachment, +) +from .chat_type import ChatType, KnownChatType @dataclass @@ -35,6 +49,36 @@ class Chat(ChatTypeComponent): attachment: Optional[Attachment] = None supplement: Optional[dict] = None + @classmethod + def deserialize(cls, data: JSON) -> "Chat": + if "attachment" in data: + attachment = _attachment_type_map.get(int(data["type"]), Attachment).deserialize(data["attachment"]) + del data["attachment"] + else: + attachment = None + obj = super().deserialize(data) + obj.attachment = attachment + return obj + +# TODO More +_attachment_type_map: dict[KnownChatType, Type[Attachment]] = { + KnownChatType.PHOTO: PhotoAttachment, + KnownChatType.VIDEO: VideoAttachment, + KnownChatType.CONTACT: ContactAttachment, + KnownChatType.AUDIO: AudioAttachment, + KnownChatType.DITEMEMOTICON: EmoticonAttachment, + KnownChatType.SCHEDULE: OpenScheduleAttachment, + KnownChatType.VOTE: VoteAttachment, + KnownChatType.MAP: MapAttachment, + KnownChatType.PROFILE: ProfileAttachment, + KnownChatType.FILE: FileAttachment, + KnownChatType.POST: PostAttachment, + KnownChatType.REPLY: ReplyAttachment, + KnownChatType.OPEN_SCHEDULE: OpenScheduleAttachment, + KnownChatType.OPEN_VOTE: VoteAttachment, + KnownChatType.OPEN_POST: PostAttachment, +} + @dataclass class TypedChat(Chat, ChatTypeComponent): diff --git a/matrix_appservice_kakaotalk/portal.py b/matrix_appservice_kakaotalk/portal.py index 249a756..e012fc2 100644 --- a/matrix_appservice_kakaotalk/portal.py +++ b/matrix_appservice_kakaotalk/portal.py @@ -15,7 +15,16 @@ # along with this program. If not, see . from __future__ import annotations -from typing import TYPE_CHECKING, Any, AsyncGenerator, Pattern, cast +from typing import ( + TYPE_CHECKING, + Any, + AsyncGenerator, + Awaitable, + Callable, + Pattern, + cast, +) +from io import BytesIO import asyncio import re import time @@ -24,9 +33,13 @@ from mautrix.appservice import IntentAPI from mautrix.bridge import BasePortal, NotificationDisabler, async_getter_lock from mautrix.errors import MatrixError from mautrix.types import ( + AudioInfo, ContentURI, + EncryptedFile, EventID, EventType, + FileInfo, + ImageInfo, LocationMessageEventContent, MediaMessageEventContent, Membership, @@ -35,7 +48,9 @@ from mautrix.types import ( RoomID, TextMessageEventContent, UserID, + VideoInfo, ) +from mautrix.util import ffmpeg, magic, variation_selector from mautrix.util.message_send_checkpoint import MessageSendCheckpointStatus from mautrix.util.simple_lock import SimpleLock @@ -50,7 +65,10 @@ from .formatter import kakaotalk_to_matrix, matrix_to_kakaotalk from .kt.types.bson import Long from .kt.types.channel.channel_info import ChannelInfo from .kt.types.channel.channel_type import KnownChannelType, ChannelType -from .kt.types.chat.chat import Chatlog +from .kt.types.chat import Chatlog, KnownChatType +from .kt.types.chat.attachment import ( + PhotoAttachment, +) from .kt.client.types import UserInfoUnion, PortalChannelInfo, ChannelProps from .kt.client.errors import CommandException @@ -158,6 +176,18 @@ class Portal(DBPortal, BasePortal): NotificationDisabler.puppet_cls = p.Puppet NotificationDisabler.config_enabled = cls.config["bridge.backfill.disable_notifications"] + # TODO More + cls._message_handler_type_map: dict[ + KnownChatType, + Callable[[Portal, u.User, IntentAPI, Chatlog], Awaitable[list[EventID]]] + ] = { + KnownChatType.TEXT: cls._handle_remote_text, + KnownChatType.PHOTO: cls._handle_remote_photo, + #KnownChatType.VIDEO: cls._handle_remote_video, + #KnownChatType.AUDIO: cls._handle_remote_audio, + #KnownChatType.FILE: cls._handle_remote_file, + } + # region DB conversion async def delete(self) -> None: @@ -268,9 +298,8 @@ class Portal(DBPortal, BasePortal): await self.save() return info - """ @classmethod - async def _reupload_kt_file( + async def _reupload_remote_file( cls, url: str, source: u.User, @@ -278,20 +307,19 @@ class Portal(DBPortal, BasePortal): *, filename: str | None = None, encrypt: bool = False, - referer: str = "messenger_thread_photo", find_size: bool = False, convert_audio: bool = False, ) -> tuple[ContentURI, FileInfo | VideoInfo | AudioInfo | ImageInfo, EncryptedFile | None]: if not url: raise ValueError("URL not provided") - headers = {"referer": f"fbapp://{source.state.application.client_id}/{referer}"} sandbox = cls.config["bridge.sandbox_media_download"] - async with source.client.get(url, headers=headers, sandbox=sandbox) as resp: + # TODO Referer header? + async with source.client.get(url, sandbox=sandbox) as resp: length = int(resp.headers["Content-Length"]) if length > cls.matrix.media_config.upload_size: raise ValueError("File not available: too large") data = await resp.read() - mime = magic.from_buffer(data, mime=True) + mime = magic.mimetype(data) if convert_audio and mime != "audio/ogg": data = await ffmpeg.convert_bytes( data, ".ogg", output_args=("-c:a", "libopus"), input_mime=mime @@ -312,7 +340,6 @@ class Portal(DBPortal, BasePortal): if decryption_info: decryption_info.url = url return url, info, decryption_info - """ async def _update_name(self, name: str) -> bool: if not name: @@ -859,10 +886,9 @@ class Portal(DBPortal, BasePortal): source: u.User, sender: p.Puppet, message: Chatlog, - reply_to: Chatlog | None = None, ) -> None: try: - await self._handle_remote_message(source, sender, message, reply_to) + await self._handle_remote_message(source, sender, message) except Exception: self.log.exception( "Error handling KakaoTalk message %s", @@ -874,7 +900,6 @@ class Portal(DBPortal, BasePortal): source: u.User, sender: p.Puppet, message: Chatlog, - reply_to: Chatlog | None = None, ) -> None: self.log.debug(f"Handling KakaoTalk event {message.logId}") if not self.mxid: @@ -896,25 +921,59 @@ class Portal(DBPortal, BasePortal): await intent.ensure_joined(self.mxid) self._backfill_leave.add(intent) - if message.attachment: - self.log.info("TODO: _handle_remote_message attachments") - if message.supplement: - self.log.info("TODO: _handle_remote_message supplements") - if message.text: - content = await kakaotalk_to_matrix(message.text) - event_id = await self._send_message(intent, content, timestamp=message.sendAt) - await DBMessage( - mxid=event_id, - mx_room=self.mxid, - ktid=message.logId, - index=0, - kt_chat=self.ktid, - kt_receiver=self.kt_receiver, - timestamp=message.sendAt, - ).insert() - await self._send_delivery_receipt(event_id) - else: + event_ids = [] + handler = self._message_handler_type_map.get(message.type) + if not handler: + self.log.warning(f"No handler for message type {message.type}, falling back to text") + handler = Portal._handle_remote_text + event_ids += await handler(self, source, intent, message) + if not event_ids: self.log.warning(f"Unhandled KakaoTalk message {message.logId}") + return + event_ids = [event_id for event_id in event_ids if event_id] + self.log.debug(f"Handled KakaoTalk message {message.logId} -> {event_ids}") + await DBMessage.bulk_create( + ktid=message.logId, + kt_chat=self.ktid, + kt_receiver=self.kt_receiver, + mx_room=self.mxid, + timestamp=message.sendAt, + event_ids=event_ids, + ) + await self._send_delivery_receipt(event_ids[-1]) + + async def _handle_remote_text(self, source: u.User, intent: IntentAPI, message: Chatlog) -> list[EventID]: + # TODO Handle mentions properly + content = await kakaotalk_to_matrix(message.text) + # TODO Replies + return [await self._send_message(intent, content, timestamp=message.sendAt)] + + async def _handle_remote_photo(self, source: u.User, intent: IntentAPI, message: Chatlog) -> list[EventID]: + assert message.attachment + assert message.attachment.url or message.attachment.urls + url = message.attachment.url or message.attachment.urls[0] + assert isinstance(message.attachment, PhotoAttachment) + info = ImageInfo( + width=message.attachment.w, + height=message.attachment.h, + ) + # TODO Animated images? + mxc, additional_info, decryption_info = await self._reupload_remote_file( + url, + source, + intent, + encrypt=self.encrypted, + find_size=False, + ) + info.size = additional_info.size + info.mimetype = additional_info.mimetype + content = MediaMessageEventContent( + url=mxc, file=decryption_info, msgtype=MessageType.IMAGE, body=message.text, info=info + ) + if not content: + return [] + # TODO Replies + return [await self._send_message(intent, content, timestamp=message.sendAt)] # TODO Many more remote handlers diff --git a/matrix_appservice_kakaotalk/user.py b/matrix_appservice_kakaotalk/user.py index feca078..0003769 100644 --- a/matrix_appservice_kakaotalk/user.py +++ b/matrix_appservice_kakaotalk/user.py @@ -606,7 +606,6 @@ class User(DBUser, BaseUser): await portal.backfill_lock.wait(evt.logId) if not puppet.name: portal.schedule_resync(self, puppet) - # TODO reply_to await portal.handle_remote_message(self, puppet, evt) # TODO Many more handlers