Compare commits

..

2 Commits

Author SHA1 Message Date
8c2775056d Set logged-in flag after manual login
Not just after loading a session
2022-03-06 03:23:05 -05:00
bb3d7057b3 Implement logging out from the bridge
But there is no API for logging out of KakaoTalk
2022-03-06 03:23:05 -05:00
3 changed files with 101 additions and 35 deletions

View File

@ -24,7 +24,7 @@ from mautrix.util.signed_token import sign_token
from ..kt.client import Client as KakaoTalkClient from ..kt.client import Client as KakaoTalkClient
from ..kt.client.errors import DeviceVerificationRequired, IncorrectPasscode, IncorrectPassword, CommandException from ..kt.client.errors import DeviceVerificationRequired, IncorrectPasscode, IncorrectPassword, CommandException
#from .. import puppet as pu from .. import puppet as pu
from .typehint import CommandEvent from .typehint import CommandEvent
SECTION_AUTH = HelpSection("Authentication", 10, "") 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_]", help_args="[_email_]",
) )
async def login(evt: CommandEvent) -> None: 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") await evt.reply("You're already logged in")
return return
@ -156,10 +156,43 @@ async def _handle_login_failure(evt: CommandEvent, e: Exception) -> None:
await evt.reply(f"{message}: {e}") 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: async def logout(evt: CommandEvent) -> None:
#puppet = await pu.Puppet.get_by_ktid(evt.sender.ktid) if len(evt.args) >= 1:
await evt.sender.logout() if evt.args[0] == "--reset-device":
#if puppet.is_real_user: reset_device = True
# await puppet.switch_mxid(None, None) else:
await evt.reply("Successfully logged out") 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."
)

View File

@ -172,7 +172,7 @@ class User(DBUser, BaseUser):
@property @property
def has_state(self) -> bool: 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 # region Database getters
@ -233,10 +233,13 @@ class User(DBUser, BaseUser):
async def get_uuid(self, force: bool = False) -> str: async def get_uuid(self, force: bool = False) -> str:
if self.uuid is None or force: 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() await self.save()
return self.uuid return self.uuid
async def _generate_uuid(self) -> str:
return await Client.generate_uuid(await self.get_all_uuids())
# endregion # endregion
@property @property
@ -369,8 +372,7 @@ class User(DBUser, BaseUser):
finally: finally:
self._is_refreshing = False self._is_refreshing = False
async def logout(self, remove_ktid: bool = True) -> bool: async def logout(self, *, remove_ktid: bool = True, reset_device: bool = False) -> bool:
# TODO Remove tokens too?
ok = True ok = True
self.stop_listen() self.stop_listen()
if self.has_state: if self.has_state:
@ -385,12 +387,15 @@ class User(DBUser, BaseUser):
await self.client.stop() await self.client.stop()
self.client = None self.client = None
if remove_ktid: if self.ktid and remove_ktid:
if self.ktid:
#await UserPortal.delete_all(self.ktid) #await UserPortal.delete_all(self.ktid)
del self.by_ktid[self.ktid] del self.by_ktid[self.ktid]
self.ktid = None self.ktid = None
self.uuid = None
if reset_device:
self.uuid = await self._generate_uuid()
self.access_token = None
self.refresh_token = None
await self.save() await self.save()
return ok return ok
@ -583,10 +588,11 @@ class User(DBUser, BaseUser):
self.log.info("TODO: stop_listen") self.log.info("TODO: stop_listen")
async def on_logged_in(self, oauth_credential: OAuthCredential) -> None: 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.oauth_credential = oauth_credential
self.client = Client(self, log=self.log.getChild("ktclient")) self.client = Client(self, log=self.log.getChild("ktclient"))
await self.save() await self.save()
self._is_logged_in = True
try: try:
self._logged_in_info = await self.client.fetch_logged_in_user(post_login=True) self._logged_in_info = await self.client.fetch_logged_in_user(post_login=True)
self._logged_in_info_time = time.monotonic() self._logged_in_info_time = time.monotonic()

View File

@ -28,6 +28,7 @@ import {
class UserClient { class UserClient {
static #initializing = false
#talkClient = new TalkClient() #talkClient = new TalkClient()
get talkClient() { return this.#talkClient } get talkClient() { return this.#talkClient }
@ -37,16 +38,28 @@ class UserClient {
get serviceClient() { return this.#serviceClient } get serviceClient() { return this.#serviceClient }
/** /**
* @param {string} mxid The ID of the associated Matrix user * DO NOT CONSTRUCT DIRECTLY. Callers should use {@link UserClient#create} instead.
* @param {OAuthCredential} credential The tokens that API calls may use * @param {string} mxid
* @param {OAuthCredential} credential
*/ */
constructor(mxid, credential) { constructor(mxid, credential) {
if (!UserClient.#initializing) {
throw new Error("Private constructor")
}
UserClient.#initializing = false
this.mxid = mxid this.mxid = mxid
this.credential = credential 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) { static async create(mxid, credential) {
this.#initializing = true
const userClient = new UserClient(mxid, credential) const userClient = new UserClient(mxid, credential)
userClient.#serviceClient = await ServiceApiClient.create(this.credential) userClient.#serviceClient = await ServiceApiClient.create(this.credential)
return userClient return userClient
} }
@ -194,9 +207,9 @@ export default class PeerClient {
/** /**
* Checked lookup of a UserClient for a given mxid. * Checked lookup of a UserClient for a given mxid.
* @param {string} mxid * @param {string} mxid
* @returns {UserClient}
*/ */
#getUser(mxid) { #getUser(mxid) {
/** @type {UserClient} */
const userClient = this.userClients.get(mxid) const userClient = this.userClients.get(mxid)
if (userClient === undefined) { if (userClient === undefined) {
throw new Error(`Could not find user ${mxid}`) throw new Error(`Could not find user ${mxid}`)
@ -204,6 +217,17 @@ export default class PeerClient {
return userClient 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 * @param {Object} req
* @param {OAuthCredential} req.oauth_credential * @param {OAuthCredential} req.oauth_credential
@ -270,9 +294,7 @@ export default class PeerClient {
* @param {OAuthCredential} req.oauth_credential * @param {OAuthCredential} req.oauth_credential
*/ */
getOwnProfile = async (req) => { getOwnProfile = async (req) => {
const serviceClient = const serviceClient = await this.#getServiceClient(req.mxid, req.oauth_credential)
this.userClients.get(req.mxid)?.serviceClient ||
await ServiceApiClient.create(req.oauth_credential)
return await serviceClient.requestMyProfile() return await serviceClient.requestMyProfile()
} }
@ -283,10 +305,8 @@ export default class PeerClient {
* @param {Long} req.user_id * @param {Long} req.user_id
*/ */
getProfile = async (req) => { getProfile = async (req) => {
const serviceClient = const serviceClient = await this.#getServiceClient(req.mxid, req.oauth_credential)
this.userClients.get(mxid)?.serviceClient || return await serviceClient.requestProfile(req.user_id)
await ServiceApiClient.create(req.oauth_credential)
return await serviceClient.requestProfile(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) { #makeCommandResult(result) {
return { return {
success: true, success: true,
@ -318,12 +348,9 @@ export default class PeerClient {
} }
} }
/** #voidCommandResult = {
* @param {Object} req success: true,
* @param {string} req.mxid status: 0,
*/
handleStop = async (req) => {
this.#getUser(req.mxid).close()
} }
handleUnknownCommand = () => { handleUnknownCommand = () => {