209 lines
5.8 KiB
Python
209 lines
5.8 KiB
Python
"""Entry point. Authorizes on Aternos
|
|
and allows to manage your account"""
|
|
|
|
import os
|
|
import re
|
|
from typing import Optional, List
|
|
|
|
from selenium.webdriver.common.by import By
|
|
from selenium.webdriver.support import expected_conditions as EC
|
|
from selenium.webdriver.remote.webelement import WebElement
|
|
|
|
from python_aternos.atserver import PartialServerInfo
|
|
|
|
from .atselenium import SeleniumHelper, Remote
|
|
|
|
from .atlog import log, is_debug, set_debug
|
|
from .aterrors import CredentialsError
|
|
|
|
|
|
class Client:
|
|
"""Aternos API Client class, object
|
|
of which contains user's auth data"""
|
|
|
|
def __init__(self, driver: Remote) -> None:
|
|
|
|
self.se = SeleniumHelper(driver)
|
|
|
|
# Config
|
|
self.sessions_dir = '~'
|
|
# ###
|
|
|
|
self.saved_session = '~/.aternos' # will be rewritten by login()
|
|
|
|
def login(
|
|
self,
|
|
username: str,
|
|
password: str,
|
|
code: Optional[int] = None) -> None:
|
|
"""Log in to your Aternos account
|
|
with a username and a plain password
|
|
|
|
Args:
|
|
username (str): Username
|
|
password (str): Plain-text password
|
|
code (Optional[int], optional): 2FA code
|
|
"""
|
|
|
|
self.se.load_page('/go')
|
|
|
|
err_block = self.se.find_element(By.CLASS_NAME, 'login-error')
|
|
err_alert = self.se.find_element(By.CLASS_NAME, 'alert-wrapper')
|
|
|
|
self.se.exec_js(f'''
|
|
document.getElementById('user').value = '{username}'
|
|
document.getElementById('password').value = '{password}'
|
|
document.getElementById('twofactor-code').value = '{code}'
|
|
login()
|
|
''')
|
|
|
|
def logged_in_or_error(driver: Remote):
|
|
return (
|
|
driver.current_url.find('/servers') != -1 or
|
|
err_block.is_displayed() or
|
|
err_alert.is_displayed()
|
|
)
|
|
|
|
self.se.wait.until(logged_in_or_error)
|
|
|
|
if self.se.driver.current_url.find('/go') != -1:
|
|
|
|
if err_block.is_displayed():
|
|
raise CredentialsError(err_block.text)
|
|
|
|
if err_alert.is_displayed():
|
|
raise CredentialsError(err_alert.text)
|
|
|
|
self.se.wait.until(lambda d: d.title.find('Cloudflare') == -1)
|
|
|
|
if not self.se.get_cookie('ATERNOS_SESSION'):
|
|
raise CredentialsError('Session cookie is empty')
|
|
|
|
print(self.se.get_cookie('ATERNOS_SESSION')) # TODO: remove, this is for debug
|
|
|
|
def login_with_session(self, session: str) -> None:
|
|
"""Log in using ATERNOS_SESSION cookie
|
|
|
|
Args:
|
|
session (str): Session cookie value
|
|
"""
|
|
|
|
self.se.set_cookie('ATERNOS_SESSION', session)
|
|
|
|
def logout(self) -> None:
|
|
"""Log out from the Aternos account"""
|
|
|
|
self.se.load_page('/servers')
|
|
self.se.find_element(By.CLASS_NAME, 'logout').click()
|
|
|
|
self.remove_session(self.saved_session)
|
|
|
|
def list_servers(self) -> List[PartialServerInfo]:
|
|
|
|
CARD_CLASS = 'servercard'
|
|
|
|
self.se.load_page('/servers')
|
|
|
|
def create_obj(s: WebElement) -> PartialServerInfo:
|
|
return PartialServerInfo(
|
|
id=s.get_dom_attribute('data-id'),
|
|
name=s.get_dom_attribute('title'),
|
|
software='',
|
|
status=(
|
|
s
|
|
.get_dom_attribute('class')
|
|
.replace(CARD_CLASS, '')
|
|
.split()[0]
|
|
),
|
|
players=0,
|
|
se=self.se,
|
|
)
|
|
|
|
return list(map(
|
|
create_obj,
|
|
self.se.find_elements(By.CLASS_NAME, CARD_CLASS),
|
|
))
|
|
|
|
def restore_session(self, file: str = '~/.aternos') -> None:
|
|
"""Restores ATERNOS_SESSION cookie from a session file
|
|
|
|
Args:
|
|
file (str, optional): Filename
|
|
|
|
Raises:
|
|
FileNotFoundError: If the file cannot be found
|
|
"""
|
|
|
|
file = os.path.expanduser(file)
|
|
log.debug('Restoring session from %s', file)
|
|
|
|
if not os.path.exists(file):
|
|
raise FileNotFoundError()
|
|
|
|
with open(file, 'rt', encoding='utf-8') as f:
|
|
session = f.readline().strip()
|
|
|
|
self.login_with_session(session)
|
|
self.saved_session = file
|
|
|
|
def save_session(self, file: str = '~/.aternos') -> None:
|
|
"""Saves an ATERNOS_SESSION cookie to a file
|
|
|
|
Args:
|
|
file (str, optional):
|
|
File where the session cookie must be saved
|
|
"""
|
|
|
|
file = os.path.expanduser(file)
|
|
log.debug('Saving session to %s', file)
|
|
|
|
with open(file, 'wt', encoding='utf-8') as f:
|
|
f.write(self.se.get_cookie('ATERNOS_SESSION') + '\n')
|
|
|
|
def remove_session(self, file: str = '~/.aternos') -> None:
|
|
"""Removes a file which contains
|
|
ATERNOS_SESSION cookie saved
|
|
with `save_session()`
|
|
|
|
Args:
|
|
file (str, optional): Filename
|
|
"""
|
|
|
|
file = os.path.expanduser(file)
|
|
log.debug('Removing session file: %s', file)
|
|
|
|
try:
|
|
os.remove(file)
|
|
except OSError as err:
|
|
log.warning('Unable to delete session file: %s', err)
|
|
|
|
@staticmethod
|
|
def session_filename(username: str, sessions_dir: str = '~') -> str:
|
|
"""Generates a session file name
|
|
|
|
Args:
|
|
username (str): Authenticated user
|
|
sessions_dir (str, optional): Path to directory
|
|
with automatically saved sessions
|
|
|
|
Returns:
|
|
Filename
|
|
"""
|
|
|
|
# unsafe symbols replacement
|
|
repl = '_'
|
|
|
|
secure = re.sub(
|
|
r'[^A-Za-z0-9_-]',
|
|
repl, username,
|
|
)
|
|
|
|
return f'{sessions_dir}/.at_{secure}'
|
|
|
|
@property
|
|
def debug(self) -> bool:
|
|
return is_debug()
|
|
|
|
@debug.setter
|
|
def debug(self, state: bool) -> None:
|
|
return set_debug(state)
|