From a9633118c5c97aa5244c708d61214ded6fe1930f Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Mon, 4 Apr 2022 20:07:30 -0400 Subject: [PATCH] Allow syncing (existing) direct chat channel by inviting puppet to DM --- .../kt/client/client.py | 18 ++++++++- matrix_appservice_kakaotalk/portal.py | 40 ++++++++----------- matrix_appservice_kakaotalk/user.py | 34 ++++++++++------ node/src/client.js | 34 +++++++++++++++- 4 files changed, 87 insertions(+), 39 deletions(-) diff --git a/matrix_appservice_kakaotalk/kt/client/client.py b/matrix_appservice_kakaotalk/kt/client/client.py index f219e50..c12d744 100644 --- a/matrix_appservice_kakaotalk/kt/client/client.py +++ b/matrix_appservice_kakaotalk/kt/client/client.py @@ -53,7 +53,7 @@ from ..types.request import ( from .types import PortalChannelInfo, UserInfoUnion, ChannelProps -from .errors import InvalidAccessToken +from .errors import InvalidAccessToken, CommandException from .error_helper import raise_unsuccessful_response try: @@ -257,6 +257,22 @@ class Client: "list_friends", ) + async def get_friend_dm_id(self, friend_id: Long) -> Long | None: + try: + return await self._api_user_request_result( + Long, + "get_friend_dm_id", + friend_id=friend_id.serialize(), + ) + except CommandException: + self.log.exception(f"Could not find friend with ID {friend_id}") + return None + + async def get_memo_ids(self) -> list[Long]: + return ResultListType(Long).deserialize( + await self._rpc_client.request("get_memo_ids", mxid=self.user.mxid) + ) + async def send_message(self, channel_props: ChannelProps, text: str) -> Chatlog: return await self._api_user_request_result( Chatlog, diff --git a/matrix_appservice_kakaotalk/portal.py b/matrix_appservice_kakaotalk/portal.py index d31abd8..6f035d7 100644 --- a/matrix_appservice_kakaotalk/portal.py +++ b/matrix_appservice_kakaotalk/portal.py @@ -122,7 +122,7 @@ class Portal(DBPortal, BasePortal): config: Config _main_intent: IntentAPI | None - _kt_sender: int | None + _kt_sender: Long | None _create_room_lock: asyncio.Lock _send_locks: dict[int, asyncio.Lock] _noop_lock: FakeLock = FakeLock() @@ -218,7 +218,7 @@ class Portal(DBPortal, BasePortal): if self.mxid: await DBMessage.delete_all_by_room(self.mxid) self.by_mxid.pop(self.mxid, None) - self.by_ktid.pop(self._ktid_full, None) + self.by_ktid.pop(self.ktid_full, None) self.mxid = None self.name_set = False self.avatar_set = False @@ -230,7 +230,7 @@ class Portal(DBPortal, BasePortal): # region Properties @property - def _ktid_full(self) -> tuple[int, int]: + def ktid_full(self) -> tuple[Long, Long]: return self.ktid, self.kt_receiver @property @@ -263,11 +263,7 @@ class Portal(DBPortal, BasePortal): @property def main_intent(self) -> IntentAPI: if not self._main_intent: - raise ValueError( - "Portal must be postinit()ed before main_intent can be used" - if not self.is_direct else - "Direct chat portal must call postinit and _update_participants before main_intent can be used" - ) + raise ValueError("Portal must be postinit()ed before main_intent can be used") return self._main_intent async def get_dm_puppet(self) -> p.Puppet | None: @@ -485,7 +481,7 @@ class Portal(DBPortal, BasePortal): self, source: u.User, participant: UserInfoUnion ) -> bool: # TODO nick map? - self.log.trace("Syncing participant %s", participant.id) + self.log.trace(f"Syncing participant {participant.userId}") puppet = await p.Puppet.get_by_ktid(participant.userId) await puppet.update_info_from_participant(source, participant) changed = False @@ -504,14 +500,6 @@ class Portal(DBPortal, BasePortal): if participants is None: self.log.debug("Called _update_participants with no participants, fetching them now...") participants = await source.client.get_participants(self.channel_props) - if not self._main_intent: - assert self.is_direct, "_main_intent for non-direct chat portal should have been set already" - self._kt_sender = participants[ - 0 if self.kt_type == KnownChannelType.MemoChat or participants[0].userId != source.ktid else 1 - ].userId - self._main_intent = (await self.get_dm_puppet()).default_mxid_intent - else: - self._kt_sender = (await p.Puppet.get_by_mxid(self._main_intent.mxid)).ktid if self.is_direct else None sync_tasks = [ self._update_participant(source, pcp) for pcp in participants ] @@ -1277,21 +1265,25 @@ class Portal(DBPortal, BasePortal): # region Database getters async def postinit(self) -> None: - self.by_ktid[self._ktid_full] = self + self.by_ktid[self.ktid_full] = self if self.mxid: self.by_mxid[self.mxid] = self if not self.is_direct: self._main_intent = self.az.intent - elif self.mxid: + else: # TODO Save kt_sender in DB instead? Depends on if DM channels are shared... user = await u.User.get_by_ktid(self.kt_receiver) assert user, f"Found no user for this portal's receiver of {self.kt_receiver}" - if user.is_connected: - await self._update_participants(user) + if self.kt_type == KnownChannelType.MemoChat: + self._kt_sender = user.ktid else: - self.log.debug(f"Not setting _main_intent of new direct chat for disconnected user {user.ktid}") - else: - self.log.debug("Not setting _main_intent of new direct chat until after checking participant list") + # NOTE This throws if the user isn't connected--good! + # Nothing should init a portal for a disconnected user. + participants = await user.client.get_participants(self.channel_props) + self._kt_sender = participants[ + 0 if participants[0].userId != user.ktid else 1 + ].userId + self._main_intent = (await p.Puppet.get_by_ktid(self._kt_sender)).default_mxid_intent @classmethod @async_getter_lock diff --git a/matrix_appservice_kakaotalk/user.py b/matrix_appservice_kakaotalk/user.py index d0e2b49..34449ff 100644 --- a/matrix_appservice_kakaotalk/user.py +++ b/matrix_appservice_kakaotalk/user.py @@ -40,7 +40,7 @@ from .kt.client.errors import AuthenticationRequired, ResponseError from .kt.types.api.struct.profile import ProfileStruct from .kt.types.bson import Long from .kt.types.channel.channel_info import ChannelInfo, NormalChannelInfo, NormalChannelData -from .kt.types.channel.channel_type import ChannelType +from .kt.types.channel.channel_type import ChannelType, KnownChannelType from .kt.types.chat.chat import Chatlog from .kt.types.client.client_session import LoginDataItem, LoginResult from .kt.types.oauth import OAuthCredential @@ -556,18 +556,28 @@ class User(DBUser, BaseUser): return await pu.Puppet.get_by_ktid(self.ktid) async def get_portal_with(self, puppet: pu.Puppet, create: bool = True) -> po.Portal | None: - # TODO - return None - """ - if not self.ktid or not self.client: + # TODO Make upstream request to return custom failure message + if not self.ktid or not self.is_connected: + return None + if puppet.ktid != self.ktid: + kt_type = KnownChannelType.DirectChat + ktid = await self.client.get_friend_dm_id(puppet.ktid) + else: + kt_type = KnownChannelType.MemoChat + memo_ids = await self.client.get_memo_ids() + if not memo_ids: + ktid = Long(0) + else: + ktid = memo_ids[0] + if len(memo_ids) > 1: + self.log.info("Found multiple memo chats, so using the first one as a fallback") + if ktid: + return await po.Portal.get_by_ktid( + ktid, kt_receiver=self.ktid, create=create, kt_type=kt_type + ) + else: + self.log.warning(f"Didn't find an existing DM channel with KakaoTalk user {puppet.ktid}, so not creating one") return None - return await po.Portal.get_by_ktid( - await self.client.get_dm_channel_id_for(puppet.ktid), - kt_receiver=self.ktid, - create=create, - kt_type=KnownChannelType.DirectChat if puppet.ktid != self.ktid else KnownChannelType.MemoChat - ) - """ # region KakaoTalk event handling diff --git a/node/src/client.js b/node/src/client.js index 50832a3..61ac6e7 100644 --- a/node/src/client.js +++ b/node/src/client.js @@ -464,13 +464,41 @@ export default class PeerClient { /** * @param {Object} req - * @param {?string} req.mxid - * @param {?OAuthCredential} req.oauth_credential + * @param {string} req.mxid */ listFriends = async (req) => { return await this.#getUser(req.mxid).serviceClient.requestFriendList() } + /** + * @param {Object} req + * @param {string} req.mxid The user whose friend is being looked up. + * @param {string} req.friend_id The friend to search for. + * @param {string} propertyName The property to retrieve from the specified friend. + */ + getFriendProperty = async (req, propertyName) => { + const res = await this.#getUser(req.mxid).serviceClient.findFriendById(req.friend_id) + if (!res.success) return res + + return this.#makeCommandResult(res.result.friend[propertyName]) + } + + /** + * @param {Object} req + * @param {string} req.mxid + */ + getMemoIds = (req) => { + /** @type Long[] */ + const channelIds = [] + const channelList = this.#getUser(req.mxid).talkClient.channelList + for (const channel of channelList.all()) { + if (channel.info.type == "MemoChat") { + channelIds.push(channel.channelId) + } + } + return channelIds + } + /** * @param {Object} req * @param {string} req.mxid @@ -590,6 +618,8 @@ export default class PeerClient { get_participants: this.getParticipants, get_chats: this.getChats, list_friends: this.listFriends, + get_friend_dm_id: req => this.getFriendProperty(req, "directChatId"), + get_memo_ids: this.getMemoIds, send_message: this.sendMessage, send_media: this.sendMedia, }[req.command] || this.handleUnknownCommand