Outbound media messages
This commit is contained in:
parent
256c4d429a
commit
ace4eefec7
|
@ -34,7 +34,7 @@ class Message:
|
||||||
|
|
||||||
mxid: EventID
|
mxid: EventID
|
||||||
mx_room: RoomID
|
mx_room: RoomID
|
||||||
ktid: Long = field(converter=Long)
|
ktid: Long | None = field(converter=lambda ktid: Long(ktid) if ktid is not None else None)
|
||||||
index: int
|
index: int
|
||||||
kt_chat: Long = field(converter=Long)
|
kt_chat: Long = field(converter=Long)
|
||||||
kt_receiver: Long = field(converter=Long)
|
kt_receiver: Long = field(converter=Long)
|
||||||
|
@ -104,9 +104,9 @@ class Message:
|
||||||
@classmethod
|
@classmethod
|
||||||
async def bulk_create(
|
async def bulk_create(
|
||||||
cls,
|
cls,
|
||||||
ktid: str,
|
ktid: Long,
|
||||||
kt_chat: int,
|
kt_chat: Long,
|
||||||
kt_receiver: int,
|
kt_receiver: Long,
|
||||||
event_ids: list[EventID],
|
event_ids: list[EventID],
|
||||||
timestamp: int,
|
timestamp: int,
|
||||||
mx_room: RoomID,
|
mx_room: RoomID,
|
||||||
|
|
|
@ -41,7 +41,7 @@ async def create_v1_tables(conn: Connection) -> None:
|
||||||
"""CREATE TABLE portal (
|
"""CREATE TABLE portal (
|
||||||
ktid BIGINT,
|
ktid BIGINT,
|
||||||
kt_receiver BIGINT,
|
kt_receiver BIGINT,
|
||||||
kt_type TEXT,
|
kt_type TEXT NOT NULL,
|
||||||
mxid TEXT UNIQUE,
|
mxid TEXT UNIQUE,
|
||||||
name TEXT,
|
name TEXT,
|
||||||
photo_id TEXT,
|
photo_id TEXT,
|
||||||
|
@ -72,13 +72,13 @@ async def create_v1_tables(conn: Connection) -> None:
|
||||||
)
|
)
|
||||||
await conn.execute(
|
await conn.execute(
|
||||||
"""CREATE TABLE message (
|
"""CREATE TABLE message (
|
||||||
mxid TEXT,
|
mxid TEXT NOT NULL,
|
||||||
mx_room TEXT,
|
mx_room TEXT NOT NULL,
|
||||||
ktid BIGINT,
|
ktid BIGINT,
|
||||||
kt_receiver BIGINT,
|
kt_receiver BIGINT NOT NULL,
|
||||||
"index" SMALLINT,
|
"index" SMALLINT NOT NULL,
|
||||||
kt_chat BIGINT,
|
kt_chat BIGINT NOT NULL,
|
||||||
timestamp BIGINT,
|
timestamp BIGINT NOT NULL,
|
||||||
PRIMARY KEY (ktid, kt_receiver, "index"),
|
PRIMARY KEY (ktid, kt_receiver, "index"),
|
||||||
FOREIGN KEY (kt_chat, kt_receiver) REFERENCES portal(ktid, kt_receiver)
|
FOREIGN KEY (kt_chat, kt_receiver) REFERENCES portal(ktid, kt_receiver)
|
||||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||||
|
|
|
@ -40,7 +40,7 @@ from ..types.api.struct.profile import ProfileReqStruct, ProfileStruct
|
||||||
from ..types.api.struct import FriendListStruct
|
from ..types.api.struct import FriendListStruct
|
||||||
from ..types.bson import Long
|
from ..types.bson import Long
|
||||||
from ..types.client.client_session import LoginResult
|
from ..types.client.client_session import LoginResult
|
||||||
from ..types.chat.chat import Chatlog
|
from ..types.chat import Chatlog, KnownChatType
|
||||||
from ..types.oauth import OAuthCredential, OAuthInfo
|
from ..types.oauth import OAuthCredential, OAuthInfo
|
||||||
from ..types.request import (
|
from ..types.request import (
|
||||||
deserialize_result,
|
deserialize_result,
|
||||||
|
@ -254,6 +254,32 @@ class Client:
|
||||||
text=text,
|
text=text,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def send_media(
|
||||||
|
self,
|
||||||
|
channel_props: ChannelProps,
|
||||||
|
media_type: KnownChatType,
|
||||||
|
data: bytes,
|
||||||
|
filename: str,
|
||||||
|
*,
|
||||||
|
width: int | None = None,
|
||||||
|
height: int | None = None,
|
||||||
|
ext: str | None = None,
|
||||||
|
) -> Chatlog:
|
||||||
|
return await self._api_user_request_result(
|
||||||
|
Chatlog,
|
||||||
|
"send_media",
|
||||||
|
channel_props=channel_props.serialize(),
|
||||||
|
type=media_type,
|
||||||
|
data=list(data),
|
||||||
|
name=filename,
|
||||||
|
width=width,
|
||||||
|
height=height,
|
||||||
|
ext=ext,
|
||||||
|
# Don't log the bytes
|
||||||
|
# TODO Disable logging per-argument, not per-command
|
||||||
|
is_secret=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# TODO Combine these into one
|
# TODO Combine these into one
|
||||||
|
|
||||||
|
|
|
@ -19,11 +19,17 @@ from typing import Optional, NewType, Union
|
||||||
|
|
||||||
from attr import dataclass
|
from attr import dataclass
|
||||||
|
|
||||||
from mautrix.types import SerializableAttrs, JSON, deserializer
|
from mautrix.types import (
|
||||||
|
SerializableAttrs,
|
||||||
|
JSON,
|
||||||
|
deserializer,
|
||||||
|
MessageType,
|
||||||
|
)
|
||||||
|
|
||||||
from ..types.bson import Long
|
from ..types.bson import Long
|
||||||
from ..types.channel.channel_info import NormalChannelInfo
|
from ..types.channel.channel_info import NormalChannelInfo
|
||||||
from ..types.channel.channel_type import ChannelType
|
from ..types.channel.channel_type import ChannelType
|
||||||
|
from ..types.chat import KnownChatType
|
||||||
from ..types.openlink.open_channel_info import OpenChannelInfo
|
from ..types.openlink.open_channel_info import OpenChannelInfo
|
||||||
from ..types.user.channel_user_info import NormalChannelUserInfo, OpenChannelUserInfo
|
from ..types.user.channel_user_info import NormalChannelUserInfo, OpenChannelUserInfo
|
||||||
|
|
||||||
|
@ -64,3 +70,17 @@ class PortalChannelInfo(SerializableAttrs):
|
||||||
class ChannelProps(SerializableAttrs):
|
class ChannelProps(SerializableAttrs):
|
||||||
id: Long
|
id: Long
|
||||||
type: ChannelType
|
type: ChannelType
|
||||||
|
|
||||||
|
|
||||||
|
# TODO Add non-media types, like polls & maps
|
||||||
|
TO_MSGTYPE_MAP: dict[MessageType, KnownChatType] = {
|
||||||
|
MessageType.TEXT: KnownChatType.TEXT,
|
||||||
|
MessageType.IMAGE: KnownChatType.PHOTO,
|
||||||
|
MessageType.STICKER: KnownChatType.PHOTO,
|
||||||
|
MessageType.VIDEO: KnownChatType.VIDEO,
|
||||||
|
MessageType.AUDIO: KnownChatType.AUDIO,
|
||||||
|
MessageType.FILE: KnownChatType.FILE,
|
||||||
|
}
|
||||||
|
|
||||||
|
# https://stackoverflow.com/a/483833
|
||||||
|
FROM_MSGTYPE_MAP: dict[KnownChatType, MessageType] = {v: k for k, v in TO_MSGTYPE_MAP.items()}
|
||||||
|
|
|
@ -25,6 +25,7 @@ from typing import (
|
||||||
cast,
|
cast,
|
||||||
)
|
)
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
from mimetypes import guess_extension
|
||||||
import asyncio
|
import asyncio
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
|
@ -50,7 +51,7 @@ from mautrix.types import (
|
||||||
UserID,
|
UserID,
|
||||||
VideoInfo,
|
VideoInfo,
|
||||||
)
|
)
|
||||||
from mautrix.util import ffmpeg, magic, variation_selector
|
from mautrix.util import ffmpeg, magic
|
||||||
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
|
||||||
|
|
||||||
|
@ -70,7 +71,12 @@ from .kt.types.chat.attachment import (
|
||||||
PhotoAttachment,
|
PhotoAttachment,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .kt.client.types import UserInfoUnion, PortalChannelInfo, ChannelProps
|
from .kt.client.types import (
|
||||||
|
UserInfoUnion,
|
||||||
|
PortalChannelInfo,
|
||||||
|
ChannelProps,
|
||||||
|
TO_MSGTYPE_MAP,
|
||||||
|
)
|
||||||
from .kt.client.errors import CommandException
|
from .kt.client.errors import CommandException
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -745,24 +751,24 @@ class Portal(DBPortal, BasePortal):
|
||||||
if message.msgtype == MessageType.TEXT or message.msgtype == MessageType.NOTICE:
|
if message.msgtype == MessageType.TEXT or message.msgtype == MessageType.NOTICE:
|
||||||
await self._handle_matrix_text(event_id, sender, message)
|
await self._handle_matrix_text(event_id, sender, message)
|
||||||
elif message.msgtype.is_media:
|
elif message.msgtype.is_media:
|
||||||
await self._handle_matrix_media(event_id, sender, message, is_relay)
|
await self._handle_matrix_media(event_id, sender, message)
|
||||||
# elif message.msgtype == MessageType.LOCATION:
|
# elif message.msgtype == MessageType.LOCATION:
|
||||||
# await self._handle_matrix_location(sender, message)
|
# await self._handle_matrix_location(sender, message)
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError(f"Unsupported message type {message.msgtype}")
|
raise NotImplementedError(f"Unsupported message type {message.msgtype}")
|
||||||
|
|
||||||
async def _make_dbm(self, sender: u.User, event_id: EventID, ktid: Long) -> None:
|
async def _make_dbm(self, event_id: EventID, ktid: Long | None = None) -> DBMessage:
|
||||||
await DBMessage(
|
dbm = DBMessage(
|
||||||
mxid=event_id,
|
mxid=event_id,
|
||||||
mx_room=self.mxid,
|
mx_room=self.mxid,
|
||||||
ktid=ktid,
|
ktid=ktid,
|
||||||
index=0,
|
index=0,
|
||||||
kt_chat=self.ktid,
|
kt_chat=self.ktid,
|
||||||
kt_receiver=self.kt_receiver,
|
kt_receiver=self.kt_receiver,
|
||||||
# TODO?
|
|
||||||
#kt_sender=sender.ktid,
|
|
||||||
timestamp=int(time.time() * 1000),
|
timestamp=int(time.time() * 1000),
|
||||||
).insert()
|
)
|
||||||
|
await dbm.insert()
|
||||||
|
return dbm
|
||||||
|
|
||||||
async def _handle_matrix_text(
|
async def _handle_matrix_text(
|
||||||
self, event_id: EventID, sender: u.User, message: TextMessageEventContent
|
self, event_id: EventID, sender: u.User, message: TextMessageEventContent
|
||||||
|
@ -776,11 +782,10 @@ class Portal(DBPortal, BasePortal):
|
||||||
#mentions=converted.mentions,
|
#mentions=converted.mentions,
|
||||||
#reply_to=converted.reply_to,
|
#reply_to=converted.reply_to,
|
||||||
)
|
)
|
||||||
except CommandException:
|
except CommandException as e:
|
||||||
self.log.debug(f"Error handling Matrix message {event_id}")
|
self.log.debug(f"Error handling Matrix message {event_id}: {e!s}")
|
||||||
raise
|
raise
|
||||||
|
await self._make_dbm(event_id, chatlog.logId)
|
||||||
await self._make_dbm(sender, event_id, chatlog.logId)
|
|
||||||
self.log.debug(f"Handled Matrix message {event_id} -> {chatlog.logId}")
|
self.log.debug(f"Handled Matrix message {event_id} -> {chatlog.logId}")
|
||||||
sender.send_remote_checkpoint(
|
sender.send_remote_checkpoint(
|
||||||
MessageSendCheckpointStatus.SUCCESS,
|
MessageSendCheckpointStatus.SUCCESS,
|
||||||
|
@ -791,9 +796,60 @@ class Portal(DBPortal, BasePortal):
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _handle_matrix_media(
|
async def _handle_matrix_media(
|
||||||
self, event_id: EventID, sender: u.User, message: MediaMessageEventContent, is_relay: bool
|
self, event_id: EventID, sender: u.User, message: MediaMessageEventContent
|
||||||
) -> None:
|
) -> None:
|
||||||
self.log.info("TODO: _handle_matrix_media")
|
if message.file and decrypt_attachment:
|
||||||
|
data = await self.main_intent.download_media(message.file.url)
|
||||||
|
data = decrypt_attachment(
|
||||||
|
data, message.file.key.key, message.file.hashes.get("sha256"), message.file.iv
|
||||||
|
)
|
||||||
|
elif message.url:
|
||||||
|
data = await self.main_intent.download_media(message.url)
|
||||||
|
else:
|
||||||
|
raise NotImplementedError("No file or URL specified")
|
||||||
|
mime = message.info.mimetype or magic.mimetype(data)
|
||||||
|
""" TODO Replies
|
||||||
|
reply_to = None
|
||||||
|
if message.relates_to.rel_type == RelationType.REPLY:
|
||||||
|
reply_to_msg = await DBMessage.get_by_mxid(message.relates_to.event_id, self.mxid)
|
||||||
|
if reply_to_msg:
|
||||||
|
reply_to = reply_to_msg.fbid
|
||||||
|
else:
|
||||||
|
self.log.warning(
|
||||||
|
f"Couldn't find reply target {message.relates_to.event_id}"
|
||||||
|
" to bridge media message reply metadata to KakaoTalk"
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
filename = message.body
|
||||||
|
width, height = None, None
|
||||||
|
# TODO Find out why/if stickers are always blank
|
||||||
|
if message.info in (MessageType.IMAGE, MessageType.STICKER, MessageType.VIDEO):
|
||||||
|
width = message.info.width
|
||||||
|
height = message.info.height
|
||||||
|
try:
|
||||||
|
chatlog = await sender.client.send_media(
|
||||||
|
self.channel_props,
|
||||||
|
TO_MSGTYPE_MAP[message.msgtype],
|
||||||
|
data,
|
||||||
|
filename,
|
||||||
|
width=width,
|
||||||
|
height=height,
|
||||||
|
ext=guess_extension(mime)[1:],
|
||||||
|
# TODO
|
||||||
|
#reply_to=reply_to,
|
||||||
|
)
|
||||||
|
except CommandException as e:
|
||||||
|
self.log.debug(f"Error uploading media for Matrix message {event_id}: {e!s}")
|
||||||
|
raise
|
||||||
|
await self._make_dbm(event_id, chatlog.logId)
|
||||||
|
self.log.debug(f"Handled Matrix message {event_id} -> {chatlog.logId}")
|
||||||
|
sender.send_remote_checkpoint(
|
||||||
|
MessageSendCheckpointStatus.SUCCESS,
|
||||||
|
event_id,
|
||||||
|
self.mxid,
|
||||||
|
EventType.ROOM_MESSAGE,
|
||||||
|
message.msgtype,
|
||||||
|
)
|
||||||
|
|
||||||
async def _handle_matrix_location(
|
async def _handle_matrix_location(
|
||||||
self, sender: u.User, message: LocationMessageEventContent
|
self, sender: u.User, message: LocationMessageEventContent
|
||||||
|
|
|
@ -451,6 +451,30 @@ export default class PeerClient {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Object} req
|
||||||
|
* @param {string} req.mxid
|
||||||
|
* @param {Object} req.channel_props
|
||||||
|
* @param {int} req.type
|
||||||
|
* @param {[number]} req.data
|
||||||
|
* @param {string} req.name
|
||||||
|
* @param {?int} req.width
|
||||||
|
* @param {?int} req.height
|
||||||
|
* @param {?string} req.ext
|
||||||
|
*/
|
||||||
|
sendMedia = async (req) => {
|
||||||
|
const userClient = this.#getUser(req.mxid)
|
||||||
|
const talkChannel = await userClient.getChannel(req.channel_props)
|
||||||
|
|
||||||
|
return await talkChannel.sendMedia(req.type, {
|
||||||
|
data: Uint8Array.from(req.data),
|
||||||
|
name: req.name,
|
||||||
|
width: req.width,
|
||||||
|
height: req.height,
|
||||||
|
ext: req.ext,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#makeCommandResult(result) {
|
#makeCommandResult(result) {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
@ -529,6 +553,7 @@ export default class PeerClient {
|
||||||
get_chats: this.getChats,
|
get_chats: this.getChats,
|
||||||
list_friends: this.listFriends,
|
list_friends: this.listFriends,
|
||||||
send_message: this.sendMessage,
|
send_message: this.sendMessage,
|
||||||
|
send_media: this.sendMedia,
|
||||||
}[req.command] || this.handleUnknownCommand
|
}[req.command] || this.handleUnknownCommand
|
||||||
}
|
}
|
||||||
const resp = { id: req.id }
|
const resp = { id: req.id }
|
||||||
|
|
Loading…
Reference in New Issue