Outbound replies

This commit is contained in:
Andrew Ferrazzutti 2022-04-05 15:44:02 -04:00
parent 3ced968494
commit 2b9c59a2af
5 changed files with 67 additions and 105 deletions

View File

@ -17,104 +17,66 @@ from __future__ import annotations
from typing import NamedTuple from typing import NamedTuple
from mautrix.types import Format, MessageEventContent, RelationType, RoomID from mautrix.appservice import IntentAPI
from mautrix.util.formatter import ( from mautrix.types import MessageEventContent, RelationType, RoomID
EntityString,
EntityType,
MarkdownString,
MatrixParser as BaseMatrixParser,
SimpleEntity,
)
from mautrix.util.logging import TraceLogger 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 from ..db import Message as DBMessage
class SendParams(NamedTuple): class SendParams(NamedTuple):
text: str text: str
mentions: list[None] # TODO Mentions
reply_to: str reply_to: ReplyAttachment
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
async def matrix_to_kakaotalk( async def matrix_to_kakaotalk(
content: MessageEventContent, room_id: RoomID, log: TraceLogger content: MessageEventContent, room_id: RoomID, log: TraceLogger, intent: IntentAPI
) -> SendParams: ) -> SendParams:
mentions = [] # NOTE By design, this *throws* if user intent can't be matched (i.e. if a reply can't be created)
reply_to = None # TODO Mentions
if content.relates_to.rel_type == RelationType.REPLY: if content.relates_to.rel_type == RelationType.REPLY:
message = await DBMessage.get_by_mxid(content.relates_to.event_id, room_id) message = await DBMessage.get_by_mxid(content.relates_to.event_id, room_id)
if message: if not message:
content.trim_reply_fallback() raise ValueError(
reply_to = message.ktid
else:
log.warning(
f"Couldn't find reply target {content.relates_to.event_id}" 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"]: try:
parsed = await MatrixParser().parse(content["formatted_body"]) mx_event = await intent.get_event(room_id, message.mxid)
text = parsed.text except:
mentions = [] log.exception(f"Failed to find Matrix event for reply target {message.mxid}")
for mention in parsed.entities: raise
mxid = mention.extra_info["user_id"] kt_sender = pu.Puppet.get_id_from_mxid(mx_event.sender)
user = await u.User.get_by_mxid(mxid, create=False) if kt_sender is None:
if user and user.ktid: raise ValueError(
ktid = user.ktid f"Found no KakaoTalk user ID for reply target sender {mx_event.sender}"
else: )
puppet = await pu.Puppet.get_by_mxid(mxid, create=False) content.trim_reply_fallback()
if puppet: reply_to = ReplyAttachment(
ktid = puppet.ktid # TODO
else: #mentions=[],
continue # TODO What are reply URLs for?
#mentions.append( #urls=[],
# Mention(user_id=str(ktid), offset=mention.offset, length=mention.length) # 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: else:
text = content.body reply_to = None
return SendParams(text=text, mentions=mentions, reply_to=reply_to) return SendParams(text=content.body, reply_to=reply_to)

View File

@ -41,6 +41,7 @@ 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 import Chatlog, KnownChatType from ..types.chat import Chatlog, KnownChatType
from ..types.chat.attachment import ReplyAttachment
from ..types.oauth import OAuthCredential, OAuthInfo from ..types.oauth import OAuthCredential, OAuthInfo
from ..types.packet.chat.kickout import KnownKickoutType, KickoutRes from ..types.packet.chat.kickout import KnownKickoutType, KickoutRes
from ..types.request import ( from ..types.request import (
@ -273,12 +274,18 @@ class Client:
await self._rpc_client.request("get_memo_ids", mxid=self.user.mxid) 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( return await self._api_user_request_result(
Chatlog, Chatlog,
"send_chat", "send_chat",
channel_props=channel_props.serialize(), channel_props=channel_props.serialize(),
text=text, text=text,
reply_to=reply_to.serialize() if reply_to is not None else None,
) )
async def send_media( async def send_media(

View File

@ -26,7 +26,7 @@ from .mention import MentionStruct
@dataclass(kw_only=True) @dataclass(kw_only=True)
class ReplyAttachment(Attachment): class ReplyAttachment(Attachment):
attach_only: bool 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_linkId: Optional[Long] = None
src_logId: Long src_logId: Long
src_mentions: list[MentionStruct] src_mentions: list[MentionStruct]

View File

@ -46,6 +46,7 @@ from mautrix.types import (
Membership, Membership,
MessageEventContent, MessageEventContent,
MessageType, MessageType,
RelationType,
RoomID, RoomID,
TextMessageEventContent, TextMessageEventContent,
UserID, UserID,
@ -770,6 +771,8 @@ class Portal(DBPortal, BasePortal):
) -> None: ) -> None:
if message.get_edit(): if message.get_edit():
raise NotImplementedError("Edits are not supported by the KakaoTalk bridge.") 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}") sender, is_relay = await self.get_relay_sender(orig_sender, f"message {event_id}")
if not sender: if not sender:
raise Exception("not logged in") raise Exception("not logged in")
@ -802,14 +805,14 @@ class Portal(DBPortal, BasePortal):
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
) -> None: ) -> 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: try:
chatlog = await sender.client.send_message( chatlog = await sender.client.send_message(
self.channel_props, self.channel_props,
text=converted.text, text=converted.text,
# TODO # TODO
#mentions=converted.mentions, #mentions=converted.mentions,
#reply_to=converted.reply_to, reply_to=converted.reply_to,
) )
except CommandException as e: except CommandException as e:
self.log.debug(f"Error handling Matrix message {event_id}: {e!s}") self.log.debug(f"Error handling Matrix message {event_id}: {e!s}")
@ -837,18 +840,6 @@ class Portal(DBPortal, BasePortal):
else: else:
raise NotImplementedError("No file or URL specified") raise NotImplementedError("No file or URL specified")
mimetype = message.info.mimetype or magic.mimetype(data) 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 filename = message.body
width, height = None, None width, height = None, None
if message.info in (MessageType.IMAGE, MessageType.STICKER, MessageType.VIDEO): if message.info in (MessageType.IMAGE, MessageType.STICKER, MessageType.VIDEO):
@ -863,8 +854,6 @@ class Portal(DBPortal, BasePortal):
width=width, width=width,
height=height, height=height,
ext=guess_extension(mimetype)[1:], ext=guess_extension(mimetype)[1:],
# TODO
#reply_to=reply_to,
) )
except CommandException as e: except CommandException as e:
self.log.debug(f"Error uploading media for Matrix message {event_id}: {e!s}") self.log.debug(f"Error uploading media for Matrix message {event_id}: {e!s}")

View File

@ -24,8 +24,9 @@ import {
util, util,
} from "node-kakao" } from "node-kakao"
/** @typedef {import("node-kakao").OAuthCredential} OAuthCredential */ /** @typedef {import("node-kakao").OAuthCredential} OAuthCredential */
/** @typedef {import("node-kakao/dist/talk").TalkChannelList} TalkChannelList */
/** @typedef {import("node-kakao").ChannelType} ChannelType */ /** @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" import chat from "node-kakao/chat"
const { KnownChatType } = chat const { KnownChatType } = chat
@ -502,17 +503,20 @@ export default class PeerClient {
} }
/** /**
* TODO Mentions
* @param {Object} req * @param {Object} req
* @param {string} req.mxid * @param {string} req.mxid
* @param {Object} req.channel_props * @param {Object} req.channel_props
* @param {string} req.text * @param {string} req.text
* @param {?ReplyAttachment} req.reply_to
*/ */
sendChat = async (req) => { sendChat = async (req) => {
const talkChannel = await this.#getUserChannel(req.mxid, req.channel_props) const talkChannel = await this.#getUserChannel(req.mxid, req.channel_props)
return await talkChannel.sendChat({ return await talkChannel.sendChat({
type: KnownChatType.TEXT,
text: req.text, text: req.text,
type: !!req.reply_to ? KnownChatType.REPLY : KnownChatType.TEXT,
attachment: req.reply_to,
}) })
} }