Compare commits

...

2 Commits

17 changed files with 630 additions and 51 deletions

View File

@ -95,7 +95,7 @@ async def enter_password(evt: CommandEvent) -> None:
except MForbidden: except MForbidden:
pass pass
assert(evt.sender.command_status) assert evt.sender.command_status
req = { req = {
"uuid": await evt.sender.get_uuid(), "uuid": await evt.sender.get_uuid(),
"form": { "form": {
@ -125,7 +125,7 @@ async def enter_password(evt: CommandEvent) -> None:
async def enter_dv_code(evt: CommandEvent) -> None: async def enter_dv_code(evt: CommandEvent) -> None:
assert(evt.sender.command_status) assert evt.sender.command_status
req: dict = evt.sender.command_status["req"] req: dict = evt.sender.command_status["req"]
passcode = evt.content.body passcode = evt.content.body
await evt.mark_read() await evt.mark_read()

View File

@ -21,7 +21,7 @@ from asyncpg import Record
from attr import dataclass, field from attr import dataclass, field
from mautrix.types import EventID, RoomID 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 from ..kt.types.bson import Long
@ -101,6 +101,30 @@ class Message:
"VALUES ($1, $2, $3, $4, $5, $6, $7)" "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: async def insert(self) -> None:
q = self._insert_query q = self._insert_query
await self.db.execute( await self.db.execute(

View File

@ -23,7 +23,7 @@ with any other potential backend.
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, cast, Type, Optional, Union from typing import TYPE_CHECKING, cast, Type, Optional, Union
from contextlib import asynccontextmanager
import logging import logging
import urllib.request import urllib.request
@ -65,6 +65,12 @@ if TYPE_CHECKING:
from ...user import User 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 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 # TODO If no state is stored, consider using free functions instead of classmethods
class Client: class Client:
@ -154,6 +160,7 @@ class Client:
self, self,
url: Union[str, URL], url: Union[str, URL],
headers: Optional[dict[str, str]] = None, headers: Optional[dict[str, str]] = None,
sandbox: bool = False,
**kwargs, **kwargs,
) -> _RequestContextManager: ) -> _RequestContextManager:
# TODO Is auth ever needed? # TODO Is auth ever needed?
@ -163,6 +170,8 @@ class Client:
**(headers or {}), **(headers or {}),
} }
url = URL(url) url = URL(url)
if sandbox:
return sandboxed_get(url)
return self.http.get(url, headers=headers, **kwargs) return self.http.get(url, headers=headers, **kwargs)
# endregion # endregion
@ -204,7 +213,7 @@ class Client:
profile_req_struct = await self._api_user_request_result( profile_req_struct = await self._api_user_request_result(
ProfileReqStruct, ProfileReqStruct,
"get_profile", "get_profile",
user_id=user_id.serialize() user_id=user_id.serialize(),
) )
return profile_req_struct.profile return profile_req_struct.profile
@ -219,7 +228,7 @@ class Client:
return await self._api_user_request_result( return await self._api_user_request_result(
ResultListType(UserInfoUnion), ResultListType(UserInfoUnion),
"get_participants", "get_participants",
channel_props=channel_props.serialize() channel_props=channel_props.serialize(),
) )
async def get_chats(self, channel_props: ChannelProps, sync_from: Long | None, limit: int | None) -> list[Chatlog]: async def get_chats(self, channel_props: ChannelProps, sync_from: Long | None, limit: int | None) -> list[Chatlog]:
@ -228,7 +237,7 @@ class Client:
"get_chats", "get_chats",
channel_props=channel_props.serialize(), channel_props=channel_props.serialize(),
sync_from=sync_from.serialize() if sync_from else None, sync_from=sync_from.serialize() if sync_from else None,
limit=limit limit=limit,
) )
async def list_friends(self) -> FriendListStruct: async def list_friends(self) -> FriendListStruct:
@ -242,7 +251,7 @@ class Client:
Chatlog, Chatlog,
"send_message", "send_message",
channel_props=channel_props.serialize(), channel_props=channel_props.serialize(),
text=text text=text,
) )

View File

@ -13,6 +13,14 @@
# #
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
""" ##from .web_client import *
from .auth_api_client import * ##from .xvc import * # as xvc
""" #from .struct import * # as struct
#from .auth_api_client import *
##from .service_api_client import *
##from .oauth_api_client import *
##from .attachment_api_client import *
##from .open_upload_api_client import *
##from .web_api_util import * # as webApiUtil
##from .header_util import * # as headerUtil
##from .service_api_util import * # as serviceApiUtil

View File

@ -13,7 +13,6 @@
# #
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
""" from .attachment import *
from .chat_type import * from .chat_type import *
from .chat import * from .chat import *
"""

View File

