Outbound replies
This commit is contained in:
parent
3ced968494
commit
2b9c59a2af
@ -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"
|
||||||
|
)
|
||||||
|
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),
|
||||||
)
|
)
|
||||||
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:
|
else:
|
||||||
puppet = await pu.Puppet.get_by_mxid(mxid, create=False)
|
reply_to = None
|
||||||
if puppet:
|
return SendParams(text=content.body, reply_to=reply_to)
|
||||||
ktid = puppet.ktid
|
|
||||||
else:
|
|
||||||
continue
|
|
||||||
#mentions.append(
|
|
||||||
# Mention(user_id=str(ktid), offset=mention.offset, length=mention.length)
|
|
||||||
#)
|
|
||||||
else:
|
|
||||||
text = content.body
|
|
||||||
return SendParams(text=text, mentions=mentions, reply_to=reply_to)
|
|
||||||
|
@ -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(
|
||||||
|
@ -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]
|
||||||
|
@ -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}")
|
||||||
|
@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user