Allow login flow to be more like the official PC client
This commit is contained in:
parent
6623dd46c0
commit
e44536f9f2
|
@ -22,9 +22,17 @@ from mautrix.errors import MForbidden
|
||||||
#from mautrix.util.signed_token import sign_token
|
#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 (
|
||||||
|
AnotherLogonDetected,
|
||||||
|
CommandException,
|
||||||
|
DeviceVerificationRequired,
|
||||||
|
IncorrectPasscode,
|
||||||
|
IncorrectPassword,
|
||||||
|
)
|
||||||
|
|
||||||
from .. import puppet as pu
|
from .. import puppet as pu
|
||||||
|
from ..db import LoginCredential
|
||||||
|
|
||||||
from .typehint import CommandEvent
|
from .typehint import CommandEvent
|
||||||
|
|
||||||
SECTION_AUTH = HelpSection("Authentication", 10, "")
|
SECTION_AUTH = HelpSection("Authentication", 10, "")
|
||||||
|
@ -46,28 +54,47 @@ send_password = "Please send your password here to log in"
|
||||||
missing_email = "Please use `$cmdprefix+sp login <email>` to log in here"
|
missing_email = "Please use `$cmdprefix+sp login <email>` to log in here"
|
||||||
try_again_or_cancel = "Try again, or say `$cmdprefix+sp cancel` to give up."
|
try_again_or_cancel = "Try again, or say `$cmdprefix+sp cancel` to give up."
|
||||||
|
|
||||||
|
_CMD_CONTINUE_LOGIN = "continue-login"
|
||||||
|
|
||||||
|
|
||||||
@command_handler(
|
@command_handler(
|
||||||
needs_auth=False,
|
needs_auth=False,
|
||||||
management_only=True,
|
management_only=True,
|
||||||
help_section=SECTION_AUTH,
|
help_section=SECTION_AUTH,
|
||||||
help_text="Log in to KakaoTalk",
|
help_text="Log in to KakaoTalk. Optionally save login credentials & have the bridge use them when needed to automatically restore your login session.",
|
||||||
help_args="[_email_]",
|
help_args="[--save] [_email_]",
|
||||||
)
|
)
|
||||||
async def login(evt: CommandEvent) -> None:
|
async def login(evt: CommandEvent) -> None:
|
||||||
if await evt.sender.is_logged_in():
|
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
|
||||||
|
|
||||||
email = evt.args[0] if len(evt.args) > 0 else None
|
save = len(evt.args) > 0 and evt.args[0] == "--save"
|
||||||
|
email = evt.args[0 if not save else 1] if len(evt.args) > 0 else None
|
||||||
|
|
||||||
if email:
|
if email:
|
||||||
evt.sender.command_status = {
|
evt.sender.command_status = {
|
||||||
"action": "Login",
|
"action": "Login",
|
||||||
"room_id": evt.room_id,
|
"room_id": evt.room_id,
|
||||||
"next": enter_password,
|
"next": _enter_password,
|
||||||
"email": email,
|
"email": email,
|
||||||
|
"save": save,
|
||||||
|
"forced": evt.sender.force_login,
|
||||||
}
|
}
|
||||||
|
try:
|
||||||
|
creds = await LoginCredential.get_by_mxid(evt.sender.mxid)
|
||||||
|
except:
|
||||||
|
evt.log.exception("Exception while looking for saved password")
|
||||||
|
creds = None
|
||||||
|
if creds and creds.email == email:
|
||||||
|
await evt.reply("Logging in with saved password")
|
||||||
|
await _login_with_password(
|
||||||
|
evt,
|
||||||
|
evt.sender.command_status.pop("email"),
|
||||||
|
creds.password,
|
||||||
|
evt.sender.command_status.pop("forced"),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
""" TODO Implement web login
|
""" TODO Implement web login
|
||||||
if evt.bridge.public_website:
|
if evt.bridge.public_website:
|
||||||
|
@ -94,26 +121,47 @@ async def login(evt: CommandEvent) -> None:
|
||||||
if not email:
|
if not email:
|
||||||
await evt.reply(f"{missing_email}.")
|
await evt.reply(f"{missing_email}.")
|
||||||
else:
|
else:
|
||||||
await evt.reply(f"{send_password}.")
|
save_warning = (
|
||||||
|
" NOTE: When saving your credentials, the bridge will store your KakaoTalk password **unencrypted** until you discard it with `$cmdprefix+sp forget-password`."
|
||||||
|
" If you would rather not have the bridge save your password, type `$cmdprefix+sp cancel`, then `$cmdprefix+sp login [_email_]` without `--save`."
|
||||||
|
) if save else ""
|
||||||
|
await evt.reply(f"{send_password}.{save_warning}")
|
||||||
|
|
||||||
|
|
||||||
async def enter_password(evt: CommandEvent) -> None:
|
async def _enter_password(evt: CommandEvent) -> None:
|
||||||
try:
|
try:
|
||||||
await evt.az.intent.redact(evt.room_id, evt.event_id)
|
await evt.az.intent.redact(evt.room_id, evt.event_id)
|
||||||
except MForbidden:
|
except MForbidden:
|
||||||
pass
|
await evt.mark_read()
|
||||||
|
|
||||||
assert evt.sender.command_status
|
assert evt.sender.command_status
|
||||||
|
await _login_with_password(
|
||||||
|
evt,
|
||||||
|
evt.sender.command_status.pop("email"),
|
||||||
|
evt.content.body,
|
||||||
|
evt.sender.command_status.pop("forced"),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _login_with_password(evt: CommandEvent, email: str, password: str, forced: bool) -> None:
|
||||||
req = {
|
req = {
|
||||||
"uuid": await evt.sender.get_uuid(),
|
"uuid": await evt.sender.get_uuid(),
|
||||||
"form": {
|
"form": {
|
||||||
"email": evt.sender.command_status["email"],
|
"email": email,
|
||||||
"password": evt.content.body,
|
"password": password,
|
||||||
}
|
},
|
||||||
|
"forced": forced,
|
||||||
}
|
}
|
||||||
|
await _try_login(evt, req)
|
||||||
|
|
||||||
|
async def _try_login(evt: CommandEvent, req: dict) -> None:
|
||||||
|
save = (
|
||||||
|
evt.sender.command_status.get("save", False)
|
||||||
|
if evt.sender.command_status
|
||||||
|
else False
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
await _do_login(evt, req)
|
await _do_login(evt, req, save)
|
||||||
except DeviceVerificationRequired:
|
except DeviceVerificationRequired as e:
|
||||||
|
if evt.sender.command_status and evt.sender.command_status.get("next") != _enter_dv_code:
|
||||||
await evt.reply(
|
await evt.reply(
|
||||||
"Open KakaoTalk on your smartphone. It should show a device registration passcode. "
|
"Open KakaoTalk on your smartphone. It should show a device registration passcode. "
|
||||||
"Enter that passcode here."
|
"Enter that passcode here."
|
||||||
|
@ -121,38 +169,98 @@ async def enter_password(evt: CommandEvent) -> None:
|
||||||
evt.sender.command_status = {
|
evt.sender.command_status = {
|
||||||
"action": "Login",
|
"action": "Login",
|
||||||
"room_id": evt.room_id,
|
"room_id": evt.room_id,
|
||||||
"next": enter_dv_code,
|
"next": _enter_dv_code,
|
||||||
|
"save": save,
|
||||||
"req": req,
|
"req": req,
|
||||||
}
|
}
|
||||||
|
else:
|
||||||
|
evt.log.error("Device registration still required after having just registered")
|
||||||
|
await _handle_login_failure(evt, e)
|
||||||
|
except AnotherLogonDetected as e:
|
||||||
|
if not req["forced"]:
|
||||||
|
await evt.reply(
|
||||||
|
"You are currently logged in to KakaoTalk on a PC or another bridge. "
|
||||||
|
"In order to log in to this bridge, you will be forced to log out from your other session. "
|
||||||
|
f"To proceed, type `$cmdprefix+sp {_CMD_CONTINUE_LOGIN}`. Otherwise, type `$cmdprefix+sp cancel`."
|
||||||
|
)
|
||||||
|
evt.sender.command_status = {
|
||||||
|
"action": "Login",
|
||||||
|
"room_id": evt.room_id,
|
||||||
|
"next": _force_login,
|
||||||
|
"save": save,
|
||||||
|
"req": req,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
evt.log.error("Failed to force-login while logged in elsewhere")
|
||||||
|
await _handle_login_failure(evt, e)
|
||||||
except IncorrectPassword:
|
except IncorrectPassword:
|
||||||
await evt.reply(f"Incorrect password. {try_again_or_cancel}")
|
await evt.reply(f"Incorrect password. {try_again_or_cancel}")
|
||||||
|
evt.sender.command_status = {
|
||||||
|
"action": "Login",
|
||||||
|
"room_id": evt.room_id,
|
||||||
|
"next": _enter_password,
|
||||||
|
"email": req["form"]["email"],
|
||||||
|
"save": save,
|
||||||
|
"forced": req["forced"],
|
||||||
|
}
|
||||||
#except OAuthException as e:
|
#except OAuthException as e:
|
||||||
# await evt.reply(f"Error from KakaoTalk:\n\n> {e}")
|
# await evt.reply(f"Error from KakaoTalk:\n\n> {e}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await _handle_login_failure(evt, e)
|
await _handle_login_failure(evt, e)
|
||||||
|
|
||||||
|
|
||||||
async def enter_dv_code(evt: CommandEvent) -> None:
|
async def _enter_dv_code(evt: CommandEvent) -> None:
|
||||||
assert evt.sender.command_status
|
assert evt.sender.command_status
|
||||||
req: dict = evt.sender.command_status["req"]
|
req: dict = evt.sender.command_status.pop("req")
|
||||||
passcode = evt.content.body
|
passcode = evt.content.body
|
||||||
await evt.mark_read()
|
await evt.mark_read()
|
||||||
try:
|
try:
|
||||||
await KakaoTalkClient.register_device(passcode, **req)
|
await KakaoTalkClient.register_device(passcode, **req)
|
||||||
await _do_login(evt, req)
|
|
||||||
except IncorrectPasscode:
|
except IncorrectPasscode:
|
||||||
await evt.reply(f"Incorrect device registration passcode. {try_again_or_cancel}")
|
await evt.reply(f"Incorrect device registration passcode. {try_again_or_cancel}")
|
||||||
#except OAuthException as e:
|
#except OAuthException as e:
|
||||||
# await evt.reply(f"Error from KakaoTalk:\n\n> {e}")
|
# await evt.reply(f"Error from KakaoTalk:\n\n> {e}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await _handle_login_failure(evt, e)
|
await _handle_login_failure(evt, e)
|
||||||
|
else:
|
||||||
|
await _try_login(evt, req)
|
||||||
|
|
||||||
|
|
||||||
async def _do_login(evt: CommandEvent, req: dict) -> None:
|
async def _force_login(evt: CommandEvent) -> None:
|
||||||
|
command = evt.content.body
|
||||||
|
if command != _CMD_CONTINUE_LOGIN:
|
||||||
|
await evt.reply(
|
||||||
|
f"Unknown action `{command}`. "
|
||||||
|
f"Use `$cmdprefix+sp {_CMD_CONTINUE_LOGIN}` or `$cmdprefix+sp cancel`."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
assert evt.sender.command_status
|
||||||
|
evt.sender.command_status["forced"] = True
|
||||||
|
await evt.mark_read()
|
||||||
|
await _try_login(evt, evt.sender.command_status.pop("req"))
|
||||||
|
|
||||||
|
|
||||||
|
async def _do_login(evt: CommandEvent, req: dict, save: bool) -> None:
|
||||||
oauth_credential = await KakaoTalkClient.login(**req)
|
oauth_credential = await KakaoTalkClient.login(**req)
|
||||||
await evt.sender.on_logged_in(oauth_credential)
|
await evt.sender.on_logged_in(oauth_credential)
|
||||||
evt.sender.command_status = None
|
evt.sender.command_status = None
|
||||||
await evt.reply("Successfully logged in")
|
await evt.reply("Successfully logged in")
|
||||||
|
try:
|
||||||
|
if save:
|
||||||
|
email = req["form"]["email"]
|
||||||
|
password = req["form"]["password"]
|
||||||
|
creds = await LoginCredential.get_by_mxid(evt.sender.mxid)
|
||||||
|
if creds:
|
||||||
|
evt.log.warning("Found pre-existing login credentials, so updating them now")
|
||||||
|
creds.email = email
|
||||||
|
creds.password = password
|
||||||
|
await creds.save()
|
||||||
|
else:
|
||||||
|
await LoginCredential(evt.sender.mxid, email, password).insert()
|
||||||
|
else:
|
||||||
|
await LoginCredential.delete_by_mxid(evt.sender.mxid)
|
||||||
|
except:
|
||||||
|
evt.log.exception("Error updating saved credentials after successful login")
|
||||||
|
|
||||||
async def _handle_login_failure(evt: CommandEvent, e: Exception) -> None:
|
async def _handle_login_failure(evt: CommandEvent, e: Exception) -> None:
|
||||||
evt.sender.command_status = None
|
evt.sender.command_status = None
|
||||||
|
@ -165,6 +273,24 @@ 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,
|
||||||
|
management_only=True,
|
||||||
|
help_section=SECTION_AUTH,
|
||||||
|
help_text="Delete saved login credentials, if they were saved"
|
||||||
|
)
|
||||||
|
async def forget_password(evt: CommandEvent) -> None:
|
||||||
|
creds = await LoginCredential.get_by_mxid(evt.sender.mxid)
|
||||||
|
if not creds:
|
||||||
|
await evt.reply("The bridge wasn't storing your login credentials, so there was nothing to forget.")
|
||||||
|
else:
|
||||||
|
await creds.delete()
|
||||||
|
await evt.reply(
|
||||||
|
"This bridge is no longer storing your login credentials. "
|
||||||
|
"If you get logged out unexpectedly, you will have to manually log back in."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@command_handler(
|
@command_handler(
|
||||||
needs_auth=True,
|
needs_auth=True,
|
||||||
help_section=SECTION_AUTH,
|
help_section=SECTION_AUTH,
|
||||||
|
@ -206,3 +332,30 @@ async def reset_device(evt: CommandEvent) -> None:
|
||||||
"Your next login will use a different device ID.\n\n"
|
"Your next login will use a different device ID.\n\n"
|
||||||
"The old device must be manually de-registered from the KakaoTalk app."
|
"The old device must be manually de-registered from the KakaoTalk app."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@command_handler(
|
||||||
|
needs_auth=False,
|
||||||
|
help_section=SECTION_AUTH,
|
||||||
|
help_text="When logging in, automatically log out of any other existing KakaoTalk session"
|
||||||
|
)
|
||||||
|
async def enable_forced_login(evt: CommandEvent) -> None:
|
||||||
|
if evt.sender.force_login:
|
||||||
|
await evt.reply("Forced login is already enabled.")
|
||||||
|
return
|
||||||
|
evt.sender.force_login = True
|
||||||
|
await evt.sender.save()
|
||||||
|
await evt.reply("Forced login is now enabled.")
|
||||||
|
|
||||||
|
@command_handler(
|
||||||
|
needs_auth=False,
|
||||||
|
help_section=SECTION_AUTH,
|
||||||
|
help_text="When logging in, ask before logging out of another existing KakaoTalk session, if one exists"
|
||||||
|
)
|
||||||
|
async def disable_forced_login(evt: CommandEvent) -> None:
|
||||||
|
if not evt.sender.force_login:
|
||||||
|
await evt.reply("Forced login is already disabled.")
|
||||||
|
return
|
||||||
|
evt.sender.force_login = False
|
||||||
|
await evt.sender.save()
|
||||||
|
await evt.reply("Forced login is now disabled.")
|
||||||
|
|
|
@ -45,6 +45,12 @@ async def disconnect(evt: CommandEvent) -> None:
|
||||||
if not evt.sender.is_connected:
|
if not evt.sender.is_connected:
|
||||||
await evt.reply("You are already disconnected from KakaoTalk chats")
|
await evt.reply("You are already disconnected from KakaoTalk chats")
|
||||||
return
|
return
|
||||||
|
if not evt.sender.config["bridge.remain_logged_in_on_disconnect"]:
|
||||||
|
await evt.reply(
|
||||||
|
"This instance of the KakaoTalk bridge does not allow being disconnected from KakaoTalk chats while logged in. "
|
||||||
|
"So, to disconnect, you must log out entirely with the `logout` command."
|
||||||
|
)
|
||||||
|
return
|
||||||
await evt.mark_read()
|
await evt.mark_read()
|
||||||
await evt.sender.client.disconnect()
|
await evt.sender.client.disconnect()
|
||||||
await evt.reply("Successfully disconnected from KakaoTalk chats. To reconnect, use the `sync` command.")
|
await evt.reply("Successfully disconnected from KakaoTalk chats. To reconnect, use the `sync` command.")
|
||||||
|
|
|
@ -104,6 +104,9 @@ class Config(BaseBridgeConfig):
|
||||||
copy("bridge.periodic_reconnect.min_connected_time")
|
copy("bridge.periodic_reconnect.min_connected_time")
|
||||||
"""
|
"""
|
||||||
copy("bridge.resync_max_disconnected_time")
|
copy("bridge.resync_max_disconnected_time")
|
||||||
|
copy("bridge.remain_logged_in_on_disconnect")
|
||||||
|
copy("bridge.allow_token_relogin")
|
||||||
|
copy("bridge.reconnect_on_token_relogin")
|
||||||
copy("bridge.sync_on_startup")
|
copy("bridge.sync_on_startup")
|
||||||
copy("bridge.temporary_disconnect_notices")
|
copy("bridge.temporary_disconnect_notices")
|
||||||
copy("bridge.disable_bridge_notices")
|
copy("bridge.disable_bridge_notices")
|
||||||
|
|
|
@ -20,10 +20,11 @@ from .portal import Portal
|
||||||
from .puppet import Puppet
|
from .puppet import Puppet
|
||||||
from .upgrade import upgrade_table
|
from .upgrade import upgrade_table
|
||||||
from .user import User
|
from .user import User
|
||||||
|
from .login_credential import LoginCredential
|
||||||
|
|
||||||
|
|
||||||
def init(db: Database) -> None:
|
def init(db: Database) -> None:
|
||||||
for table in (Portal, Message, User, Puppet):
|
for table in (Portal, Message, User, Puppet, LoginCredential):
|
||||||
table.db = db
|
table.db = db
|
||||||
|
|
||||||
|
|
||||||
|
@ -34,4 +35,5 @@ __all__ = [
|
||||||
"Portal",
|
"Portal",
|
||||||
"Puppet",
|
"Puppet",
|
||||||
"User",
|
"User",
|
||||||
|
"LoginCredential",
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
# 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 <https://www.gnu.org/licenses/>.
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, ClassVar
|
||||||
|
|
||||||
|
from asyncpg import Record
|
||||||
|
from attr import dataclass
|
||||||
|
|
||||||
|
from mautrix.types import UserID
|
||||||
|
from mautrix.util.async_db import Database
|
||||||
|
|
||||||
|
fake_db = Database.create("") if TYPE_CHECKING else None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LoginCredential:
|
||||||
|
db: ClassVar[Database] = fake_db
|
||||||
|
|
||||||
|
mxid: UserID
|
||||||
|
email: str
|
||||||
|
password: str
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_by_mxid(cls, mxid: UserID) -> LoginCredential | None:
|
||||||
|
q = "SELECT mxid, email, password FROM login_credential WHERE mxid=$1"
|
||||||
|
row = await cls.db.fetchrow(q, mxid)
|
||||||
|
return cls(**row) if row else None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def delete_by_mxid(cls, mxid: UserID) -> None:
|
||||||
|
await cls.db.execute("DELETE FROM login_credential WHERE mxid=$1", mxid)
|
||||||
|
|
||||||
|
async def insert(self) -> None:
|
||||||
|
q = """
|
||||||
|
INSERT INTO login_credential (mxid, email, password)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
"""
|
||||||
|
await self.db.execute(q, self.mxid, self.email, self.password)
|
||||||
|
|
||||||
|
def get_form(self) -> dict[str, str]:
|
||||||
|
return {
|
||||||
|
"email": self.email,
|
||||||
|
"password": self.password,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def delete(self) -> None:
|
||||||
|
await self.delete_by_mxid(self.mxid)
|
||||||
|
|
||||||
|
async def save(self) -> None:
|
||||||
|
q = """
|
||||||
|
UPDATE login_credential SET email=$2, password=$3
|
||||||
|
WHERE mxid=$1
|
||||||
|
"""
|
||||||
|
await self.db.execute(q, self.mxid, self.email, self.password)
|
|
@ -19,3 +19,4 @@ upgrade_table = UpgradeTable()
|
||||||
|
|
||||||
from . import v01_initial_revision
|
from . import v01_initial_revision
|
||||||
from . import v02_channel_meta
|
from . import v02_channel_meta
|
||||||
|
from . import v03_user_connection
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
# 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 <https://www.gnu.org/licenses/>.
|
||||||
|
from mautrix.util.async_db import Connection
|
||||||
|
|
||||||
|
from . import upgrade_table
|
||||||
|
|
||||||
|
|
||||||
|
@upgrade_table.register(description="Track login credentials, connections, and force-login preferences")
|
||||||
|
async def upgrade_v3(conn: Connection) -> None:
|
||||||
|
await conn.execute('ALTER TABLE "user" ADD COLUMN force_login BOOLEAN NOT NULL DEFAULT false')
|
||||||
|
await conn.execute('ALTER TABLE "user" ADD COLUMN was_connected BOOLEAN NOT NULL DEFAULT false')
|
||||||
|
# Just for now, assume that logged in = connected
|
||||||
|
await conn.execute('UPDATE "user" SET was_connected=true WHERE access_token IS NOT NULL')
|
||||||
|
|
||||||
|
await conn.execute(
|
||||||
|
"""CREATE TABLE login_credential (
|
||||||
|
mxid TEXT PRIMARY KEY,
|
||||||
|
email TEXT NOT NULL,
|
||||||
|
password TEXT NOT NULL,
|
||||||
|
|
||||||
|
FOREIGN KEY (mxid)
|
||||||
|
REFERENCES "user" (mxid)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
)"""
|
||||||
|
)
|
|
@ -33,6 +33,8 @@ class User:
|
||||||
db: ClassVar[Database] = fake_db
|
db: ClassVar[Database] = fake_db
|
||||||
|
|
||||||
mxid: UserID
|
mxid: UserID
|
||||||
|
force_login: bool
|
||||||
|
was_connected: bool
|
||||||
ktid: Long | None = field(converter=to_optional_long)
|
ktid: Long | None = field(converter=to_optional_long)
|
||||||
uuid: str | None
|
uuid: str | None
|
||||||
access_token: str | None
|
access_token: str | None
|
||||||
|
@ -47,7 +49,7 @@ class User:
|
||||||
def _from_optional_row(cls, row: Record | None) -> User | None:
|
def _from_optional_row(cls, row: Record | None) -> User | None:
|
||||||
return cls._from_row(row) if row is not None else None
|
return cls._from_row(row) if row is not None else None
|
||||||
|
|
||||||
_columns = "mxid, ktid, uuid, access_token, refresh_token, notice_room"
|
_columns = "mxid, force_login, was_connected, ktid, uuid, access_token, refresh_token, notice_room"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def all_logged_in(cls) -> List[User]:
|
async def all_logged_in(cls) -> List[User]:
|
||||||
|
@ -76,6 +78,8 @@ class User:
|
||||||
def _values(self):
|
def _values(self):
|
||||||
return (
|
return (
|
||||||
self.mxid,
|
self.mxid,
|
||||||
|
self.force_login,
|
||||||
|
self.was_connected,
|
||||||
self.ktid,
|
self.ktid,
|
||||||
self.uuid,
|
self.uuid,
|
||||||
self.access_token,
|
self.access_token,
|
||||||
|
@ -85,8 +89,8 @@ class User:
|
||||||
|
|
||||||
async def insert(self) -> None:
|
async def insert(self) -> None:
|
||||||
q = """
|
q = """
|
||||||
INSERT INTO "user" (mxid, ktid, uuid, access_token, refresh_token, notice_room)
|
INSERT INTO "user" (mxid, force_login, was_connected, ktid, uuid, access_token, refresh_token, notice_room)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||||
"""
|
"""
|
||||||
await self.db.execute(q, *self._values)
|
await self.db.execute(q, *self._values)
|
||||||
|
|
||||||
|
@ -95,7 +99,7 @@ class User:
|
||||||
|
|
||||||
async def save(self) -> None:
|
async def save(self) -> None:
|
||||||
q = """
|
q = """
|
||||||
UPDATE "user" SET ktid=$2, uuid=$3, access_token=$4, refresh_token=$5, notice_room=$6
|
UPDATE "user" SET force_login=$2, was_connected=$3, ktid=$4, uuid=$5, access_token=$6, refresh_token=$7, notice_room=$8
|
||||||
WHERE mxid=$1
|
WHERE mxid=$1
|
||||||
"""
|
"""
|
||||||
await self.db.execute(q, *self._values)
|
await self.db.execute(q, *self._values)
|
||||||
|
|
|
@ -191,7 +191,18 @@ bridge:
|
||||||
# Set to 0 to always re-sync, or -1 to never re-sync automatically.
|
# Set to 0 to always re-sync, or -1 to never re-sync automatically.
|
||||||
# TODO Actually use this setting
|
# TODO Actually use this setting
|
||||||
resync_max_disconnected_time: 5
|
resync_max_disconnected_time: 5
|
||||||
# Should the bridge do a resync on startup?
|
# Should users remain logged in after being disconnected from chatroom updates?
|
||||||
|
# This is a convenience feature, but might make the bridge look more suspicious to KakaoTalk.
|
||||||
|
remain_logged_in_on_disconnect: true
|
||||||
|
# May the bridge restore user logins with session tokens instead of requiring a password?
|
||||||
|
# This is a convenience feature, but might make the bridge look more suspicious to KakaoTalk.
|
||||||
|
# Note that password-based login will be tried first for users who have saved their password.
|
||||||
|
allow_token_relogin: true
|
||||||
|
# Should the bridge connect users to chatroom updates after a token-based login?
|
||||||
|
# This will disconnect any KakaoTalk PC/bridge sessions that were started since the last connection.
|
||||||
|
# This is a convenience feature, but might make the bridge look more suspicious to KakaoTalk.
|
||||||
|
reconnect_on_token_relogin: true
|
||||||
|
# Should the bridge do a resync for connected users on startup?
|
||||||
sync_on_startup: true
|
sync_on_startup: true
|
||||||
# Whether or not temporary disconnections should send notices to the notice room.
|
# Whether or not temporary disconnections should send notices to the notice room.
|
||||||
# If this is false, disconnections will never send messages and connections will only send
|
# If this is false, disconnections will never send messages and connections will only send
|
||||||
|
|
|
@ -137,13 +137,19 @@ class Client:
|
||||||
return cls._api_request_void("register_device", passcode=passcode, **req)
|
return cls._api_request_void("register_device", passcode=passcode, **req)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def login(cls, **req: JSON) -> Awaitable[OAuthCredential]:
|
def login(cls, uuid: str, form: JSON, forced: bool) -> Awaitable[OAuthCredential]:
|
||||||
"""
|
"""
|
||||||
Obtain a session token by logging in with user-provided credentials.
|
Obtain a session token by logging in with user-provided credentials.
|
||||||
Must have first called register_device with these credentials.
|
Must have first called register_device with these credentials.
|
||||||
"""
|
"""
|
||||||
# NOTE Actually returns an auth LoginData, but this only needs an OAuthCredential
|
# NOTE Actually returns an auth LoginData, but this only needs an OAuthCredential
|
||||||
return cls._api_request_result(OAuthCredential, "login", **req)
|
return cls._api_request_result(
|
||||||
|
OAuthCredential,
|
||||||
|
"login",
|
||||||
|
uuid=uuid,
|
||||||
|
form=form,
|
||||||
|
forced=forced,
|
||||||
|
)
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
|
|
@ -14,13 +14,13 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""Internal helpers for error handling."""
|
"""Internal helpers for error handling."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import NoReturn, Type
|
from typing import NoReturn, Type
|
||||||
|
|
||||||
from .errors import (
|
from .errors import (
|
||||||
CommandException,
|
CommandException,
|
||||||
|
AnotherLogonDetected,
|
||||||
AuthenticationRequired,
|
AuthenticationRequired,
|
||||||
DeviceVerificationRequired,
|
DeviceVerificationRequired,
|
||||||
IncorrectPasscode,
|
IncorrectPasscode,
|
||||||
|
@ -61,7 +61,7 @@ _error_code_class_map: dict[KnownAuthStatusCode | KnownDataStatusCode | int, Typ
|
||||||
#KnownAuthStatusCode.EXCEED_DAILY_REQUEST_LIMIT_VOICECALL: "Daily voicecall limit exceeded",
|
#KnownAuthStatusCode.EXCEED_DAILY_REQUEST_LIMIT_VOICECALL: "Daily voicecall limit exceeded",
|
||||||
#KnownAuthStatusCode.EXCEED_DAILY_REQUEST_LIMIT_WITHOUT_TOKEN: "Daily tokenless request limit exceeded",
|
#KnownAuthStatusCode.EXCEED_DAILY_REQUEST_LIMIT_WITHOUT_TOKEN: "Daily tokenless request limit exceeded",
|
||||||
KnownAuthStatusCode.DEVICE_NOT_REGISTERED: DeviceVerificationRequired,
|
KnownAuthStatusCode.DEVICE_NOT_REGISTERED: DeviceVerificationRequired,
|
||||||
#KnownAuthStatusCode.ANOTHER_LOGON: "Another logon detected",
|
KnownAuthStatusCode.ANOTHER_LOGON: AnotherLogonDetected,
|
||||||
#KnownAuthStatusCode.DEVICE_REGISTER_FAILED: "Device registration failed",
|
#KnownAuthStatusCode.DEVICE_REGISTER_FAILED: "Device registration failed",
|
||||||
#KnownAuthStatusCode.INVALID_DEVICE_REGISTER: "Invalid device",
|
#KnownAuthStatusCode.INVALID_DEVICE_REGISTER: "Invalid device",
|
||||||
KnownAuthStatusCode.INVALID_PASSCODE: IncorrectPasscode,
|
KnownAuthStatusCode.INVALID_PASSCODE: IncorrectPasscode,
|
||||||
|
|
|
@ -14,7 +14,6 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""Helper functions & types for status codes for the KakaoTalk API."""
|
"""Helper functions & types for status codes for the KakaoTalk API."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from ..types.api.auth_api_client import KnownAuthStatusCode
|
from ..types.api.auth_api_client import KnownAuthStatusCode
|
||||||
|
@ -48,6 +47,9 @@ class OAuthException(CommandException):
|
||||||
class DeviceVerificationRequired(OAuthException):
|
class DeviceVerificationRequired(OAuthException):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class AnotherLogonDetected(OAuthException):
|
||||||
|
pass
|
||||||
|
|
||||||
class IncorrectPassword(OAuthException):
|
class IncorrectPassword(OAuthException):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
@ -35,10 +35,16 @@ from mautrix.util.simple_lock import SimpleLock
|
||||||
|
|
||||||
from . import portal as po, puppet as pu
|
from . import portal as po, puppet as pu
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .db import User as DBUser
|
from .db import User as DBUser, LoginCredential
|
||||||
|
|
||||||
from .kt.client import Client
|
from .kt.client import Client
|
||||||
from .kt.client.errors import AuthenticationRequired, ResponseError
|
from .kt.client.errors import (
|
||||||
|
AuthenticationRequired,
|
||||||
|
AnotherLogonDetected,
|
||||||
|
IncorrectPassword,
|
||||||
|
OAuthException,
|
||||||
|
ResponseError,
|
||||||
|
)
|
||||||
from .kt.client.types import PortalChannelInfo, SettingsStruct
|
from .kt.client.types import PortalChannelInfo, SettingsStruct
|
||||||
from .kt.types.bson import Long
|
from .kt.types.bson import Long
|
||||||
from .kt.types.channel.channel_info import ChannelInfo, NormalChannelInfo, NormalChannelData
|
from .kt.types.channel.channel_info import ChannelInfo, NormalChannelInfo, NormalChannelData
|
||||||
|
@ -107,6 +113,8 @@ class User(DBUser, BaseUser):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
mxid: UserID,
|
mxid: UserID,
|
||||||
|
force_login: bool,
|
||||||
|
was_connected: bool,
|
||||||
ktid: Long | None = None,
|
ktid: Long | None = None,
|
||||||
uuid: str | None = None,
|
uuid: str | None = None,
|
||||||
access_token: str | None = None,
|
access_token: str | None = None,
|
||||||
|
@ -115,6 +123,8 @@ class User(DBUser, BaseUser):
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
mxid=mxid,
|
mxid=mxid,
|
||||||
|
force_login=force_login,
|
||||||
|
was_connected=was_connected,
|
||||||
ktid=ktid,
|
ktid=ktid,
|
||||||
uuid=uuid,
|
uuid=uuid,
|
||||||
access_token=access_token,
|
access_token=access_token,
|
||||||
|
@ -277,7 +287,51 @@ class User(DBUser, BaseUser):
|
||||||
async def _load_session(self, is_startup: bool) -> bool:
|
async def _load_session(self, is_startup: bool) -> bool:
|
||||||
if self._is_logged_in and is_startup:
|
if self._is_logged_in and is_startup:
|
||||||
return True
|
return True
|
||||||
elif not self.has_state:
|
if not self.was_connected and not self.config["bridge.remain_logged_in_on_disconnect"]:
|
||||||
|
self.log.warning("Not logging in because last session was disconnected, and login+disconnection is forbidden by config")
|
||||||
|
await self.push_bridge_state(
|
||||||
|
BridgeStateEvent.LOGGED_OUT,
|
||||||
|
error="logged-out",
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
latest_exc: Exception | None = None
|
||||||
|
password_ok = False
|
||||||
|
try:
|
||||||
|
creds = await LoginCredential.get_by_mxid(self.mxid)
|
||||||
|
except Exception as e:
|
||||||
|
self.log.exception("Exception while looking for saved password")
|
||||||
|
creds = None
|
||||||
|
if creds:
|
||||||
|
uuid = await self.get_uuid()
|
||||||
|
form = creds.get_form()
|
||||||
|
oauth_credential = None
|
||||||
|
try:
|
||||||
|
oauth_credential = await Client.login(uuid=uuid, form=form, forced=False)
|
||||||
|
except IncorrectPassword as e:
|
||||||
|
latest_exc = e
|
||||||
|
except AnotherLogonDetected as e:
|
||||||
|
if self.force_login:
|
||||||
|
try:
|
||||||
|
# Wait a moment to make it look like a user-initiated response
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
oauth_credential = await Client.login(uuid=uuid, form=form, forced=True)
|
||||||
|
except OAuthException as e:
|
||||||
|
latest_exc = e
|
||||||
|
if oauth_credential:
|
||||||
|
self.oauth_credential = oauth_credential
|
||||||
|
await self.save()
|
||||||
|
password_ok = True
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
await creds.delete()
|
||||||
|
except:
|
||||||
|
self.log.exception("Exception while deleting incorrect password")
|
||||||
|
if password_ok or self.config["bridge.allow_token_relogin"] and self.has_state:
|
||||||
|
if not password_ok:
|
||||||
|
self.log.warning("Using token-based relogin after password-based relogin failed")
|
||||||
|
elif latest_exc:
|
||||||
|
raise latest_exc
|
||||||
|
else:
|
||||||
# If we have a user in the DB with no state, we can assume
|
# If we have a user in the DB with no state, we can assume
|
||||||
# KT logged us out and the bridge has restarted
|
# KT logged us out and the bridge has restarted
|
||||||
await self.push_bridge_state(
|
await self.push_bridge_state(
|
||||||
|
@ -295,10 +349,10 @@ class User(DBUser, BaseUser):
|
||||||
self._track_metric(METRIC_LOGGED_IN, True)
|
self._track_metric(METRIC_LOGGED_IN, True)
|
||||||
self._is_logged_in = True
|
self._is_logged_in = True
|
||||||
self.is_connected = None
|
self.is_connected = None
|
||||||
asyncio.create_task(self.post_login(is_startup=is_startup))
|
asyncio.create_task(self.post_login(is_startup=is_startup, is_token_login=not password_ok))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def _send_reset_notice(self, e: AuthenticationRequired, edit: EventID | None = None) -> None:
|
async def _send_reset_notice(self, e: OAuthException, edit: EventID | None = None) -> None:
|
||||||
await self.send_bridge_notice(
|
await self.send_bridge_notice(
|
||||||
"Got authentication error from KakaoTalk:\n\n"
|
"Got authentication error from KakaoTalk:\n\n"
|
||||||
f"> {e.message}\n\n"
|
f"> {e.message}\n\n"
|
||||||
|
@ -333,8 +387,13 @@ class User(DBUser, BaseUser):
|
||||||
self, event_id: EventID | None = None, retries: int = 3, is_startup: bool = False
|
self, event_id: EventID | None = None, retries: int = 3, is_startup: bool = False
|
||||||
) -> None:
|
) -> None:
|
||||||
try:
|
try:
|
||||||
await self._load_session(is_startup=is_startup)
|
if not await self._load_session(is_startup=is_startup) and self.has_state:
|
||||||
except AuthenticationRequired as e:
|
await self.send_bridge_notice(
|
||||||
|
"Logged out of KakaoTalk. Must use the `login` command to log back in.",
|
||||||
|
important=True,
|
||||||
|
)
|
||||||
|
await self.logout(remove_ktid=False)
|
||||||
|
except OAuthException as e:
|
||||||
await self._send_reset_notice(e, edit=event_id)
|
await self._send_reset_notice(e, edit=event_id)
|
||||||
# TODO Throw a ResponseError on network failures
|
# TODO Throw a ResponseError on network failures
|
||||||
except ResponseError as e:
|
except ResponseError as e:
|
||||||
|
@ -383,6 +442,7 @@ class User(DBUser, BaseUser):
|
||||||
|
|
||||||
self._is_logged_in = False
|
self._is_logged_in = False
|
||||||
self.is_connected = None
|
self.is_connected = None
|
||||||
|
self.was_connected = False
|
||||||
self._client = None
|
self._client = None
|
||||||
|
|
||||||
if self.ktid and remove_ktid:
|
if self.ktid and remove_ktid:
|
||||||
|
@ -392,7 +452,7 @@ class User(DBUser, BaseUser):
|
||||||
|
|
||||||
await self.save()
|
await self.save()
|
||||||
|
|
||||||
async def post_login(self, is_startup: bool) -> None:
|
async def post_login(self, is_startup: bool, is_token_login: bool) -> None:
|
||||||
self.log.info("Running post-login actions")
|
self.log.info("Running post-login actions")
|
||||||
assert self.ktid
|
assert self.ktid
|
||||||
self._add_to_cache()
|
self._add_to_cache()
|
||||||
|
@ -406,13 +466,19 @@ class User(DBUser, BaseUser):
|
||||||
except Exception:
|
except Exception:
|
||||||
self.log.exception("Failed to automatically enable custom puppet")
|
self.log.exception("Failed to automatically enable custom puppet")
|
||||||
|
|
||||||
|
if not is_token_login or self.was_connected and self.config["bridge.reconnect_on_token_relogin"]:
|
||||||
# TODO Check if things break when a live message comes in during syncing
|
# TODO Check if things break when a live message comes in during syncing
|
||||||
if self.config["bridge.sync_on_startup"] or not is_startup:
|
if self.config["bridge.sync_on_startup"] or not is_startup:
|
||||||
sync_count = self.config["bridge.initial_chat_sync"]
|
sync_count = self.config["bridge.initial_chat_sync"]
|
||||||
else:
|
else:
|
||||||
sync_count = None
|
sync_count = None
|
||||||
# TODO Don't auto-connect on startup if user's last state was disconnected
|
|
||||||
await self.connect_and_sync(sync_count)
|
await self.connect_and_sync(sync_count)
|
||||||
|
else:
|
||||||
|
await self.send_bridge_notice(
|
||||||
|
f"Logged into KakaoTalk. To connect to chatroom updates, use the `sync` command."
|
||||||
|
)
|
||||||
|
self.was_connected = False
|
||||||
|
await self.save()
|
||||||
|
|
||||||
async def get_direct_chats(self) -> dict[UserID, list[RoomID]]:
|
async def get_direct_chats(self) -> dict[UserID, list[RoomID]]:
|
||||||
return {
|
return {
|
||||||
|
@ -645,14 +711,16 @@ class User(DBUser, BaseUser):
|
||||||
if self.temp_disconnect_notices:
|
if self.temp_disconnect_notices:
|
||||||
await self.send_bridge_notice("Connected to KakaoTalk chats")
|
await self.send_bridge_notice("Connected to KakaoTalk chats")
|
||||||
await self.push_bridge_state(BridgeStateEvent.CONNECTED)
|
await self.push_bridge_state(BridgeStateEvent.CONNECTED)
|
||||||
|
self.was_connected = True
|
||||||
|
await self.save()
|
||||||
|
|
||||||
async def on_disconnect(self, res: KickoutRes | None) -> None:
|
async def on_disconnect(self, res: KickoutRes | None) -> None:
|
||||||
self.is_connected = False
|
self.is_connected = False
|
||||||
self._track_metric(METRIC_CONNECTED, False)
|
self._track_metric(METRIC_CONNECTED, False)
|
||||||
|
logout = not self.config["bridge.remain_logged_in_on_disconnect"]
|
||||||
if res:
|
if res:
|
||||||
logout = False
|
|
||||||
if res.reason == KnownKickoutType.LOGIN_ANOTHER:
|
if res.reason == KnownKickoutType.LOGIN_ANOTHER:
|
||||||
reason_str = "Logged in from another desktop client."
|
reason_str = "Logged in on a PC or another bridge."
|
||||||
elif res.reason == KnownKickoutType.CHANGE_SERVER:
|
elif res.reason == KnownKickoutType.CHANGE_SERVER:
|
||||||
# TODO Reconnect automatically instead
|
# TODO Reconnect automatically instead
|
||||||
reason_str = "KakaoTalk backend server changed."
|
reason_str = "KakaoTalk backend server changed."
|
||||||
|
@ -661,12 +729,19 @@ class User(DBUser, BaseUser):
|
||||||
logout = True
|
logout = True
|
||||||
else:
|
else:
|
||||||
reason_str = f"Unknown reason ({res.reason})."
|
reason_str = f"Unknown reason ({res.reason})."
|
||||||
if not logout:
|
|
||||||
reason_suffix = " To reconnect, use the `sync` command."
|
|
||||||
# TODO What bridge state to push?
|
|
||||||
else:
|
else:
|
||||||
reason_suffix = " You are now logged out."
|
reason_str = None
|
||||||
|
if not logout:
|
||||||
|
# TODO What bridge state to push?
|
||||||
|
self.was_connected = False
|
||||||
|
await self.save()
|
||||||
|
else:
|
||||||
await self.logout()
|
await self.logout()
|
||||||
|
if reason_str:
|
||||||
|
if not logout:
|
||||||
|
reason_suffix = "To reconnect, use the `sync` command."
|
||||||
|
else:
|
||||||
|
reason_suffix = "You are now logged out. To log back in, use the `login` command."
|
||||||
await self.send_bridge_notice(f"Disconnected from KakaoTalk: {reason_str} {reason_suffix}")
|
await self.send_bridge_notice(f"Disconnected from KakaoTalk: {reason_str} {reason_suffix}")
|
||||||
|
|
||||||
async def on_error(self, error: JSON) -> None:
|
async def on_error(self, error: JSON) -> None:
|
||||||
|
@ -681,7 +756,7 @@ class User(DBUser, BaseUser):
|
||||||
self.is_connected = False
|
self.is_connected = False
|
||||||
self._track_metric(METRIC_CONNECTED, False)
|
self._track_metric(METRIC_CONNECTED, False)
|
||||||
self._client = None
|
self._client = None
|
||||||
if self._is_logged_in:
|
if self._is_logged_in and self.was_connected:
|
||||||
if self.temp_disconnect_notices:
|
if self.temp_disconnect_notices:
|
||||||
await self.send_bridge_notice(
|
await self.send_bridge_notice(
|
||||||
"Disconnected from KakaoTalk: backend helper module exited. "
|
"Disconnected from KakaoTalk: backend helper module exited. "
|
||||||
|
@ -702,7 +777,7 @@ class User(DBUser, BaseUser):
|
||||||
# Should also catch unlikely authentication errors
|
# Should also catch unlikely authentication errors
|
||||||
self._logged_in_info = await self._client.start()
|
self._logged_in_info = await self._client.start()
|
||||||
self._logged_in_info_time = time.monotonic()
|
self._logged_in_info_time = time.monotonic()
|
||||||
asyncio.create_task(self.post_login(is_startup=True))
|
asyncio.create_task(self.post_login(is_startup=True, is_token_login=False))
|
||||||
|
|
||||||
@async_time(METRIC_CHAT)
|
@async_time(METRIC_CHAT)
|
||||||
async def on_chat(self, chat: Chatlog, channel_id: Long, channel_type: ChannelType) -> None:
|
async def on_chat(self, chat: Chatlog, channel_id: Long, channel_type: ChannelType) -> None:
|
||||||
|
|
|
@ -520,6 +520,7 @@ export default class PeerClient {
|
||||||
* @param {Object} req
|
* @param {Object} req
|
||||||
* @param {string} req.uuid
|
* @param {string} req.uuid
|
||||||
* @param {Object} req.form
|
* @param {Object} req.form
|
||||||
|
* @param {boolean} req.forced
|
||||||
* @returns The response of the login attempt, including obtained
|
* @returns The response of the login attempt, including obtained
|
||||||
* credentials for subsequent token-based login. If a required device passcode
|
* credentials for subsequent token-based login. If a required device passcode
|
||||||
* request failed, its status is stored here.
|
* request failed, its status is stored here.
|
||||||
|
@ -527,7 +528,7 @@ export default class PeerClient {
|
||||||
handleLogin = async (req) => {
|
handleLogin = async (req) => {
|
||||||
// TODO Look for a logout API call
|
// TODO Look for a logout API call
|
||||||
const authClient = await this.#createAuthClient(req.uuid)
|
const authClient = await this.#createAuthClient(req.uuid)
|
||||||
const loginRes = await authClient.login(req.form, true)
|
const loginRes = await authClient.login(req.form, req.forced)
|
||||||
if (loginRes.status === KnownAuthStatusCode.DEVICE_NOT_REGISTERED) {
|
if (loginRes.status === KnownAuthStatusCode.DEVICE_NOT_REGISTERED) {
|
||||||
const passcodeRes = await authClient.requestPasscode(req.form)
|
const passcodeRes = await authClient.requestPasscode(req.form)
|
||||||
if (!passcodeRes.success) {
|
if (!passcodeRes.success) {
|
||||||
|
|
Loading…
Reference in New Issue