From 9a33f3dcf21a734fade0fea9ffa0995554eb6091 Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Sun, 10 Apr 2022 22:56:51 -0400 Subject: [PATCH] Use More/LessSettings instead of profile on login; add whoami command --- matrix_appservice_kakaotalk/commands/conn.py | 29 +++- .../kt/client/client.py | 21 ++- .../kt/client/types.py | 7 + .../kt/types/api/struct/__init__.py | 2 +- .../kt/types/api/struct/account.py | 141 ++++++++++++++++++ matrix_appservice_kakaotalk/user.py | 17 ++- node/src/client.js | 63 ++++++-- .../matrix-appservice-kakaotalk-node.service | 18 +++ systemd/matrix-appservice-kakaotalk.service | 16 ++ 9 files changed, 282 insertions(+), 32 deletions(-) create mode 100644 matrix_appservice_kakaotalk/kt/types/api/struct/account.py create mode 100644 systemd/matrix-appservice-kakaotalk-node.service create mode 100644 systemd/matrix-appservice-kakaotalk.service diff --git a/matrix_appservice_kakaotalk/commands/conn.py b/matrix_appservice_kakaotalk/commands/conn.py index db78e12..2799c19 100644 --- a/matrix_appservice_kakaotalk/commands/conn.py +++ b/matrix_appservice_kakaotalk/commands/conn.py @@ -14,6 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from mautrix.bridge.commands import HelpSection, command_handler +from mautrix.types import SerializerError from .typehint import CommandEvent @@ -53,24 +54,36 @@ async def disconnect(evt: CommandEvent) -> None: needs_auth=True, management_only=True, help_section=SECTION_CONNECTION, - help_text="Check if you're logged into KakaoTalk & connected to chats", + help_text="Check if you're logged into KakaoTalk and retrieve your account information", ) -async def ping(evt: CommandEvent) -> None: - if not await evt.sender.is_logged_in(): - await evt.reply("You're not logged into KakaoTalk") - return +async def whoami(evt: CommandEvent) -> None: await evt.mark_read() try: own_info = await evt.sender.get_own_info() await evt.reply( - f"You're logged in as {own_info.nickname} (user ID {evt.sender.ktid})." - "\n\n" - f"You are {'connected to' if evt.sender.is_connected else '**disconnected** from'} KakaoTalk chats.\n\n" + f"You're logged in as `{own_info.more.uuid}` (nickname: {own_info.more.nickName}, user ID: {evt.sender.ktid})." + ) + except SerializerError: + evt.sender.log.exception("Failed to deserialize settings struct") + await evt.reply( + f"You're logged in, but the bridge is unable to retrieve your profile information (user ID: {evt.sender.ktid})." ) except CommandException as e: await evt.reply(f"Error from KakaoTalk: {e}") +@command_handler( + needs_auth=True, + management_only=True, + help_section=SECTION_CONNECTION, + help_text="Check if you're connected to KakaoTalk chats", +) +async def ping(evt: CommandEvent) -> None: + await evt.reply( + f"You are {'connected to' if evt.sender.is_connected else '**disconnected** from'} KakaoTalk chats." + ) + + @command_handler( needs_auth=True, management_only=True, diff --git a/matrix_appservice_kakaotalk/kt/client/client.py b/matrix_appservice_kakaotalk/kt/client/client.py index b1511e9..0267cc5 100644 --- a/matrix_appservice_kakaotalk/kt/client/client.py +++ b/matrix_appservice_kakaotalk/kt/client/client.py @@ -32,6 +32,7 @@ from aiohttp import ClientSession from aiohttp.client import _RequestContextManager from yarl import URL +from mautrix.types import SerializerError from mautrix.util.logging import TraceLogger from ...config import Config @@ -55,7 +56,12 @@ from ..types.request import ( CommandResultDoneValue ) -from .types import PortalChannelInfo, UserInfoUnion, ChannelProps +from .types import ( + ChannelProps, + PortalChannelInfo, + SettingsStruct, + UserInfoUnion, +) from .errors import InvalidAccessToken, CommandException from .error_helper import raise_unsuccessful_response @@ -197,17 +203,21 @@ class Client: # region post-token commands - async def start(self) -> ProfileStruct: + async def start(self) -> SettingsStruct | None: """ Initialize user-specific bridging & state by providing a token obtained from a prior login. Receive the user's profile info in response. """ - profile_req_struct = await self._api_user_request_result(ProfileReqStruct, "start") + try: + settings_struct = await self._api_user_request_result(SettingsStruct, "start") + except SerializerError: + self.log.exception("Unable to deserialize settings struct, but starting client anyways") + settings_struct = None if not self._rpc_disconnection_task: self._rpc_disconnection_task = asyncio.create_task(self._rpc_disconnection_handler()) else: self.log.warning("Called \"start\" on an already-started client") - return profile_req_struct.profile + return settings_struct async def stop(self) -> None: """Immediately stop bridging this user.""" @@ -246,6 +256,9 @@ class Client: await self._rpc_client.request("disconnect", mxid=self.user.mxid) await self._on_disconnect(None) + async def get_settings(self) -> SettingsStruct: + return await self._api_user_request_result(SettingsStruct, "get_settings") + async def get_own_profile(self) -> ProfileStruct: profile_req_struct = await self._api_user_request_result(ProfileReqStruct, "get_own_profile") return profile_req_struct.profile diff --git a/matrix_appservice_kakaotalk/kt/client/types.py b/matrix_appservice_kakaotalk/kt/client/types.py index b00ba94..6b2cb91 100644 --- a/matrix_appservice_kakaotalk/kt/client/types.py +++ b/matrix_appservice_kakaotalk/kt/client/types.py @@ -26,6 +26,7 @@ from mautrix.types import ( MessageType, ) +from ..types.api.struct import MoreSettingsStruct, LessSettingsStruct from ..types.bson import Long from ..types.channel.channel_info import NormalChannelInfo from ..types.channel.channel_type import ChannelType @@ -34,6 +35,12 @@ from ..types.openlink.open_channel_info import OpenChannelInfo from ..types.user.channel_user_info import NormalChannelUserInfo, OpenChannelUserInfo +@dataclass +class SettingsStruct(SerializableAttrs): + more: MoreSettingsStruct + less: LessSettingsStruct + + ChannelInfoUnion = NewType("ChannelInfoUnion", Union[NormalChannelInfo, OpenChannelInfo]) @deserializer(ChannelInfoUnion) diff --git a/matrix_appservice_kakaotalk/kt/types/api/struct/__init__.py b/matrix_appservice_kakaotalk/kt/types/api/struct/__init__.py index eb92a23..7ae6988 100644 --- a/matrix_appservice_kakaotalk/kt/types/api/struct/__init__.py +++ b/matrix_appservice_kakaotalk/kt/types/api/struct/__init__.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . ##from .login import * -##from .account import * +from .account import * from .profile import * from .friends import * ##from .openlink import * diff --git a/matrix_appservice_kakaotalk/kt/types/api/struct/account.py b/matrix_appservice_kakaotalk/kt/types/api/struct/account.py new file mode 100644 index 0000000..3e879e0 --- /dev/null +++ b/matrix_appservice_kakaotalk/kt/types/api/struct/account.py @@ -0,0 +1,141 @@ +# matrix-appservice-kakaotalk - A Matrix-KakaoTalk puppeting bridge. +# Copyright (C) 2022 Tulir Asokan, Andrew Ferrazzutti +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from typing import Optional, Union + +from attr import dataclass + +from mautrix.types import SerializableAttrs + +from ...bson import Long + + +@dataclass +class OpenChatSettingsStruct(SerializableAttrs): + chatMemberMaxJoin: int + chatRoomMaxJoin: int + createLinkLimit: 10; + createCardLinkLimit: 3; + numOfStaffLimit: 5; + rewritable: bool + handoverEnabled: bool + + +@dataclass(kw_only=True) +class MoreSettingsStruct(SerializableAttrs): + since: int + + @dataclass + class ClientConf(SerializableAttrs): + osVersion: str + clientConf: ClientConf + + available: int + available2: int + friendsPollingInterval: Optional[int] = None # NOTE Made optional + settingsPollingInterval: Optional[int] = None # NOTE Made optional + profilePollingInterval: Optional[int] = None # NOTE Made optional + moreListPollingInterval: Optional[int] = None # NOTE Made optional + morePayPollingInterval: Optional[int] = None # NOTE Made optional + daumMediaPollingInterval: Optional[int] = None # NOTE Made optional + lessSettingsPollingInterval: Optional[int] = None # NOTE Made optional + + @dataclass + class MoreApps(SerializableAttrs): + recommend: Optional[list[str]] = None # NOTE From unknown[] + all: Optional[list[str]] = None # NOTE From unknown[] + moreApps: MoreApps + + shortcuts: Optional[dict[str, int]] = None # NOTE Made optional + seasonProfileRev: int + seasonNoticeRev: int + serviceUserId: Union[Long, int] + accountId: int + accountDisplayId: str + hashedAccountId: str + pstnNumber: str + formattedPstnNumber: str + nsnNumber: str + formattedNsnNumber: str + contactNameSync: int + allowMigration: bool + emailStatus: int + emailAddress: str + emailVerified: bool + uuid: str + uuidSearchable: bool + nickName: str + openChat: OpenChatSettingsStruct + profileImageUrl: str + fullProfileImageUrl: str + originalProfileImageUrl: str + statusMessage: str + + +@dataclass(kw_only=True) +class LessSettingsStruct(SerializableAttrs): + kakaoAutoLoginDomain: list[str] + daumSsoDomain: list[str] + + @dataclass + class GoogleMapsApi(SerializableAttrs): + key: str + signature: str + googleMapsApi: GoogleMapsApi + + @dataclass + class ChatReportLimit(SerializableAttrs): + chat: int + open_chat: int + plus_chat: int + chat_report_limit: ChatReportLimit + + externalApiList: str # NOTE From unknown + + @dataclass + class BirthdayFriends(SerializableAttrs): + landing_url: str + birthday_friends: BirthdayFriends + + messageDeleteTime: Optional[int] = None # NOTE Made optional + + @dataclass + class VoiceTalk(SerializableAttrs): + groupCallMaxParticipants: int + voiceTalk: VoiceTalk + + profileActions: bool + + @dataclass + class PostExpirationSetting(SerializableAttrs): + flagOn: bool + newPostTerm: int + postExpirationSetting: PostExpirationSetting + + kakaoAlertIds: list[int] + + +@dataclass +class LoginTokenStruct(SerializableAttrs): + token: str + expires: int + + +___all___ = [ + "OpenChatSettingsStruct", + "MoreSettingsStruct", + "LessSettingsStruct", + "LoginTokenStruct", +] \ No newline at end of file diff --git a/matrix_appservice_kakaotalk/user.py b/matrix_appservice_kakaotalk/user.py index 54e4368..2dfb4dc 100644 --- a/matrix_appservice_kakaotalk/user.py +++ b/matrix_appservice_kakaotalk/user.py @@ -25,6 +25,7 @@ from mautrix.types import ( JSON, MessageType, RoomID, + SerializerError, TextMessageEventContent, UserID, ) @@ -38,7 +39,7 @@ from .db import User as DBUser from .kt.client import Client from .kt.client.errors import AuthenticationRequired, ResponseError -from .kt.types.api.struct.profile import ProfileStruct +from .kt.client.types import SettingsStruct from .kt.types.bson import Long from .kt.types.channel.channel_info import ChannelInfo, NormalChannelInfo, NormalChannelData from .kt.types.channel.channel_type import ChannelType, KnownChannelType @@ -96,7 +97,7 @@ class User(DBUser, BaseUser): _db_instance: DBUser | None _sync_lock: SimpleLock _is_rpc_reconnecting: bool - _logged_in_info: ProfileStruct | None + _logged_in_info: SettingsStruct | None _logged_in_info_time: float def __init__( @@ -253,9 +254,9 @@ class User(DBUser, BaseUser): self.log.warning(f"UUID mismatch: expected {self.uuid}, got {oauth_credential.deviceUUID}") self.uuid = oauth_credential.deviceUUID - async def get_own_info(self) -> ProfileStruct: + async def get_own_info(self) -> SettingsStruct: if not self._logged_in_info or self._logged_in_info_time + 60 * 60 < time.monotonic(): - self._logged_in_info = await self.client.get_own_profile() + self._logged_in_info = await self.client.get_settings() self._logged_in_info_time = time.monotonic() return self._logged_in_info @@ -272,7 +273,7 @@ class User(DBUser, BaseUser): return False client = Client(self, log=self.log.getChild("ktclient")) user_info = await client.start() - # NOTE On failure, client.start throws instead of returning False + # NOTE On failure, client.start throws instead of returning something falsy self.log.info("Loaded session successfully") self.client = client self._logged_in_info = user_info @@ -303,6 +304,12 @@ class User(DBUser, BaseUser): if self._is_logged_in is None or _override: try: self._is_logged_in = bool(await self.get_own_info()) + except SerializerError: + self.log.exception( + "Unable to deserialize settings struct, " + "but didn't get auth error, so treating user as logged in" + ) + self._is_logged_in = True except Exception: self.log.exception("Exception checking login status") self._is_logged_in = False diff --git a/node/src/client.js b/node/src/client.js index b0f128f..164fa8e 100644 --- a/node/src/client.js +++ b/node/src/client.js @@ -460,8 +460,7 @@ export default class PeerClient { */ userStart = async (req) => { const userClient = this.#tryGetUser(req.mxid) || await UserClient.create(req.mxid, req.oauth_credential, this) - // TODO Should call requestMore/LessSettings instead - const res = await userClient.serviceClient.requestMyProfile() + const res = await this.#getSettings(userClient.serviceClient) if (res.success) { this.userClients.set(req.mxid, userClient) } @@ -496,7 +495,31 @@ export default class PeerClient { /** * @param {Object} req - * @param {?string} req.mxid + * @param {string} req.mxid + */ + getSettings = async (req) => { + return await this.#getSettings(this.#getUser(req.mxid).serviceClient) + } + + /** + * @param {ServiceApiClient} serviceClient + */ + #getSettings = async (serviceClient) => { + const moreRes = await serviceClient.requestMoreSettings() + if (!moreRes.success) return moreRes + + const lessRes = await serviceClient.requestLessSettings() + if (!lessRes.success) return lessRes + + return makeCommandResult({ + more: moreRes.result, + less: lessRes.result, + }) + } + + /** + * @param {Object} req + * @param {string} req.mxid */ getOwnProfile = async (req) => { return await this.#getUser(req.mxid).serviceClient.requestMyProfile() @@ -504,7 +527,15 @@ export default class PeerClient { /** * @param {Object} req - * @param {?string} req.mxid + * @param {string} req.mxid + */ + getOwnProfile = async (req) => { + return await this.#getUser(req.mxid).serviceClient.requestMyProfile() + } + + /** + * @param {Object} req + * @param {string} req.mxid * @param {Long} req.user_id */ getProfile = async (req) => { @@ -522,7 +553,7 @@ export default class PeerClient { const res = await talkChannel.updateAll() if (!res.success) return res - return this.#makeCommandResult({ + return makeCommandResult({ name: talkChannel.getDisplayName(), participants: Array.from(talkChannel.getAllUserInfo()), // TODO Image @@ -574,7 +605,7 @@ export default class PeerClient { const res = await this.#getUser(req.mxid).serviceClient.findFriendById(req.friend_id) if (!res.success) return res - return this.#makeCommandResult(res.result.friend[propertyName]) + return makeCommandResult(res.result.friend[propertyName]) } /** @@ -662,14 +693,6 @@ export default class PeerClient { }) } - #makeCommandResult(result) { - return { - success: true, - status: 0, - result: result - } - } - handleUnknownCommand = () => { throw new Error("Unknown command") } @@ -735,6 +758,7 @@ export default class PeerClient { stop: this.userStop, connect: this.handleConnect, disconnect: this.handleDisconnect, + get_settings: this.getSettings, get_own_profile: this.getOwnProfile, get_profile: this.getProfile, get_portal_channel_info: this.getPortalChannelInfo, @@ -790,6 +814,17 @@ export default class PeerClient { } +/** + * @param {object} result + */ +function makeCommandResult(result) { + return { + success: true, + status: 0, + result: result + } +} + /** * @param {TalkChannelList} channelList * @param {ChannelType} channelType diff --git a/systemd/matrix-appservice-kakaotalk-node.service b/systemd/matrix-appservice-kakaotalk-node.service new file mode 100644 index 0000000..474638c --- /dev/null +++ b/systemd/matrix-appservice-kakaotalk-node.service @@ -0,0 +1,18 @@ +[Unit] +Description=Node backend for matrix-appservice-kakaotalk +After=multi-user.target network.target + +[Service] +; User=matrix-appservice-kakaotalk +; Group=matrix-appservice-kakaotalk +Type=notify +NotifyAccess=all +WorkingDirectory=/opt/matrix-appservice-kakaotalk/node +ConfigurationDirectory=matrix-appservice-kakaotalk +RuntimeDirectory=matrix-appservice-kakaotalk +ExecStart=/usr/bin/env node src/main.js --config ${CONFIGURATION_DIRECTORY}/config.json +Restart=on-failure +RestartSec=3 + +[Install] +WantedBy=multi-user.target diff --git a/systemd/matrix-appservice-kakaotalk.service b/systemd/matrix-appservice-kakaotalk.service new file mode 100644 index 0000000..d50b498 --- /dev/null +++ b/systemd/matrix-appservice-kakaotalk.service @@ -0,0 +1,16 @@ +[Unit] +Description=matrix-appservice-kakaotalk bridge +After=multi-user.target network.target + +[Service] +; User=matrix-appservice-kakaotalk +; Group=matrix-appservice-kakaotalk +WorkingDirectory=/opt/matrix-appservice-kakaotalk +ConfigurationDirectory=matrix-appservice-kakaotalk +RuntimeDirectory=matrix-appservice-kakaotalk +ExecStart=/opt/matrix-appservice-kakaotalk/.venv/bin/python -m matrix_appservice_kakaotalk -c ${CONFIGURATION_DIRECTORY}/config.yaml +Restart=on-failure +RestartSec=3 + +[Install] +WantedBy=multi-user.target