Outgoing mentions

This commit is contained in:
Andrew Ferrazzutti 2022-04-06 12:49:23 -04:00
parent 6867e6b349
commit 84e6a5829d
5 changed files with 79 additions and 18 deletions

View File

@ -18,29 +18,63 @@ from __future__ import annotations
from typing import NamedTuple from typing import NamedTuple
from mautrix.appservice import IntentAPI from mautrix.appservice import IntentAPI
from mautrix.types import MessageEventContent, RelationType, RoomID from mautrix.types import Format, MessageEventContent, RelationType, RoomID, UserID
from mautrix.util.formatter import (
EntityString,
EntityType,
MarkdownString,
MatrixParser,
SimpleEntity,
)
from mautrix.util.logging import TraceLogger from mautrix.util.logging import TraceLogger
from ..kt.types.bson import Long from ..kt.types.bson import Long
from ..kt.types.chat.attachment.reply import ReplyAttachment from ..kt.types.chat.attachment import ReplyAttachment, MentionStruct
from ..kt.client.types import TO_MSGTYPE_MAP from ..kt.client.types import TO_MSGTYPE_MAP
from .. import puppet as pu from .. import puppet as pu, user as u
from ..db import Message as DBMessage from ..db import Message as DBMessage
class SendParams(NamedTuple): class SendParams(NamedTuple):
text: str text: str
# TODO Mentions mentions: list[MentionStruct] | None
reply_to: ReplyAttachment reply_to: ReplyAttachment
class KakaoTalkFormatString(EntityString[SimpleEntity, EntityType], MarkdownString):
def format(self, entity_type: EntityType, **kwargs) -> KakaoTalkFormatString:
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"]},
)
)
self.text = f"@{self.text}"
return self
class ToKakaoTalkParser(MatrixParser[KakaoTalkFormatString]):
fs = KakaoTalkFormatString
async def _get_id_from_mxid(mxid: UserID) -> Long | None:
user = await u.User.get_by_mxid(mxid, create=False)
if user and user.ktid:
return user.ktid
else:
puppet = await pu.Puppet.get_by_mxid(mxid, create=False)
return puppet.ktid if puppet else None
async def matrix_to_kakaotalk( async def matrix_to_kakaotalk(
content: MessageEventContent, room_id: RoomID, log: TraceLogger, intent: IntentAPI content: MessageEventContent, room_id: RoomID, log: TraceLogger, intent: IntentAPI
) -> SendParams: ) -> SendParams:
# NOTE By design, this *throws* if user intent can't be matched (i.e. if a reply can't be created) # 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: 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 not message: if not message:
@ -53,7 +87,7 @@ async def matrix_to_kakaotalk(
except: except:
log.exception(f"Failed to find Matrix event for reply target {message.mxid}") log.exception(f"Failed to find Matrix event for reply target {message.mxid}")
raise raise
kt_sender = pu.Puppet.get_id_from_mxid(mx_event.sender) kt_sender = await _get_id_from_mxid(mx_event.sender)
if kt_sender is None: if kt_sender is None:
raise ValueError( raise ValueError(
f"Found no KakaoTalk user ID for reply target sender {mx_event.sender}" f"Found no KakaoTalk user ID for reply target sender {mx_event.sender}"
@ -75,8 +109,36 @@ async def matrix_to_kakaotalk(
# TODO Check if source message needs to be formatted # TODO Check if source message needs to be formatted
src_message=mx_event.content.body, src_message=mx_event.content.body,
src_type=TO_MSGTYPE_MAP[mx_event.content.msgtype], src_type=TO_MSGTYPE_MAP[mx_event.content.msgtype],
src_userId=Long(kt_sender), src_userId=kt_sender,
) )
else: else:
reply_to = None reply_to = None
return SendParams(text=content.body, reply_to=reply_to) if content.get("format", None) == Format.HTML and content["formatted_body"]:
parsed = await ToKakaoTalkParser().parse(content["formatted_body"])
text = parsed.text
mentions_by_user: dict[Long, MentionStruct] = {}
# Make sure to not create remote mentions for any remote user not in the room
if parsed.entities:
joined_members = set(await intent.get_room_members(room_id))
last_offset = 0
at = 0
for mention in sorted(parsed.entities, key=lambda entity: entity.offset):
mxid = mention.extra_info["user_id"]
if mxid not in joined_members:
continue
ktid = await _get_id_from_mxid(mxid)
if ktid is None:
continue
at += text[last_offset:mention.offset+1].count("@")
last_offset = mention.offset+1
mention = mentions_by_user.setdefault(ktid, MentionStruct(
at=[],
len=mention.length,
user_id=ktid,
))
mention.at.append(at)
mentions = list(mentions_by_user.values())
else:
text = content.body
mentions = None
return SendParams(text=text, mentions=mentions, reply_to=reply_to)

View File

@ -41,7 +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.chat.attachment import MentionStruct, 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 (
@ -279,6 +279,7 @@ class Client:
channel_props: ChannelProps, channel_props: ChannelProps,
text: str, text: str,
reply_to: ReplyAttachment | None, reply_to: ReplyAttachment | None,
mentions: list[MentionStruct] | None,
) -> Chatlog: ) -> Chatlog:
return await self._api_user_request_result( return await self._api_user_request_result(
Chatlog, Chatlog,
@ -286,6 +287,7 @@ class Client:
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, reply_to=reply_to.serialize() if reply_to is not None else None,
mentions=[m.serialize() for m in mentions] if mentions is not None else None,
) )
async def send_media( async def send_media(

View File

@ -94,15 +94,12 @@ ResultType = TypeVar("ResultType", bound=Serializable)
def ResultListType(result_type: Type[ResultType]): def ResultListType(result_type: Type[ResultType]):
class _ResultListType(list[result_type], Serializable): class _ResultListType(list[result_type], Serializable):
def __init__(self, iterable: Iterable[result_type]=()):
list.__init__(self, (result_type.deserialize(x) for x in iterable))
def serialize(self) -> list[JSON]: def serialize(self) -> list[JSON]:
return [v.serialize() for v in self] return [v.serialize() for v in self]
@classmethod @classmethod
def deserialize(cls, data: list[JSON]) -> "_ResultListType": def deserialize(cls, data: list[JSON]) -> "_ResultListType":
return cls(data) return [result_type.deserialize(item) for item in data]
return _ResultListType return _ResultListType

View File

@ -811,9 +811,8 @@ class Portal(DBPortal, BasePortal):
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
#mentions=converted.mentions,
reply_to=converted.reply_to, reply_to=converted.reply_to,
mentions=converted.mentions,
) )
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}")

