This repository has been archived on 2024-07-30. You can view files and clone it, but cannot push or open issues or pull requests.
python-aternos/python_aternos/atclient.py

521 lines
14 KiB
Python
Raw Normal View History

2022-07-01 14:28:39 +04:00
"""Entry point. Authorizes on Aternos
and allows to manage your account"""
2022-06-16 15:40:10 +04:00
import os
import re
import hashlib
import logging
2022-09-29 19:17:38 +04:00
import base64
from typing import List, Dict
from typing import Optional, Type
2022-06-16 15:40:10 +04:00
import lxml.html
2022-06-16 15:40:10 +04:00
from .atserver import AternosServer
from .atconnect import AternosConnect
from .aterrors import CredentialsError
2022-09-23 17:21:17 +04:00
from .aterrors import TwoFactorAuthError
2022-06-16 15:40:10 +04:00
from . import atjsparse
from .atjsparse import Interpreter
from .atjsparse import Js2PyInterpreter
2022-06-23 15:13:56 +04:00
2022-06-16 15:40:10 +04:00
class Client:
2022-09-23 16:53:47 +04:00
"""Aternos API Client class, object
of which contains user's auth data"""
2022-06-23 15:13:56 +04:00
def __init__(
self,
atconn: AternosConnect,
servers: Optional[List[str]] = None) -> None:
2022-09-23 16:53:47 +04:00
"""Aternos API Client class, object
of which contains user's auth data
Args:
atconn (AternosConnect):
AternosConnect instance with initialized Aternos session
servers (Optional[List[str]], optional):
List with servers IDs
"""
2022-06-23 15:13:56 +04:00
self.atconn = atconn
self.saved_session = ''
self.parsed = False
self.servers: List[AternosServer] = []
if servers:
self.refresh_servers(servers)
2022-06-23 15:13:56 +04:00
@classmethod
def from_hashed(
cls,
username: str,
md5: str,
2022-09-23 17:21:17 +04:00
code: Optional[int] = None,
2022-10-05 19:59:28 +04:00
sessions_dir: str = '~',
js: Type[Interpreter] = Js2PyInterpreter,
2022-10-05 19:59:28 +04:00
**custom_args):
"""Log in to an Aternos account with
a username and a hashed password
Args:
username (str): Your username
md5 (str): Your password hashed with MD5
2022-09-23 17:21:17 +04:00
code (Optional[int]): 2FA code
sessions_dir (str): Path to the directory
where session will be automatically saved
js (Type[Interpreter]): Preferred JS interpreter,
any class from `atjsparse`
inheriting `Interpreter` class
2022-10-05 19:59:28 +04:00
**custom_args (tuple, optional): Keyword arguments
which will be passed to CloudScraper `__init__`
Raises:
CredentialsError: If the API didn't
return a valid session cookie
2022-06-23 15:13:56 +04:00
"""
filename = cls.session_file(
username, sessions_dir
)
try:
return cls.restore_session(
filename, **custom_args
)
except (OSError, CredentialsError):
pass
atjsparse.get_interpreter(create=js)
atconn = AternosConnect()
if len(custom_args) > 0:
atconn.add_args(**custom_args)
2022-09-29 18:55:37 +04:00
atconn.parse_token()
atconn.generate_sec()
2022-06-23 15:13:56 +04:00
credentials = {
'user': username,
2022-09-23 17:21:17 +04:00
'password': md5,
2022-06-23 15:13:56 +04:00
}
2022-09-23 17:21:17 +04:00
if code is not None:
2022-09-29 18:18:15 +04:00
credentials['code'] = str(code)
2022-09-23 17:21:17 +04:00
2022-06-23 15:13:56 +04:00
loginreq = atconn.request_cloudflare(
2022-07-01 14:28:39 +04:00
'https://aternos.org/panel/ajax/account/login.php',
2022-06-23 15:13:56 +04:00
'POST', data=credentials, sendtoken=True
)
2022-09-23 17:21:17 +04:00
if b'"show2FA":true' in loginreq.content:
raise TwoFactorAuthError('2FA code is required')
2022-06-23 15:13:56 +04:00
if 'ATERNOS_SESSION' not in loginreq.cookies:
raise CredentialsError(
'Check your username and password'
)
obj = cls(atconn)
obj.saved_session = filename
try:
obj.save_session(filename)
except OSError:
pass
return obj
2022-06-23 15:13:56 +04:00
@classmethod
def from_credentials(
cls,
username: str,
password: str,
2022-09-23 17:21:17 +04:00
code: Optional[int] = None,
2022-10-05 19:59:28 +04:00
sessions_dir: str = '~',
js: Type[Interpreter] = Js2PyInterpreter,
2022-10-05 19:59:28 +04:00
**custom_args):
2022-06-23 15:13:56 +04:00
"""Log in to Aternos with a username and a plain password
Args:
username (str): Your username
password (str): Your password without any encryption
2022-09-23 17:21:17 +04:00
code (Optional[int]): 2FA code
sessions_dir (str): Path to the directory
where session will be automatically saved
js (Type[Interpreter]): Preferred JS interpreter,
any class from `atjsparse`
inheriting `Interpreter` class
2022-10-05 19:59:28 +04:00
**custom_args (tuple, optional): Keyword arguments
which will be passed to CloudScraper `__init__`
2022-06-23 15:13:56 +04:00
"""
md5 = Client.md5encode(password)
return cls.from_hashed(
2022-09-23 17:21:17 +04:00
username, md5, code,
sessions_dir, js,
**custom_args
)
2022-06-23 15:13:56 +04:00
@classmethod
def from_session(
cls,
session: str,
2022-10-05 19:59:28 +04:00
servers: Optional[List[str]] = None,
js: Type[Interpreter] = Js2PyInterpreter,
2022-10-05 19:59:28 +04:00
**custom_args):
2022-06-23 15:13:56 +04:00
"""Log in to Aternos using a session cookie value
Args:
session (str): Value of ATERNOS_SESSION cookie
servers (Optional[List[str]]): List of cached servers IDs.
js (Type[Interpreter]): Preferred JS interpreter,
any class from `atjsparse`
inheriting `Interpreter` class
2022-10-05 19:59:28 +04:00
**custom_args (tuple, optional): Keyword arguments
which will be passed to CloudScraper `__init__`
2022-06-23 15:13:56 +04:00
"""
atjsparse.get_interpreter(create=js)
2022-06-23 15:13:56 +04:00
atconn = AternosConnect()
2022-10-05 19:59:28 +04:00
atconn.add_args(**custom_args)
2022-06-23 15:13:56 +04:00
atconn.session.cookies['ATERNOS_SESSION'] = session
2022-10-05 19:59:28 +04:00
2022-06-23 15:13:56 +04:00
atconn.parse_token()
atconn.generate_sec()
return cls(atconn, servers)
2022-06-23 15:13:56 +04:00
@classmethod
2022-10-05 19:59:28 +04:00
def restore_session(
cls,
file: str = '~/.aternos',
js: Type[Interpreter] = Js2PyInterpreter,
2022-10-05 19:59:28 +04:00
**custom_args):
"""Log in to Aternos using
a saved ATERNOS_SESSION cookie
2022-06-23 15:13:56 +04:00
Args:
file (str, optional): File where a session cookie was saved
js (Type[Interpreter]): Preferred JS interpreter,
any class from `atjsparse`
inheriting `Interpreter` class
2022-10-05 19:59:28 +04:00
**custom_args (tuple, optional): Keyword arguments
which will be passed to CloudScraper `__init__`
2022-06-23 15:13:56 +04:00
"""
file = os.path.expanduser(file)
2022-09-30 14:39:16 +04:00
logging.debug('Restoring session from %s', file)
if not os.path.exists(file):
raise FileNotFoundError()
with open(file, 'rt', encoding='utf-8') as f:
saved = f.read() \
.strip() \
.replace('\r\n', '\n') \
.split('\n')
session = saved[0].strip()
if session == '':
raise CredentialsError(
'Unable to read session cookie, '
'the first line is empty'
)
if len(saved) > 1:
2022-09-30 14:23:50 +04:00
obj = cls.from_session(
session=session,
2022-10-05 19:59:28 +04:00
servers=saved[1:],
js=js,
2022-10-05 19:59:28 +04:00
**custom_args
)
2022-09-30 14:23:50 +04:00
else:
2022-10-05 19:59:28 +04:00
obj = cls.from_session(
session,
js=js,
2022-10-05 19:59:28 +04:00
**custom_args
)
obj.saved_session = file
return obj
2022-06-23 15:13:56 +04:00
@staticmethod
def md5encode(passwd: str) -> str:
"""Encodes the given string with MD5
Args:
passwd (str): String to encode
Returns:
Hexdigest hash of the string in lowercase
2022-06-23 15:13:56 +04:00
"""
encoded = hashlib.md5(passwd.encode('utf-8'))
return encoded.hexdigest().lower()
@staticmethod
def session_file(username: str, sessions_dir: str = '~') -> str:
"""Generates session file name
for authenticated user
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}'
def save_session(
self,
file: str = '~/.aternos',
incl_servers: bool = True) -> None:
2022-06-23 15:13:56 +04:00
"""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 to
reduce API requests count (recommended)
2022-06-23 15:13:56 +04:00
"""
file = os.path.expanduser(file)
2022-09-30 14:39:16 +04:00
logging.debug('Saving session to %s', file)
with open(file, 'wt', encoding='utf-8') as f:
2022-06-23 15:13:56 +04:00
f.write(self.atconn.atsession + '\n')
if not incl_servers:
return
for s in self.servers:
f.write(s.servid + '\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)
2022-09-30 14:39:16 +04:00
logging.debug('Removing session file: %s', file)
try:
os.remove(file)
except OSError as err:
2022-09-30 14:39:16 +04:00
logging.warning('Unable to delete session file: %s', err)
def list_servers(self, cache: bool = True) -> List[AternosServer]:
2022-06-23 15:13:56 +04:00
"""Parses a list of your servers from Aternos website
Args:
cache (bool, optional): If the function should use
cached servers list (recommended)
Returns:
List of AternosServer objects
2022-06-23 15:13:56 +04:00
"""
if cache and self.parsed:
return self.servers
2022-06-23 15:13:56 +04:00
serverspage = self.atconn.request_cloudflare(
'https://aternos.org/servers/', 'GET'
)
serverstree = lxml.html.fromstring(serverspage.content)
servers = serverstree.xpath(
'//div[@class="server-body"]/@data-id'
2022-06-23 15:13:56 +04:00
)
self.refresh_servers(servers)
# Update session file (add servers)
try:
self.save_session(self.saved_session)
except OSError as err:
2022-09-30 14:39:16 +04:00
logging.warning('Unable to save servers list to file: %s', err)
return self.servers
def refresh_servers(self, ids: List[str]) -> None:
"""Replaces cached servers list creating
AternosServer objects by given IDs
Args:
ids (List[str]): Servers unique identifiers
"""
self.servers = []
for s in ids:
servid = s.strip()
if servid == '':
continue
2022-06-23 15:13:56 +04:00
2022-09-30 14:39:16 +04:00
logging.debug('Adding server %s', servid)
srv = AternosServer(servid, self.atconn)
self.servers.append(srv)
2022-06-23 15:13:56 +04:00
self.parsed = True
2022-06-16 15:40:10 +04:00
2022-06-23 15:13:56 +04:00
def get_server(self, servid: str) -> AternosServer:
"""Creates a server object from the server ID.
Use this instead of list_servers
if you know the ID to save some time.
2022-06-16 15:40:10 +04:00
Returns:
AternosServer object
2022-06-23 15:13:56 +04:00
"""
2022-06-16 15:40:10 +04:00
2022-06-23 15:13:56 +04:00
return AternosServer(servid, self.atconn)
def logout(self) -> None:
"""Log out from Aternos account"""
self.atconn.request_cloudflare(
'https://aternos.org/panel/ajax/account/logout.php',
'GET', sendtoken=True
)
2022-06-16 15:40:10 +04:00
self.remove_session(self.saved_session)
2022-06-23 15:13:56 +04:00
def change_username(self, value: str) -> None:
"""Changes a username in your Aternos account
2022-06-16 15:40:10 +04:00
Args:
value (str): New username
2022-06-23 15:13:56 +04:00
"""
2022-06-16 15:40:10 +04:00
2022-06-23 15:13:56 +04:00
self.atconn.request_cloudflare(
'https://aternos.org/panel/ajax/account/username.php',
'POST', data={'username': value}, sendtoken=True
2022-06-23 15:13:56 +04:00
)
2022-06-16 15:40:10 +04:00
2022-06-23 15:13:56 +04:00
def change_email(self, value: str) -> None:
"""Changes an e-mail in your Aternos account
2022-06-16 15:40:10 +04:00
Args:
value (str): New e-mail
Raises:
ValueError: If an invalid e-mail address
was passed to the function
2022-06-23 15:13:56 +04:00
"""
2022-06-16 15:40:10 +04:00
2022-06-23 15:13:56 +04:00
email = re.compile(
r'^[A-Za-z0-9\-_+.]+@[A-Za-z0-9\-_+.]+\.[A-Za-z0-9\-]+$|^$'
)
if not email.match(value):
raise ValueError('Invalid e-mail!')
2022-06-16 15:40:10 +04:00
2022-06-23 15:13:56 +04:00
self.atconn.request_cloudflare(
'https://aternos.org/panel/ajax/account/email.php',
'POST', data={'email': value}, sendtoken=True
2022-06-23 15:13:56 +04:00
)
2022-06-16 15:40:10 +04:00
2022-06-23 15:13:56 +04:00
def change_password(self, old: str, new: str) -> None:
"""Changes a password in your Aternos account
2022-06-16 15:40:10 +04:00
Args:
old (str): Old password
new (str): New password
"""
self.change_password_hashed(
Client.md5encode(old),
Client.md5encode(new),
)
def change_password_hashed(self, old: str, new: str) -> None:
"""Changes a password in your Aternos account.
Unlike `change_password`, this function
takes hashed passwords as arguments
Args:
old (str): Old password hashed with MD5
new (str): New password hashed with MD5
2022-06-23 15:13:56 +04:00
"""
2022-06-16 15:40:10 +04:00
2022-06-23 15:13:56 +04:00
self.atconn.request_cloudflare(
'https://aternos.org/panel/ajax/account/password.php',
'POST', data={
'oldpassword': old,
2022-09-23 17:21:17 +04:00
'newpassword': new,
}, sendtoken=True
)
def qrcode_2fa(self) -> Dict[str, str]:
"""Requests a secret code and
a QR code for enabling 2FA"""
2022-09-29 18:54:16 +04:00
return self.atconn.request_cloudflare(
2022-09-23 17:21:17 +04:00
'https://aternos.org/panel/ajax/account/secret.php',
'GET', sendtoken=True
).json()
2022-09-29 19:17:38 +04:00
def save_qr(self, qrcode: str, filename: str) -> None:
"""Writes a 2FA QR code into a png-file
Args:
qrcode (str): Base64 encoded png image from `qrcode_2fa()`
filename (str): Where the QR code image must be saved.
Existing file will be rewritten.
"""
data = qrcode.removeprefix('data:image/png;base64,')
png = base64.b64decode(data)
with open(filename, 'wb') as f:
f.write(png)
2022-09-23 17:21:17 +04:00
2022-09-29 19:17:38 +04:00
def enable_2fa(self, code: int) -> None:
2022-09-23 17:21:17 +04:00
"""Enables Two-Factor Authentication
Args:
code (int): 2FA code
"""
self.atconn.request_cloudflare(
'https://aternos.org/panel/ajax/account/twofactor.php',
'POST', data={
'code': code
}, sendtoken=True
)
def disable_2fa(self, code: int) -> None:
"""Disables Two-Factor Authentication
Args:
code (int): 2FA code
"""
self.atconn.request_cloudflare(
'https://aternos.org/panel/ajax/account/disbaleTwofactor.php',
'POST', data={
'code': code
}, sendtoken=True
2022-06-23 15:13:56 +04:00
)