From bb3d7057b3d56d804758c1e187b7c371f83c9d0a Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Sun, 6 Mar 2022 03:14:59 -0500 Subject: [PATCH] Implement logging out from the bridge But there is no API for logging out of KakaoTalk --- matrix_appservice_kakaotalk/commands/auth.py | 49 +++++++++++++--- matrix_appservice_kakaotalk/user.py | 27 +++++---- node/src/client.js | 59 ++++++++++++++------ 3 files changed, 100 insertions(+), 35 deletions(-) diff --git a/matrix_appservice_kakaotalk/commands/auth.py b/matrix_appservice_kakaotalk/commands/auth.py index 75e02be..36aab59 100644 --- a/matrix_appservice_kakaotalk/commands/auth.py +++ b/matrix_appservice_kakaotalk/commands/auth.py @@ -24,7 +24,7 @@ from mautrix.util.signed_token import sign_token from ..kt.client import Client as KakaoTalkClient from ..kt.client.errors import DeviceVerificationRequired, IncorrectPasscode, IncorrectPassword, CommandException -#from .. import puppet as pu +from .. import puppet as pu from .typehint import CommandEvent SECTION_AUTH = HelpSection("Authentication", 10, "") @@ -53,7 +53,7 @@ try_again_or_cancel = "Try again, or say `$cmdprefix+sp cancel` to give up." help_args="[_email_]", ) async def login(evt: CommandEvent) -> None: - if evt.sender.client: + if await evt.sender.is_logged_in(): await evt.reply("You're already logged in") return @@ -156,10 +156,43 @@ async def _handle_login_failure(evt: CommandEvent, e: Exception) -> None: await evt.reply(f"{message}: {e}") -@command_handler(needs_auth=True, help_section=SECTION_AUTH, help_text="Log out of KakaoTalk") +@command_handler( + needs_auth=True, + help_section=SECTION_AUTH, + help_text="Log out of KakaoTalk (and optionally change your virtual device ID for next login)", + help_args="[--reset-device]", +) async def logout(evt: CommandEvent) -> None: - #puppet = await pu.Puppet.get_by_ktid(evt.sender.ktid) - await evt.sender.logout() - #if puppet.is_real_user: - # await puppet.switch_mxid(None, None) - await evt.reply("Successfully logged out") + if len(evt.args) >= 1: + if evt.args[0] == "--reset-device": + reset_device = True + else: + await evt.reply("**Usage:** `$cmdprefix+sp logout [--reset-device]`") + return + else: + reset_device = False + + puppet = await pu.Puppet.get_by_ktid(evt.sender.ktid) + await evt.sender.logout(reset_device=reset_device) + if puppet.is_real_user: + await puppet.switch_mxid(None, None) + + message = "Successfully logged out" + if reset_device: + message += ( + ", and your next login will use a different device ID.\n\n" + "The old device must be manually de-registered from the KakaoTalk app." + ) + await evt.reply(message) + + +@command_handler(needs_auth=False, help_section=SECTION_AUTH, help_text="Change your virtual device ID for next login") +async def reset_device(evt: CommandEvent) -> None: + if await evt.sender.is_logged_in(): + await evt.reply("This command requires you to be logged out.") + else: + await evt.sender.logout(reset_device=True) + await evt.reply( + "Your next login will use a different device ID.\n\n" + "The old device must be manually de-registered from the KakaoTalk app." + ) diff --git a/matrix_appservice_kakaotalk/user.py b/matrix_appservice_kakaotalk/user.py index b897844..082dc55 100644 --- a/matrix_appservice_kakaotalk/user.py +++ b/matrix_appservice_kakaotalk/user.py @@ -172,7 +172,7 @@ class User(DBUser, BaseUser): @property def has_state(self) -> bool: - return self.uuid and self.ktid and self.access_token and self.refresh_token + return bool(self.uuid and self.ktid and self.access_token and self.refresh_token) # region Database getters @@ -233,10 +233,13 @@ class User(DBUser, BaseUser): async def get_uuid(self, force: bool = False) -> str: if self.uuid is None or force: - self.uuid = await Client.generate_uuid(await self.get_all_uuids()) + self.uuid = await self._generate_uuid() await self.save() return self.uuid + async def _generate_uuid(self) -> str: + return await Client.generate_uuid(await self.get_all_uuids()) + # endregion @property @@ -369,8 +372,7 @@ class User(DBUser, BaseUser): finally: self._is_refreshing = False - async def logout(self, remove_ktid: bool = True) -> bool: - # TODO Remove tokens too? + async def logout(self, *, remove_ktid: bool = True, reset_device: bool = False) -> bool: ok = True self.stop_listen() if self.has_state: @@ -385,12 +387,15 @@ class User(DBUser, BaseUser): await self.client.stop() self.client = None - if remove_ktid: - if self.ktid: - #await UserPortal.delete_all(self.ktid) - del self.by_ktid[self.ktid] - self.ktid = None - self.uuid = None + if self.ktid and remove_ktid: + #await UserPortal.delete_all(self.ktid) + del self.by_ktid[self.ktid] + self.ktid = None + + if reset_device: + self.uuid = await self._generate_uuid() + self.access_token = None + self.refresh_token = None await self.save() return ok @@ -583,7 +588,7 @@ class User(DBUser, BaseUser): self.log.info("TODO: stop_listen") async def on_logged_in(self, oauth_credential: OAuthCredential) -> None: - self.log.debug(f"Successfully logged in as {oauth_credential.uuid}") + self.log.debug(f"Successfully logged in as {oauth_credential.userId}") self.oauth_credential = oauth_credential self.client = Client(self, log=self.log.getChild("ktclient")) await self.save() diff --git a/node/src/client.js b/node/src/client.js index 509ea1e..1e34195 100644 --- a/node/src/client.js +++ b/node/src/client.js @@ -28,6 +28,7 @@ import { class UserClient { + static #initializing = false #talkClient = new TalkClient() get talkClient() { return this.#talkClient } @@ -37,16 +38,28 @@ class UserClient { get serviceClient() { return this.#serviceClient } /** - * @param {string} mxid The ID of the associated Matrix user - * @param {OAuthCredential} credential The tokens that API calls may use + * DO NOT CONSTRUCT DIRECTLY. Callers should use {@link UserClient#create} instead. + * @param {string} mxid + * @param {OAuthCredential} credential */ constructor(mxid, credential) { + if (!UserClient.#initializing) { + throw new Error("Private constructor") + } + UserClient.#initializing = false + this.mxid = mxid this.credential = credential } + /** + * @param {string} mxid The ID of the associated Matrix user + * @param {OAuthCredential} credential The tokens that API calls may use + */ static async create(mxid, credential) { + this.#initializing = true const userClient = new UserClient(mxid, credential) + userClient.#serviceClient = await ServiceApiClient.create(this.credential) return userClient } @@ -194,15 +207,26 @@ export default class PeerClient { /** * Checked lookup of a UserClient for a given mxid. * @param {string} mxid + * @returns {UserClient} */ #getUser(mxid) { - /** @type {UserClient} */ const userClient = this.userClients.get(mxid) if (userClient === undefined) { throw new Error(`Could not find user ${mxid}`) } return userClient } + + /** + * Get the service client for the specified user ID, or create + * and return a new service client if no user ID is provided. + * @param {string} mxid + * @param {OAuthCredential} oauth_credential + */ + async #getServiceClient(mxid, oauth_credential) { + return this.userClients.get(mxid)?.serviceClient || + await ServiceApiClient.create(oauth_credential) + } /** * @param {Object} req @@ -270,9 +294,7 @@ export default class PeerClient { * @param {OAuthCredential} req.oauth_credential */ getOwnProfile = async (req) => { - const serviceClient = - this.userClients.get(req.mxid)?.serviceClient || - await ServiceApiClient.create(req.oauth_credential) + const serviceClient = await this.#getServiceClient(req.mxid, req.oauth_credential) return await serviceClient.requestMyProfile() } @@ -283,10 +305,8 @@ export default class PeerClient { * @param {Long} req.user_id */ getProfile = async (req) => { - const serviceClient = - this.userClients.get(mxid)?.serviceClient || - await ServiceApiClient.create(req.oauth_credential) - return await serviceClient.requestProfile(user_id) + const serviceClient = await this.#getServiceClient(req.mxid, req.oauth_credential) + return await serviceClient.requestProfile(req.user_id) } /** @@ -310,6 +330,16 @@ export default class PeerClient { }) } + /** + * @param {Object} req + * @param {string} req.mxid + */ + handleStop = async (req) => { + this.#getUser(req.mxid).close() + this.userClients.delete(req.mxid) + return this.#voidCommandResult + } + #makeCommandResult(result) { return { success: true, @@ -318,12 +348,9 @@ export default class PeerClient { } } - /** - * @param {Object} req - * @param {string} req.mxid - */ - handleStop = async (req) => { - this.#getUser(req.mxid).close() + #voidCommandResult = { + success: true, + status: 0, } handleUnknownCommand = () => {