View File

@ -26,6 +26,7 @@ import {
/** @typedef {import("node-kakao").OAuthCredential} OAuthCredential */ /** @typedef {import("node-kakao").OAuthCredential} OAuthCredential */
/** @typedef {import("node-kakao").ChannelType} ChannelType */ /** @typedef {import("node-kakao").ChannelType} ChannelType */
/** @typedef {import("node-kakao").ReplyAttachment} ReplyAttachment */ /** @typedef {import("node-kakao").ReplyAttachment} ReplyAttachment */
/** @typedef {import("node-kakao").MentionStruct} MentionStruct */
/** @typedef {import("node-kakao/dist/talk").TalkChannelList} TalkChannelList */ /** @typedef {import("node-kakao/dist/talk").TalkChannelList} TalkChannelList */
import chat from "node-kakao/chat" import chat from "node-kakao/chat"
@ -503,12 +504,12 @@ 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 * @param {?ReplyAttachment} req.reply_to
* @param {?MentionStruct[]} req.mentions
*/ */
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)
@ -516,7 +517,7 @@ export default class PeerClient {
return await talkChannel.sendChat({ return await talkChannel.sendChat({
text: req.text, text: req.text,
type: !!req.reply_to ? KnownChatType.REPLY : KnownChatType.TEXT, type: !!req.reply_to ? KnownChatType.REPLY : KnownChatType.TEXT,
attachment: req.reply_to, attachment: !req.mentions ? req.reply_to : {...req.reply_to, mentions: req.mentions},
}) })
} }
@ -525,7 +526,7 @@ export default class PeerClient {
* @param {string} req.mxid * @param {string} req.mxid
* @param {Object} req.channel_props * @param {Object} req.channel_props
* @param {int} req.type * @param {int} req.type
* @param {[number]} req.data * @param {number[]} req.data
* @param {string} req.name * @param {string} req.name
* @param {?int} req.width * @param {?int} req.width
* @param {?int} req.height * @param {?int} req.height