2022-02-25 02:22:50 -05:00
# 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/>.
2022-04-10 23:40:11 -04:00
#import time
2022-02-25 02:22:50 -05:00
2022-04-10 23:40:11 -04:00
#from yarl import URL
2022-02-25 02:22:50 -05:00
from mautrix . bridge . commands import HelpSection , command_handler
from mautrix . errors import MForbidden
2022-04-10 23:40:11 -04:00
#from mautrix.util.signed_token import sign_token
2022-02-25 02:22:50 -05:00
from . . kt . client import Client as KakaoTalkClient
2022-05-04 02:50:59 -04:00
from . . kt . client . errors import (
AnotherLogonDetected ,
CommandException ,
DeviceVerificationRequired ,
IncorrectPasscode ,
IncorrectPassword ,
)
2022-02-25 02:22:50 -05:00
2022-03-06 03:14:59 -05:00
from . . import puppet as pu
2022-05-04 02:50:59 -04:00
from . . db import LoginCredential
2022-02-25 02:22:50 -05:00
from . typehint import CommandEvent
SECTION_AUTH = HelpSection ( " Authentication " , 10 , " " )
2022-04-10 23:40:11 -04:00
""" TODO Implement web login
2022-02-25 02:22:50 -05:00
web_unsupported = (
" This instance of the KakaoTalk bridge does not support the web-based login interface "
)
alternative_web_login = (
" Alternatively, you may use [the web-based login interface]( {url} ) "
" to prevent the bridge and homeserver from seeing your password "
)
forced_web_login = (
" This instance of the KakaoTalk bridge does not allow in-Matrix login. "
" Please use [the web-based login interface]( {url} ). "
)
2022-04-10 23:40:11 -04:00
"""
2022-02-25 02:22:50 -05:00
send_password = " Please send your password here to log in "
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. "
2022-05-04 02:50:59 -04:00
_CMD_CONTINUE_LOGIN = " continue-login "
2022-02-25 02:22:50 -05:00
@command_handler (
needs_auth = False ,
management_only = True ,
help_section = SECTION_AUTH ,
2022-05-04 02:50:59 -04:00
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 = " [--save] [_email_] " ,
2022-02-25 02:22:50 -05:00
)
async def login ( evt : CommandEvent ) - > None :
2022-03-06 03:14:59 -05:00
if await evt . sender . is_logged_in ( ) :
2022-02-25 02:22:50 -05:00
await evt . reply ( " You ' re already logged in " )
return
2022-05-04 02:50:59 -04:00
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
2022-02-25 02:22:50 -05:00
if email :
evt . sender . command_status = {
" action " : " Login " ,
" room_id " : evt . room_id ,
2022-05-04 02:50:59 -04:00
" next " : _enter_password ,
2022-05-02 03:13:18 -04:00
" email " : email ,
2022-05-04 02:50:59 -04:00
" save " : save ,
" forced " : evt . sender . force_login ,
2022-02-25 02:22:50 -05:00
}
2022-05-04 02:50:59 -04:00
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
2022-02-25 02:22:50 -05:00
2022-04-10 23:40:11 -04:00
""" TODO Implement web login
2022-02-25 02:22:50 -05:00
if evt . bridge . public_website :
external_url = URL ( evt . config [ " appservice.public.external " ] )
token = sign_token (
evt . bridge . public_website . secret_key ,
{
" mxid " : evt . sender . mxid ,
" expiry " : int ( time . time ( ) ) + 30 * 60 ,
} ,
)
url = ( external_url / " login.html " ) . with_fragment ( token )
if not evt . config [ " appservice.public.allow_matrix_login " ] :
await evt . reply ( forced_web_login . format ( url = url ) )
elif email :
await evt . reply ( f " { send_password } . { alternative_web_login . format ( url = url ) } . " )
else :
await evt . reply ( f " { missing_email } . { alternative_web_login . format ( url = url ) } . " )
elif not email :
await evt . reply ( f " { missing_email } . { web_unsupported } . " )
else :
await evt . reply ( f " { send_password } . { web_unsupported } . " )
2022-04-10 23:40:11 -04:00
"""
if not email :
await evt . reply ( f " { missing_email } . " )
else :
2022-05-04 02:50:59 -04:00
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 } " )
2022-02-25 02:22:50 -05:00
2022-05-04 02:50:59 -04:00
async def _enter_password ( evt : CommandEvent ) - > None :
2022-02-25 02:22:50 -05:00
try :
await evt . az . intent . redact ( evt . room_id , evt . event_id )
except MForbidden :
2022-05-04 02:50:59 -04:00
await evt . mark_read ( )
2022-03-23 03:15:02 -04:00
assert evt . sender . command_status
2022-05-04 02:50:59 -04:00
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 :
2022-02-25 02:22:50 -05:00
req = {
" uuid " : await evt . sender . get_uuid ( ) ,
" form " : {
2022-05-04 02:50:59 -04:00
" email " : email ,
" password " : password ,
} ,
" forced " : forced ,
2022-02-25 02:22:50 -05:00
}
2022-05-04 02:50:59 -04:00
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
)
2022-02-25 02:22:50 -05:00
try :
2022-05-04 02:50:59 -04:00
await _do_login ( evt , req , save )
except DeviceVerificationRequired as e :
if evt . sender . command_status and evt . sender . command_status . get ( " next " ) != _enter_dv_code :
await evt . reply (
" Open KakaoTalk on your smartphone. It should show a device registration passcode. "
" Enter that passcode here. "
)
evt . sender . command_status = {
" action " : " Login " ,
" room_id " : evt . room_id ,
" next " : _enter_dv_code ,
" save " : save ,
" 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 :
await evt . reply ( f " Incorrect password. { try_again_or_cancel } " )
2022-02-25 02:22:50 -05:00
evt . sender . command_status = {
" action " : " Login " ,
" room_id " : evt . room_id ,
2022-05-04 02:50:59 -04:00
" next " : _enter_password ,
" email " : req [ " form " ] [ " email " ] ,
" save " : save ,
" forced " : req [ " forced " ] ,
2022-02-25 02:22:50 -05:00
}
#except OAuthException as e:
# await evt.reply(f"Error from KakaoTalk:\n\n> {e}")
except Exception as e :
await _handle_login_failure ( evt , e )
2022-05-04 02:50:59 -04:00
async def _enter_dv_code ( evt : CommandEvent ) - > None :
2022-03-23 03:15:02 -04:00
assert evt . sender . command_status
2022-05-04 02:50:59 -04:00
req : dict = evt . sender . command_status . pop ( " req " )
2022-02-25 02:22:50 -05:00
passcode = evt . content . body
2022-03-09 02:26:17 -05:00
await evt . mark_read ( )
2022-02-25 02:22:50 -05:00
try :
await KakaoTalkClient . register_device ( passcode , * * req )
except IncorrectPasscode :
await evt . reply ( f " Incorrect device registration passcode. { try_again_or_cancel } " )
#except OAuthException as e:
# await evt.reply(f"Error from KakaoTalk:\n\n> {e}")
except Exception as e :
await _handle_login_failure ( evt , e )
2022-05-04 02:50:59 -04:00
else :
await _try_login ( evt , req )
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 " ) )
2022-02-25 02:22:50 -05:00
2022-05-04 02:50:59 -04:00
async def _do_login ( evt : CommandEvent , req : dict , save : bool ) - > None :
2022-02-25 02:22:50 -05:00
oauth_credential = await KakaoTalkClient . login ( * * req )
await evt . sender . on_logged_in ( oauth_credential )
evt . sender . command_status = None
await evt . reply ( " Successfully logged in " )
2022-05-04 02:50:59 -04:00
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 " )
2022-02-25 02:22:50 -05:00
async def _handle_login_failure ( evt : CommandEvent , e : Exception ) - > None :
evt . sender . command_status = None
if isinstance ( e , CommandException ) :
message = " Failed to log in "
evt . log . error ( message )
else :
message = " Error while logging in "
evt . log . exception ( message )
await evt . reply ( f " { message } : { e } " )
2022-05-04 02:50:59 -04:00
@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. "
)
2022-03-06 03:14:59 -05:00
@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] " ,
)
2022-02-25 02:22:50 -05:00
async def logout ( evt : CommandEvent ) - > None :
2022-03-06 03:14:59 -05:00
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 :
2022-03-09 02:26:17 -05:00
await evt . mark_read ( )
2022-03-06 03:14:59 -05:00
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. "
)
2022-05-04 02:50:59 -04:00
@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. " )