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 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)

View File

@ -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(

View File

@ -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]

View File

@ -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}")

View File

@ -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,
})
}