Outgoing mentions
This commit is contained in:
parent
6867e6b349
commit
84e6a5829d
@ -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)
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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}")
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user