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 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 ..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 .. import puppet as pu
from .. import puppet as pu, user as u
from ..db import Message as DBMessage
class SendParams(NamedTuple):
text: str
# TODO Mentions
mentions: list[MentionStruct] | None
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(
content: MessageEventContent, room_id: RoomID, log: TraceLogger, intent: IntentAPI
) -> SendParams:
# 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 not message:
@ -53,7 +87,7 @@ async def matrix_to_kakaotalk(
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)
kt_sender = await _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}"
@ -75,8 +109,36 @@ async def matrix_to_kakaotalk(
# 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),
src_userId=kt_sender,
)
else:
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.client.client_session import LoginResult
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.packet.chat.kickout import KnownKickoutType, KickoutRes
from ..types.request import (
@ -279,6 +279,7 @@ class Client:
channel_props: ChannelProps,
text: str,
reply_to: ReplyAttachment | None,
mentions: list[MentionStruct] | None,
) -> Chatlog:
return await self._api_user_request_result(
Chatlog,
@ -286,6 +287,7 @@ class Client:
channel_props=channel_props.serialize(),
text=text,
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(

View File

@ -94,15 +94,12 @@ ResultType = TypeVar("ResultType", bound=Serializable)
def ResultListType(result_type: Type[ResultType]):
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]:
return [v.serialize() for v in self]
@classmethod
def deserialize(cls, data: list[JSON]) -> "_ResultListType":
return cls(data)
return [result_type.deserialize(item) for item in data]
return _ResultListType

View File

@ -811,9 +811,8 @@ class Portal(DBPortal, BasePortal):
chatlog = await sender.client.send_message(
self.channel_props,
text=converted.text,
# TODO
#mentions=converted.mentions,
reply_to=converted.reply_to,
mentions=converted.mentions,
)
except CommandException as e:
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").ChannelType} ChannelType */
/** @typedef {import("node-kakao").ReplyAttachment} ReplyAttachment */
/** @typedef {import("node-kakao").MentionStruct} MentionStruct */
/** @typedef {import("node-kakao/dist/talk").TalkChannelList} TalkChannelList */
import chat from "node-kakao/chat"
@ -503,12 +504,12 @@ 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
* @param {?MentionStruct[]} req.mentions
*/
sendChat = async (req) => {
const talkChannel = await this.#getUserChannel(req.mxid, req.channel_props)
@ -516,7 +517,7 @@ export default class PeerClient {
return await talkChannel.sendChat({
text: req.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 {Object} req.channel_props
* @param {int} req.type
* @param {[number]} req.data
* @param {number[]} req.data
* @param {string} req.name
* @param {?int} req.width
* @param {?int} req.height