@ -19,8 +19,7 @@ from attr import dataclass
from mautrix.types import SerializableAttrs from mautrix.types import SerializableAttrs
from ..bson import Long from .mention import *
from .mention import MentionStruct
@dataclass(kw_only=True) @dataclass(kw_only=True)
@ -28,9 +27,28 @@ class Attachment(SerializableAttrs):
shout: Optional[bool] = None shout: Optional[bool] = None
mentions: Optional[list[MentionStruct]] = None mentions: Optional[list[MentionStruct]] = None
urls: Optional[list[str]] = None urls: Optional[list[str]] = None
url: Optional[str] = None # NOTE Added since this may have replaced urls
@dataclass @dataclass
class PathAttachment(Attachment): class PathAttachment(Attachment):
path: str path: str
s: int 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?

View File

@ -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 <https://www.gnu.org/licenses/>.
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",
]

View File

@ -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 <https://www.gnu.org/licenses/>.
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",
]

View File

@ -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 <https://www.gnu.org/licenses/>.
from attr import dataclass
from . import Attachment
@dataclass
class MapAttachment(Attachment):
lat: int
lng: int
a: str
c: bool
__all__ = [
"MapAttachment",
]

View File

@ -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 <https://www.gnu.org/licenses/>.
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",
]

View File

@ -19,7 +19,7 @@ from attr import dataclass
from mautrix.types import SerializableAttrs from mautrix.types import SerializableAttrs
from ..bson import Long from ...bson import Long
@dataclass @dataclass
@ -27,3 +27,8 @@ class MentionStruct(SerializableAttrs):
at: list[int] at: list[int]
len: int len: int
user_id: Union[Long, int] user_id: Union[Long, int]
__all__ = [
"MentionStruct",
]

View File

@ -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 <https://www.gnu.org/licenses/>.
from attr import dataclass
from .post import PostAttachment
@dataclass
class OpenScheduleAttachment(PostAttachment):
scheduleId: int
title: str
eventAt: int
__all__ = [
"OpenScheduleAttachment",
]

View File

@ -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 <https://www.gnu.org/licenses/>.
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",
]

View File

@ -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 <https://www.gnu.org/licenses/>.
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",
]

View File

@ -13,16 +13,30 @@
# #
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import Optional, Union from typing import Optional, Union, Type
from attr import dataclass from attr import dataclass
from mautrix.types import SerializableAttrs from mautrix.types import SerializableAttrs, JSON
from ..bson import Long from ..bson import Long
from ..user.channel_user import ChannelUser from ..user.channel_user import ChannelUser
from ..attachment import Attachment from .attachment import (
from .chat_type import ChatType Attachment,
PhotoAttachment,
VideoAttachment,
ContactAttachment,
AudioAttachment,
EmoticonAttachment,
OpenScheduleAttachment,
VoteAttachment,
MapAttachment,
ProfileAttachment,
FileAttachment,
PostAttachment,
ReplyAttachment,
)
from .chat_type import ChatType, KnownChatType
@dataclass @dataclass
@ -35,6 +49,36 @@ class Chat(ChatTypeComponent):
attachment: Optional[Attachment] = None attachment: Optional[Attachment] = None
supplement: Optional[dict] = 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 @dataclass
class TypedChat(Chat, ChatTypeComponent): class TypedChat(Chat, ChatTypeComponent):

View File

