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 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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 *
|
||||||
"""
|
|
||||||
|
|
|
@ -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?
|
|
@ -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 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",
|
||||||
|
]
|
|
@ -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
|
# 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):
|
||||||
|
|
|
@ -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(
|
self.log.warning(f"Unhandled KakaoTalk message {message.logId}")
|
||||||
mxid=event_id,
|
return
|
||||||
mx_room=self.mxid,
|
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,
|
ktid=message.logId,
|
||||||
index=0,
|
|
||||||
kt_chat=self.ktid,
|
kt_chat=self.ktid,
|
||||||
kt_receiver=self.kt_receiver,
|
kt_receiver=self.kt_receiver,
|
||||||
|
mx_room=self.mxid,
|
||||||
timestamp=message.sendAt,
|
timestamp=message.sendAt,
|
||||||
).insert()
|
event_ids=event_ids,
|
||||||
await self._send_delivery_receipt(event_id)
|
)
|
||||||
else:
|
await self._send_delivery_receipt(event_ids[-1])
|
||||||
self.log.warning(f"Unhandled KakaoTalk message {message.logId}")
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue