Inbound message attachments, starting with images
This commit is contained in:
parent
e099886eb1
commit
256c4d429a
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
#
|
||||
# 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 .attachment import *
|
||||
from .chat_type import *
|
||||
from .chat import *
|
||||
"""
|
||||
|
|
|
@ -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?
|
|
@ -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",
|
||||
]
|
|
@ -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",
|
||||
]
|
|
@ -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",
|
||||
]
|
|
@ -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",
|
||||
]
|
|
@ -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",
|
||||
]
|
|
@ -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",
|
||||
]
|
|
@ -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",
|
||||
]
|
|
@ -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",
|
||||
]
|
|
@ -13,16 +13,30 @@
|
|||
#
|
||||
# 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 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):
|
||||
|
|
|
@ -15,7 +15,16 @@
|
|||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
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,
|
||||
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,
|
||||
index=0,
|
||||
kt_chat=self.ktid,
|
||||
kt_receiver=self.kt_receiver,
|
||||
mx_room=self.mxid,
|
||||
timestamp=message.sendAt,
|
||||
).insert()
|
||||
await self._send_delivery_receipt(event_id)
|
||||
else:
|
||||
self.log.warning(f"Unhandled KakaoTalk message {message.logId}")
|
||||
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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue