diff --git a/examples/info_example.py b/examples/info_example.py index 4ce3a61..815e4eb 100644 --- a/examples/info_example.py +++ b/examples/info_example.py @@ -6,9 +6,18 @@ from python_aternos import Client user = input('Username: ') pswd = getpass('Password: ') -driver = Firefox() +with Firefox() as driver: -atclient = Client(driver) -atclient.login(user, pswd) + atclient = Client(driver) + atclient.login(user, pswd) -driver.quit() + servers = atclient.list_servers() + + # for serv in servers: + # print( + # serv.id, serv.name, + # serv.software, + # serv.status, + # serv.players, + # ) + list(map(print, servers)) diff --git a/python_aternos/atclient.py b/python_aternos/atclient.py index 6f2fe2d..16109b3 100644 --- a/python_aternos/atclient.py +++ b/python_aternos/atclient.py @@ -3,10 +3,15 @@ and allows to manage your account""" import os import re -from typing import Optional +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 .atconnect import AJAX_URL from .atlog import log, is_debug, set_debug from .aterrors import CredentialsError @@ -25,8 +30,6 @@ class Client: # ### self.saved_session = '~/.aternos' # will be rewritten by login() - # self.atconn = AternosConnect() - # self.account = AternosAccount(self) def login( self, @@ -44,27 +47,39 @@ class Client: self.se.load_page('/go') - user_input = self.se.find_by_id('user') - user_input.clear() - user_input.send_keys(username) + err_block = self.se.find_element(By.CLASS_NAME, 'login-error') + err_alert = self.se.find_element(By.CLASS_NAME, 'alert-wrapper') - pswd_input = self.se.find_by_id('password') - pswd_input.clear() - pswd_input.send_keys(password) - - err_msg = self.se.find_by_class('login-error') - totp_input = self.se.find_by_id('twofactor-code') + 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_msg.is_displayed() or \ - totp_input.is_displayed() + return ( + driver.current_url.find('/servers') != -1 or + err_block.is_displayed() or + err_alert.is_displayed() + ) - self.se.exec_js('login()') self.se.wait.until(logged_in_or_error) - print(self.se.driver.get_cookie('ATERNOS_SESSION')) + 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 @@ -73,32 +88,50 @@ class Client: session (str): Session cookie value """ - self.se.driver.add_cookie({ - 'name': 'ATERNOS_SESSION', - 'value': session, - }) + self.se.set_cookie('ATERNOS_SESSION', session) def logout(self) -> None: """Log out from the Aternos account""" - self.atconn.request_cloudflare( - f'{AJAX_URL}/account/logout', - 'GET', sendtoken=True, - ) + 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 and, - if included, servers list, from a session file + """Restores ATERNOS_SESSION cookie from a session file Args: file (str, optional): Filename Raises: FileNotFoundError: If the file cannot be found - CredentialsError: If the session cookie - (or the file at all) has incorrect format """ file = os.path.expanduser(file) @@ -108,48 +141,24 @@ class Client: raise FileNotFoundError() with open(file, 'rt', encoding='utf-8') as f: - saved = f.read() \ - .strip() \ - .replace('\r\n', '\n') \ - .split('\n') + session = f.readline().strip() - session = saved[0].strip() - if session == '' or not session.isalnum(): - raise CredentialsError( - 'Session cookie is invalid or the file is empty' - ) - - if len(saved) > 1: - self.account.refresh_servers(saved[1:]) - - self.atconn.session.cookies['ATERNOS_SESSION'] = session + self.login_with_session(session) self.saved_session = file - def save_session( - self, - file: str = '~/.aternos', - incl_servers: bool = True) -> None: + def save_session(self, file: str = '~/.aternos') -> None: """Saves an ATERNOS_SESSION cookie to a file Args: - file (str, optional): File where a session cookie must be saved - incl_servers (bool, optional): If the function - should include the servers IDs in this file - to reduce API requests count on the next restoration - (recommended) + 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.atconn.atsession + '\n') - if not incl_servers: - return - - for s in self.account.servers: - f.write(s.servid + '\n') + f.write(self.se.get_cookie('ATERNOS_SESSION') + '\n') def remove_session(self, file: str = '~/.aternos') -> None: """Removes a file which contains diff --git a/python_aternos/atselenium.py b/python_aternos/atselenium.py index da8a287..bd81ef0 100644 --- a/python_aternos/atselenium.py +++ b/python_aternos/atselenium.py @@ -1,22 +1,24 @@ from selenium.webdriver import Remote -from selenium.webdriver.remote.webelement import WebElement from selenium.webdriver.support.wait import WebDriverWait -from selenium.webdriver.common.by import By BASE_URL = 'https://aternos.org' RM_SCRIPTS = ''' -const lst = document.querySelectorAll("script") -for (let js of lst) { - if ( - js.src.includes('googletagmanager.com') || - js.src.includes('cloudflareinsights.com') || - js.innerText.includes('LANGUAGE_VARIABLES') - ) { - js.remove() +const rmScripts = () => { + const lst = document.querySelectorAll("script") + for (let js of lst) { + if ( + js.src.includes('googletagmanager.com') || + js.src.includes('cloudflareinsights.com') || + js.innerText.includes('LANGUAGE_VARIABLES') + ) { + js.remove() + } } } +addEventListener('DOMContentLoaded', rmScripts) +rmScripts() ''' @@ -24,17 +26,25 @@ class SeleniumHelper: def __init__(self, driver: Remote) -> None: self.driver = driver - self.wait = WebDriverWait(driver, 2.0) + self.wait = WebDriverWait(driver, 8.0) + self.find_element = self.driver.find_element + self.find_elements = self.driver.find_elements def load_page(self, path: str) -> None: self.driver.get(f'{BASE_URL}{path}') self.driver.execute_script(RM_SCRIPTS) - - def find_by_id(self, value: str) -> WebElement: - return self.driver.find_element(By.ID, value) - - def find_by_class(self, value: str) -> WebElement: - return self.driver.find_element(By.CLASS_NAME, value) def exec_js(self, script: str) -> None: self.driver.execute_script(script) + + def get_cookie(self, name: str) -> str: + cookie = self.driver.get_cookie(name) + if cookie is None: + return '' + return cookie.get('value') or '' + + def set_cookie(self, name: str, value: str) -> None: + self.driver.add_cookie({ + 'name': name, + 'value': value, + }) diff --git a/python_aternos/atserver.py b/python_aternos/atserver.py index 0667142..f9058a4 100644 --- a/python_aternos/atserver.py +++ b/python_aternos/atserver.py @@ -1,26 +1,26 @@ """Aternos Minecraft server""" import re -import json import enum -from typing import Any, Dict, List -from functools import partial +from typing import List -from .atconnect import BASE_URL, AJAX_URL -from .atconnect import AternosConnect -from .atwss import AternosWss +from .atselenium import SeleniumHelper -from .atplayers import PlayersList -from .atplayers import Lists +from .atconnect import AJAX_URL -from .atfm import FileManager -from .atconf import AternosConfig +from .atstubs import PlayersList +from .atstubs import Lists + +from .atstubs import FileManager +from .atstubs import AternosConfig -from .aterrors import AternosError from .aterrors import ServerStartError +from dataclasses import dataclass + + SERVER_URL = f'{AJAX_URL}/server' status_re = re.compile( r'