@ -15,7 +15,16 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations 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 asyncio
import re import re
import time import time
@ -24,9 +33,13 @@ from mautrix.appservice import IntentAPI
from mautrix.bridge import BasePortal, NotificationDisabler, async_getter_lock from mautrix.bridge import BasePortal, NotificationDisabler, async_getter_lock
from mautrix.errors import MatrixError from mautrix.errors import MatrixError
from mautrix.types import ( from mautrix.types import (
AudioInfo,
ContentURI, ContentURI,
EncryptedFile,
EventID, EventID,
EventType, EventType,
FileInfo,
ImageInfo,
LocationMessageEventContent, LocationMessageEventContent,
MediaMessageEventContent, MediaMessageEventContent,
Membership, Membership,
@ -35,7 +48,9 @@ from mautrix.types import (
RoomID, RoomID,
TextMessageEventContent, TextMessageEventContent,
UserID, UserID,
VideoInfo,
) )
from mautrix.util import ffmpeg, magic, variation_selector
from mautrix.util.message_send_checkpoint import MessageSendCheckpointStatus from mautrix.util.message_send_checkpoint import MessageSendCheckpointStatus
from mautrix.util.simple_lock import SimpleLock 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.bson import Long
from .kt.types.channel.channel_info import ChannelInfo from .kt.types.channel.channel_info import ChannelInfo
from .kt.types.channel.channel_type import KnownChannelType, ChannelType 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.types import UserInfoUnion, PortalChannelInfo, ChannelProps
from .kt.client.errors import CommandException from .kt.client.errors import CommandException
@ -158,6 +176,18 @@ class Portal(DBPortal, BasePortal):
NotificationDisabler.puppet_cls = p.Puppet NotificationDisabler.puppet_cls = p.Puppet
NotificationDisabler.config_enabled = cls.config["bridge.backfill.disable_notifications"] 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 # region DB conversion
async def delete(self) -> None: async def delete(self) -> None:
@ -268,9 +298,8 @@ class Portal(DBPortal, BasePortal):
await self.save() await self.save()
return info return info
"""
@classmethod @classmethod
async def _reupload_kt_file( async def _reupload_remote_file(
cls, cls,
url: str, url: str,
source: u.User, source: u.User,
@ -278,20 +307,19 @@ class Portal(DBPortal, BasePortal):
*, *,
filename: str | None = None, filename: str | None = None,
encrypt: bool = False, encrypt: bool = False,
referer: str = "messenger_thread_photo",
find_size: bool = False, find_size: bool = False,
convert_audio: bool = False, convert_audio: bool = False,
) -> tuple[ContentURI, FileInfo | VideoInfo | AudioInfo | ImageInfo, EncryptedFile | None]: ) -> tuple[ContentURI, FileInfo | VideoInfo | AudioInfo | ImageInfo, EncryptedFile | None]:
if not url: if not url:
raise ValueError("URL not provided") raise ValueError("URL not provided")
headers = {"referer": f"fbapp://{source.state.application.client_id}/{referer}"}
sandbox = cls.config["bridge.sandbox_media_download"] 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"]) length = int(resp.headers["Content-Length"])
if length > cls.matrix.media_config.upload_size: if length > cls.matrix.media_config.upload_size:
raise ValueError("File not available: too large") raise ValueError("File not available: too large")
data = await resp.read() data = await resp.read()
mime = magic.from_buffer(data, mime=True) mime = magic.mimetype(data)
if convert_audio and mime != "audio/ogg": if convert_audio and mime != "audio/ogg":
data = await ffmpeg.convert_bytes( data = await ffmpeg.convert_bytes(
data, ".ogg", output_args=("-c:a", "libopus"), input_mime=mime data, ".ogg", output_args=("-c:a", "libopus"), input_mime=mime
@ -312,7 +340,6 @@ class Portal(DBPortal, BasePortal):
if decryption_info: if decryption_info:
decryption_info.url = url decryption_info.url = url
return url, info, decryption_info return url, info, decryption_info
"""
async def _update_name(self, name: str) -> bool: async def _update_name(self, name: str) -> bool:
if not name: if not name:
@ -859,10 +886,9 @@ class Portal(DBPortal, BasePortal):
source: u.User, source: u.User,
sender: p.Puppet, sender: p.Puppet,
message: Chatlog, message: Chatlog,
reply_to: Chatlog | None = None,
) -> None: ) -> None:
try: try:
await self._handle_remote_message(source, sender, message, reply_to) await self._handle_remote_message(source, sender, message)
except Exception: except Exception:
self.log.exception( self.log.exception(
"Error handling KakaoTalk message %s", "Error handling KakaoTalk message %s",
@ -874,7 +900,6 @@ class Portal(DBPortal, BasePortal):
source: u.User, source: u.User,
sender: p.Puppet, sender: p.Puppet,
message: Chatlog, message: Chatlog,
reply_to: Chatlog | None = None,
) -> None: ) -> None:
self.log.debug(f"Handling KakaoTalk event {message.logId}") self.log.debug(f"Handling KakaoTalk event {message.logId}")
if not self.mxid: if not self.mxid:
@ -896,25 +921,59 @@ class Portal(DBPortal, BasePortal):
await intent.ensure_joined(self.mxid) await intent.ensure_joined(self.mxid)
self._backfill_leave.add(intent) self._backfill_leave.add(intent)
if message.attachment: event_ids = []
self.log.info("TODO: _handle_remote_message attachments") handler = self._message_handler_type_map.get(message.type)
if message.supplement: if not handler:
self.log.info("TODO: _handle_remote_message supplements") self.log.warning(f"No handler for message type {message.type}, falling back to text")
if message.text: handler = Portal._handle_remote_text
content = await kakaotalk_to_matrix(message.text) event_ids += await handler(self, source, intent, message)
event_id = await self._send_message(intent, content, timestamp=message.sendAt) if not event_ids:
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:
self.log.warning(f"Unhandled KakaoTalk message {message.logId}") 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 # TODO Many more remote handlers

View File

@ -606,7 +606,6 @@ class User(DBUser, BaseUser):
await portal.backfill_lock.wait(evt.logId) await portal.backfill_lock.wait(evt.logId)
if not puppet.name: if not puppet.name:
portal.schedule_resync(self, puppet) portal.schedule_resync(self, puppet)
# TODO reply_to
await portal.handle_remote_message(self, puppet, evt) await portal.handle_remote_message(self, puppet, evt)
# TODO Many more handlers # TODO Many more handlers