diff --git a/matrix_appservice_kakaotalk/formatter/from_matrix.py b/matrix_appservice_kakaotalk/formatter/from_matrix.py index 0a1b0d6..94ad91f 100644 --- a/matrix_appservice_kakaotalk/formatter/from_matrix.py +++ b/matrix_appservice_kakaotalk/formatter/from_matrix.py @@ -17,104 +17,66 @@ from __future__ import annotations from typing import NamedTuple -from mautrix.types import Format, MessageEventContent, RelationType, RoomID -from mautrix.util.formatter import ( - EntityString, - EntityType, - MarkdownString, - MatrixParser as BaseMatrixParser, - SimpleEntity, -) +from mautrix.appservice import IntentAPI +from mautrix.types import MessageEventContent, RelationType, RoomID from mautrix.util.logging import TraceLogger -from .. import puppet as pu, user as u +from ..kt.types.bson import Long +from ..kt.types.chat.attachment.reply import ReplyAttachment + +from ..kt.client.types import TO_MSGTYPE_MAP + +from .. import puppet as pu from ..db import Message as DBMessage class SendParams(NamedTuple): text: str - mentions: list[None] - reply_to: str - - -class FacebookFormatString(EntityString[SimpleEntity, EntityType], MarkdownString): - def format(self, entity_type: EntityType, **kwargs) -> FacebookFormatString: - prefix = suffix = "" - if entity_type == EntityType.USER_MENTION: - self.entities.append( - SimpleEntity( - type=entity_type, - offset=0, - length=len(self.text), - extra_info={"user_id": kwargs["user_id"]}, - ) - ) - return self - elif entity_type == EntityType.BOLD: - prefix = suffix = "*" - elif entity_type == EntityType.ITALIC: - prefix = suffix = "_" - elif entity_type == EntityType.STRIKETHROUGH: - prefix = suffix = "~" - elif entity_type == EntityType.URL: - if kwargs["url"] != self.text: - suffix = f" ({kwargs['url']})" - elif entity_type == EntityType.PREFORMATTED: - prefix = f"```{kwargs['language']}\n" - suffix = "\n```" - elif entity_type == EntityType.INLINE_CODE: - prefix = suffix = "`" - elif entity_type == EntityType.BLOCKQUOTE: - children = self.trim().split("\n") - children = [child.prepend("> ") for child in children] - return self.join(children, "\n") - elif entity_type == EntityType.HEADER: - prefix = "#" * kwargs["size"] + " " - else: - return self - - self._offset_entities(len(prefix)) - self.text = f"{prefix}{self.text}{suffix}" - return self - - -class MatrixParser(BaseMatrixParser[FacebookFormatString]): - fs = FacebookFormatString + # TODO Mentions + reply_to: ReplyAttachment async def matrix_to_kakaotalk( - content: MessageEventContent, room_id: RoomID, log: TraceLogger + content: MessageEventContent, room_id: RoomID, log: TraceLogger, intent: IntentAPI ) -> SendParams: - mentions = [] - reply_to = None + # NOTE By design, this *throws* if user intent can't be matched (i.e. if a reply can't be created) + # TODO Mentions if content.relates_to.rel_type == RelationType.REPLY: message = await DBMessage.get_by_mxid(content.relates_to.event_id, room_id) - if message: - content.trim_reply_fallback() - reply_to = message.ktid - else: - log.warning( + if not message: + raise ValueError( f"Couldn't find reply target {content.relates_to.event_id}" - " to bridge text message reply metadata to Facebook" + " to bridge text message reply metadata to KakaoTalk" ) - if content.get("format", None) == Format.HTML and content["formatted_body"]: - parsed = await MatrixParser().parse(content["formatted_body"]) - text = parsed.text - mentions = [] - for mention in parsed.entities: - mxid = mention.extra_info["user_id"] - user = await u.User.get_by_mxid(mxid, create=False) - if user and user.ktid: - ktid = user.ktid - else: - puppet = await pu.Puppet.get_by_mxid(mxid, create=False) - if puppet: - ktid = puppet.ktid - else: - continue - #mentions.append( - # Mention(user_id=str(ktid), offset=mention.offset, length=mention.length) - #) + try: + mx_event = await intent.get_event(room_id, message.mxid) + except: + log.exception(f"Failed to find Matrix event for reply target {message.mxid}") + raise + kt_sender = pu.Puppet.get_id_from_mxid(mx_event.sender) + if kt_sender is None: + raise ValueError( + f"Found no KakaoTalk user ID for reply target sender {mx_event.sender}" + ) + content.trim_reply_fallback() + reply_to = ReplyAttachment( + # TODO + #mentions=[], + # TODO What are reply URLs for? + #urls=[], + # TODO Set this for emoticon reply, but must first support those + attach_only=False, + # TODO If replying with media works, must set type AND all attachment properties + # But then, the reply object must be an intersection of a ReplyAttachment and something else + #attach_type=TO_MSGTYPE_MAP.get(content.msgtype), + src_logId=message.ktid, + # TODO + src_mentions=[], + # TODO Check if source message needs to be formatted + src_message=mx_event.content.body, + src_type=TO_MSGTYPE_MAP[mx_event.content.msgtype], + src_userId=Long(kt_sender), + ) else: - text = content.body - return SendParams(text=text, mentions=mentions, reply_to=reply_to) + reply_to = None + return SendParams(text=content.body, reply_to=reply_to) diff --git a/matrix_appservice_kakaotalk/kt/client/client.py b/matrix_appservice_kakaotalk/kt/client/client.py index 701797a..55ac9cc 100644 --- a/matrix_appservice_kakaotalk/kt/client/client.py +++ b/matrix_appservice_kakaotalk/kt/client/client.py @@ -41,6 +41,7 @@ from ..types.api.struct import FriendListStruct from ..types.bson import Long from ..types.client.client_session import LoginResult from ..types.chat import Chatlog, KnownChatType +from ..types.chat.attachment import ReplyAttachment from ..types.oauth import OAuthCredential, OAuthInfo from ..types.packet.chat.kickout import KnownKickoutType, KickoutRes from ..types.request import ( @@ -273,12 +274,18 @@ class Client: await self._rpc_client.request("get_memo_ids", mxid=self.user.mxid) ) - async def send_message(self, channel_props: ChannelProps, text: str) -> Chatlog: + async def send_message( + self, + channel_props: ChannelProps, + text: str, + reply_to: ReplyAttachment | None, + ) -> Chatlog: return await self._api_user_request_result( Chatlog, "send_chat", channel_props=channel_props.serialize(), text=text, + reply_to=reply_to.serialize() if reply_to is not None else None, ) async def send_media( diff --git a/matrix_appservice_kakaotalk/kt/types/chat/attachment/reply.py b/matrix_appservice_kakaotalk/kt/types/chat/attachment/reply.py index dbb9993..5f861c2 100644 --- a/matrix_appservice_kakaotalk/kt/types/chat/attachment/reply.py +++ b/matrix_appservice_kakaotalk/kt/types/chat/attachment/reply.py @@ -26,7 +26,7 @@ from .mention import MentionStruct @dataclass(kw_only=True) class ReplyAttachment(Attachment): attach_only: bool - attach_type: int + attach_type: Optional[ChatType] = None # NOTE Changed from int for outgoing typeless replies src_linkId: Optional[Long] = None src_logId: Long src_mentions: list[MentionStruct] diff --git a/matrix_appservice_kakaotalk/portal.py b/matrix_appservice_kakaotalk/portal.py index 05623fb..b627eb7 100644 --- a/matrix_appservice_kakaotalk/portal.py +++ b/matrix_appservice_kakaotalk/portal.py @@ -46,6 +46,7 @@ from mautrix.types import ( Membership, MessageEventContent, MessageType, + RelationType, RoomID, TextMessageEventContent, UserID, @@ -770,6 +771,8 @@ class Portal(DBPortal, BasePortal): ) -> None: if message.get_edit(): raise NotImplementedError("Edits are not supported by the KakaoTalk bridge.") + if message.relates_to.rel_type == RelationType.REPLY and not message.msgtype.is_text: + raise NotImplementedError("Replying with non-text content is not supported by the KakaoTalk bridge.") sender, is_relay = await self.get_relay_sender(orig_sender, f"message {event_id}") if not sender: raise Exception("not logged in") @@ -802,14 +805,14 @@ class Portal(DBPortal, BasePortal): async def _handle_matrix_text( self, event_id: EventID, sender: u.User, message: TextMessageEventContent ) -> None: - converted = await matrix_to_kakaotalk(message, self.mxid, self.log) + converted = await matrix_to_kakaotalk(message, self.mxid, self.log, self.main_intent) try: chatlog = await sender.client.send_message( self.channel_props, text=converted.text, # TODO #mentions=converted.mentions, - #reply_to=converted.reply_to, + reply_to=converted.reply_to, ) except CommandException as e: self.log.debug(f"Error handling Matrix message {event_id}: {e!s}") @@ -837,18 +840,6 @@ class Portal(DBPortal, BasePortal): else: raise NotImplementedError("No file or URL specified") mimetype = 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.ktid - 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 if message.info in (MessageType.IMAGE, MessageType.STICKER, MessageType.VIDEO): @@ -863,8 +854,6 @@ class Portal(DBPortal, BasePortal): width=width, height=height, ext=guess_extension(mimetype)[1:], - # TODO - #reply_to=reply_to, ) except CommandException as e: self.log.debug(f"Error uploading media for Matrix message {event_id}: {e!s}") diff --git a/node/src/client.js b/node/src/client.js index 799d0d1..b214eaa 100644 --- a/node/src/client.js +++ b/node/src/client.js @@ -24,8 +24,9 @@ import { util, } from "node-kakao" /** @typedef {import("node-kakao").OAuthCredential} OAuthCredential */ -/** @typedef {import("node-kakao/dist/talk").TalkChannelList} TalkChannelList */ /** @typedef {import("node-kakao").ChannelType} ChannelType */ +/** @typedef {import("node-kakao").ReplyAttachment} ReplyAttachment */ +/** @typedef {import("node-kakao/dist/talk").TalkChannelList} TalkChannelList */ import chat from "node-kakao/chat" const { KnownChatType } = chat @@ -502,17 +503,20 @@ export default class PeerClient { } /** + * TODO Mentions * @param {Object} req * @param {string} req.mxid * @param {Object} req.channel_props * @param {string} req.text + * @param {?ReplyAttachment} req.reply_to */ sendChat = async (req) => { const talkChannel = await this.#getUserChannel(req.mxid, req.channel_props) return await talkChannel.sendChat({ - type: KnownChatType.TEXT, text: req.text, + type: !!req.reply_to ? KnownChatType.REPLY : KnownChatType.TEXT, + attachment: req.reply_to, }) }