MkDocs, Readme, Files API, Automated session saving, v2.0.1
MkDocs: sphinx docstrings rewritten to google, improved config, written the major part of how-to. Readme: centered title + logo, added badges, features list, updated changelog. Improved Files API, added automatical session saving and restoring to Client. Some changes in makefile and gitignore. License Notice now refers to all contributors.
This commit is contained in:
parent
dc52f92985
commit
4892430f19
39 changed files with 1832 additions and 569 deletions
|
@ -4,6 +4,7 @@ and allows to manage your account"""
|
|||
import os
|
||||
import re
|
||||
import hashlib
|
||||
import logging
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
|
@ -16,18 +17,24 @@ from .aterrors import CredentialsError
|
|||
|
||||
class Client:
|
||||
|
||||
"""Aternos API Client class object of which contains user's auth data
|
||||
|
||||
:param atconn: :class:`python_aternos.atconnect.AternosConnect`
|
||||
instance with initialized Aternos session
|
||||
:type atconn: python_aternos.atconnect.AternosConnect
|
||||
"""
|
||||
"""Aternos API Client class object
|
||||
of which contains user's auth data"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
atconn: AternosConnect,
|
||||
servers: Optional[List[str]] = None) -> None:
|
||||
|
||||
"""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
|
||||
"""
|
||||
|
||||
self.atconn = atconn
|
||||
self.parsed = False
|
||||
self.servers: List[AternosServer] = []
|
||||
|
@ -36,24 +43,38 @@ class Client:
|
|||
self.refresh_servers(servers)
|
||||
|
||||
@classmethod
|
||||
def from_hashed(cls, username: str, md5: str):
|
||||
def from_hashed(
|
||||
cls,
|
||||
username: str,
|
||||
md5: str,
|
||||
sessions_dir: str = '~'):
|
||||
|
||||
"""Log in to Aternos with a username and a hashed password
|
||||
"""Log in to an Aternos account with
|
||||
a username and a hashed password
|
||||
|
||||
:param username: Your username
|
||||
:type username: str
|
||||
:param md5: Your password hashed with MD5
|
||||
:type md5: str
|
||||
:raises CredentialsError: If the API
|
||||
doesn't return a valid session cookie
|
||||
:return: Client instance
|
||||
:rtype: python_aternos.Client
|
||||
Args:
|
||||
username (str): Your username
|
||||
md5 (str): Your password hashed with MD5
|
||||
sessions_dir (str): Path to the directory
|
||||
where session will be automatically saved
|
||||
|
||||
Raises:
|
||||
CredentialsError: If the API didn't
|
||||
return a valid session cookie
|
||||
"""
|
||||
|
||||
atconn = AternosConnect()
|
||||
atconn.parse_token()
|
||||
atconn.generate_sec()
|
||||
|
||||
secure = cls.secure_name(username)
|
||||
filename = f'{sessions_dir}/.at_{secure}'
|
||||
|
||||
try:
|
||||
return cls.restore_session(filename)
|
||||
except (OSError, CredentialsError):
|
||||
pass
|
||||
|
||||
credentials = {
|
||||
'user': username,
|
||||
'password': md5
|
||||
|
@ -69,23 +90,36 @@ class Client:
|
|||
'Check your username and password'
|
||||
)
|
||||
|
||||
return cls(atconn)
|
||||
obj = cls(atconn)
|
||||
|
||||
try:
|
||||
obj.save_session(filename)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
def from_credentials(cls, username: str, password: str):
|
||||
def from_credentials(
|
||||
cls,
|
||||
username: str,
|
||||
password: str,
|
||||
sessions_dir: str = '~'):
|
||||
|
||||
"""Log in to Aternos with a username and a plain password
|
||||
|
||||
:param username: Your username
|
||||
:type username: str
|
||||
:param password: Your password without any encryption
|
||||
:type password: str
|
||||
:return: Client instance
|
||||
:rtype: python_aternos.Client
|
||||
Args:
|
||||
username (str): Your username
|
||||
password (str): Your password without any encryption
|
||||
sessions_dir (str): Path to the directory
|
||||
where session will be automatically saved
|
||||
"""
|
||||
|
||||
md5 = Client.md5encode(password)
|
||||
return cls.from_hashed(username, md5)
|
||||
return cls.from_hashed(
|
||||
username, md5,
|
||||
sessions_dir
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_session(
|
||||
|
@ -95,10 +129,8 @@ class Client:
|
|||
|
||||
"""Log in to Aternos using a session cookie value
|
||||
|
||||
:param session: Value of ATERNOS_SESSION cookie
|
||||
:type session: str
|
||||
:return: Client instance
|
||||
:rtype: python_aternos.Client
|
||||
Args:
|
||||
session (str): Value of ATERNOS_SESSION cookie
|
||||
"""
|
||||
|
||||
atconn = AternosConnect()
|
||||
|
@ -113,18 +145,28 @@ class Client:
|
|||
|
||||
"""Log in to Aternos using a saved ATERNOS_SESSION cookie
|
||||
|
||||
:param file: File where a session cookie
|
||||
was saved, deafults to `~/.aternos`
|
||||
:type file: str, optional
|
||||
:return: Client instance
|
||||
:rtype: python_aternos.Client
|
||||
Args:
|
||||
file (str, optional): File where a session cookie was saved
|
||||
"""
|
||||
|
||||
file = os.path.expanduser(file)
|
||||
logging.debug(f'Restoring session from {file}')
|
||||
|
||||
if not os.path.exists(file):
|
||||
raise FileNotFoundError()
|
||||
|
||||
with open(file, 'rt', encoding='utf-8') as f:
|
||||
saved = f.read().replace('\r\n', '\n').split('\n')
|
||||
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:
|
||||
return cls.from_session(
|
||||
|
@ -139,15 +181,36 @@ class Client:
|
|||
|
||||
"""Encodes the given string with MD5
|
||||
|
||||
:param passwd: String to encode
|
||||
:type passwd: str
|
||||
:return: Hexdigest hash of the string in lowercase
|
||||
:rtype: str
|
||||
Args:
|
||||
passwd (str): String to encode
|
||||
|
||||
Returns:
|
||||
Hexdigest hash of the string in lowercase
|
||||
"""
|
||||
|
||||
encoded = hashlib.md5(passwd.encode('utf-8'))
|
||||
return encoded.hexdigest().lower()
|
||||
|
||||
@staticmethod
|
||||
def secure_name(filename: str, repl: str = '_') -> str:
|
||||
|
||||
"""Replaces unsecure characters
|
||||
in filename to underscore or `repl`
|
||||
|
||||
Args:
|
||||
filename (str): Filename
|
||||
repl (str, optional): Replacement
|
||||
for unsafe characters
|
||||
|
||||
Returns:
|
||||
str: Secure filename
|
||||
"""
|
||||
|
||||
return re.sub(
|
||||
r'[^A-Za-z0-9_-]',
|
||||
repl, filename
|
||||
)
|
||||
|
||||
def save_session(
|
||||
self,
|
||||
file: str = '~/.aternos',
|
||||
|
@ -155,17 +218,16 @@ class Client:
|
|||
|
||||
"""Saves an ATERNOS_SESSION cookie to a file
|
||||
|
||||
:param file: File where a session cookie
|
||||
must be saved, defaults to `~/.aternos`
|
||||
:type file: str, optional
|
||||
:param incl_servers: If the function
|
||||
should include the servers IDs to
|
||||
reduce API requests count (recommended),
|
||||
defaults to True
|
||||
:type incl_servers: bool, optional
|
||||
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)
|
||||
"""
|
||||
|
||||
file = os.path.expanduser(file)
|
||||
logging.debug(f'Saving session to {file}')
|
||||
|
||||
with open(file, 'wt', encoding='utf-8') as f:
|
||||
|
||||
f.write(self.atconn.atsession + '\n')
|
||||
|
@ -179,11 +241,12 @@ class Client:
|
|||
|
||||
"""Parses a list of your servers from Aternos website
|
||||
|
||||
:param cache: If the function should use
|
||||
cached servers list (recommended), defaults to True
|
||||
:type cache: bool, optional
|
||||
:return: List of :class:`python_aternos.atserver.AternosServer` objects
|
||||
:rtype: list
|
||||
Args:
|
||||
cache (bool, optional): If the function should use
|
||||
cached servers list (recommended)
|
||||
|
||||
Returns:
|
||||
List of AternosServer objects
|
||||
"""
|
||||
|
||||
if cache and self.parsed:
|
||||
|
@ -204,10 +267,10 @@ class Client:
|
|||
def refresh_servers(self, ids: List[str]) -> None:
|
||||
|
||||
"""Replaces cached servers list creating
|
||||
:class:`AternosServer` objects by given IDs
|
||||
AternosServer objects by given IDs
|
||||
|
||||
:param ids: Servers unique identifiers
|
||||
:type ids: List[str]
|
||||
Args:
|
||||
ids (List[str]): Servers unique identifiers
|
||||
"""
|
||||
|
||||
self.servers = []
|
||||
|
@ -217,6 +280,7 @@ class Client:
|
|||
if servid == '':
|
||||
continue
|
||||
|
||||
logging.debug(f'Adding server {servid}')
|
||||
srv = AternosServer(servid, self.atconn)
|
||||
self.servers.append(srv)
|
||||
|
||||
|
@ -225,17 +289,18 @@ class Client:
|
|||
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.
|
||||
Use this instead of list_servers
|
||||
if you know the ID to save some time.
|
||||
|
||||
:return: :class:`python_aternos.atserver.AternosServer` object
|
||||
:rtype: python_aternos.atserver.AternosServer
|
||||
Returns:
|
||||
AternosServer object
|
||||
"""
|
||||
|
||||
return AternosServer(servid, self.atconn)
|
||||
|
||||
def logout(self) -> None:
|
||||
|
||||
"""Logouts from Aternos account"""
|
||||
"""Log out from Aternos account"""
|
||||
|
||||
self.atconn.request_cloudflare(
|
||||
'https://aternos.org/panel/ajax/account/logout.php',
|
||||
|
@ -246,8 +311,8 @@ class Client:
|
|||
|
||||
"""Changes a username in your Aternos account
|
||||
|
||||
:param value: New username
|
||||
:type value: str
|
||||
Args:
|
||||
value (str): New username
|
||||
"""
|
||||
|
||||
self.atconn.request_cloudflare(
|
||||
|
@ -259,10 +324,12 @@ class Client:
|
|||
|
||||
"""Changes an e-mail in your Aternos account
|
||||
|
||||
:param value: New e-mail
|
||||
:type value: str
|
||||
:raises ValueError: If an invalid
|
||||
e-mail address was passed to the function
|
||||
Args:
|
||||
value (str): New e-mail
|
||||
|
||||
Raises:
|
||||
ValueError: If an invalid e-mail address
|
||||
was passed to the function
|
||||
"""
|
||||
|
||||
email = re.compile(
|
||||
|
@ -280,14 +347,27 @@ class Client:
|
|||
|
||||
"""Changes a password in your Aternos account
|
||||
|
||||
:param old: Old password
|
||||
:type old: str
|
||||
:param new: New password
|
||||
:type new: str
|
||||
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
|
||||
"""
|
||||
|
||||
old = Client.md5encode(old)
|
||||
new = Client.md5encode(new)
|
||||
self.atconn.request_cloudflare(
|
||||
'https://aternos.org/panel/ajax/account/password.php',
|
||||
'POST', data={
|
||||
|
|
|
@ -121,22 +121,25 @@ convert = {
|
|||
|
||||
class AternosConfig:
|
||||
|
||||
"""Class for editing server settings
|
||||
|
||||
:param atserv: :class:`python_aternos.atserver.AternosServer` object
|
||||
:type atserv: python_aternos.atserver.AternosServer
|
||||
"""
|
||||
"""Class for editing server settings"""
|
||||
|
||||
def __init__(self, atserv: 'AternosServer') -> None:
|
||||
|
||||
"""Class for editing server settings
|
||||
|
||||
Args:
|
||||
atserv (python_aternos.atserver.AternosServer):
|
||||
atserver.AternosServer object
|
||||
"""
|
||||
|
||||
self.atserv = atserv
|
||||
|
||||
def get_timezone(self) -> str:
|
||||
|
||||
"""Parses timezone from options page
|
||||
|
||||
:return: Area/Location
|
||||
:rtype: str
|
||||
Returns:
|
||||
Area/Location
|
||||
"""
|
||||
|
||||
optreq = self.atserv.atserver_request(
|
||||
|
@ -154,10 +157,12 @@ class AternosConfig:
|
|||
|
||||
"""Sets new timezone
|
||||
|
||||
:param value: New timezone
|
||||
:type value: str
|
||||
:raises ValueError: If given string
|
||||
doesn't match Area/Location format
|
||||
Args:
|
||||
value (str): New timezone
|
||||
|
||||
Raises:
|
||||
ValueError: If given string doesn't
|
||||
match `Area/Location` format
|
||||
"""
|
||||
|
||||
matches_tz = tzcheck.search(value)
|
||||
|
@ -176,8 +181,8 @@ class AternosConfig:
|
|||
|
||||
"""Parses Java version from options page
|
||||
|
||||
:return: Java image version
|
||||
:rtype: int
|
||||
Returns:
|
||||
Java image version
|
||||
"""
|
||||
|
||||
optreq = self.atserv.atserver_request(
|
||||
|
@ -198,8 +203,8 @@ class AternosConfig:
|
|||
|
||||
"""Sets new Java version
|
||||
|
||||
:param value: New Java image version
|
||||
:type value: int
|
||||
Args:
|
||||
value (int): New Java image version
|
||||
"""
|
||||
|
||||
self.atserv.atserver_request(
|
||||
|
@ -215,10 +220,9 @@ class AternosConfig:
|
|||
|
||||
"""Sets server.properties option
|
||||
|
||||
:param option: Option name
|
||||
:type option: str
|
||||
:param value: New value
|
||||
:type value: Any
|
||||
Args:
|
||||
option (str): Option name
|
||||
value (Any): New value
|
||||
"""
|
||||
|
||||
self.__set_prop(
|
||||
|
@ -230,12 +234,15 @@ class AternosConfig:
|
|||
|
||||
"""Parses all server.properties from options page
|
||||
|
||||
:param proptyping: If the returned dict should contain value
|
||||
that matches property type (e.g. max-players will be int)
|
||||
instead of string, defaults to True
|
||||
:type proptyping: bool, optional
|
||||
:return: Server.properties dict
|
||||
:rtype: Dict[str,Any]
|
||||
Args:
|
||||
proptyping (bool, optional):
|
||||
If the returned dict should
|
||||
contain value that matches
|
||||
property type (e.g. max-players will be int)
|
||||
instead of string
|
||||
|
||||
Returns:
|
||||
`server.properties` dictionary
|
||||
"""
|
||||
|
||||
return self.__get_all_props('https://aternos.org/options', proptyping)
|
||||
|
@ -244,8 +251,9 @@ class AternosConfig:
|
|||
|
||||
"""Updates server.properties options with the given dict
|
||||
|
||||
:param props: Dict with properties `{key:value}`
|
||||
:type props: Dict[str,Any]
|
||||
Args:
|
||||
props (Dict[str,Any]):
|
||||
Dictionary with `{key:value}` properties
|
||||
"""
|
||||
|
||||
for key in props:
|
||||
|
@ -261,16 +269,12 @@ class AternosConfig:
|
|||
|
||||
"""Sets level.dat option for specified world
|
||||
|
||||
:param option: Option name
|
||||
:type option: Union[WorldOpts,WorldRules]
|
||||
:param value: New value
|
||||
:type value: Any
|
||||
:param gamerule: If the option
|
||||
is a gamerule, defaults to False
|
||||
:type gamerule: bool, optional
|
||||
:param world: Name of the world which
|
||||
level.dat must be edited, defaults to 'world'
|
||||
:type world: str, optional
|
||||
Args:
|
||||
option (Union[WorldOpts, WorldRules]): Option name
|
||||
value (Any): New value
|
||||
gamerule (bool, optional): If the option is a gamerule
|
||||
world (str, optional): Name of the world which
|
||||
`level.dat` must be edited
|
||||
"""
|
||||
|
||||
prefix = DAT_PREFIX
|
||||
|
@ -289,14 +293,16 @@ class AternosConfig:
|
|||
|
||||
"""Parses level.dat from specified world's options page
|
||||
|
||||
:param world: Name of the world, defaults to 'world'
|
||||
:type world: str, optional
|
||||
:param proptyping: If the returned dict should contain the value
|
||||
that matches property type (e.g. randomTickSpeed will be bool)
|
||||
instead of string, defaults to True
|
||||
:type proptyping: bool, optional
|
||||
:return: Level.dat dict
|
||||
:rtype: Dict[str,Any]
|
||||
Args:
|
||||
world (str, optional): Name of the worl
|
||||
proptyping (bool, optional):
|
||||
If the returned dict should
|
||||
contain the value that matches
|
||||
property type (e.g. randomTickSpeed will be bool)
|
||||
instead of string
|
||||
|
||||
Returns:
|
||||
`level.dat` options dictionary
|
||||
"""
|
||||
|
||||
return self.__get_all_props(
|
||||
|
@ -312,11 +318,11 @@ class AternosConfig:
|
|||
"""Sets level.dat options from
|
||||
the dictionary for the specified world
|
||||
|
||||
:param props: Level.dat options
|
||||
:type props: Dict[Union[WorldOpts, WorldRules], Any]
|
||||
:param world: name of the world which
|
||||
level.dat must be edited, defaults to 'world'
|
||||
:type world: str
|
||||
Args:
|
||||
props (Dict[Union[WorldOpts, WorldRules], Any]):
|
||||
`level.dat` options
|
||||
world (str): name of the world which
|
||||
`level.dat` must be edited
|
||||
"""
|
||||
|
||||
for key in props:
|
||||
|
|
|
@ -7,7 +7,9 @@ import logging
|
|||
from functools import partial
|
||||
|
||||
from typing import Optional, Union
|
||||
from requests import Response
|
||||
from typing import Dict, Any
|
||||
|
||||
import requests
|
||||
|
||||
from cloudscraper import CloudScraper
|
||||
|
||||
|
@ -23,14 +25,12 @@ REQUA = \
|
|||
|
||||
class AternosConnect:
|
||||
|
||||
"""
|
||||
Class for sending API requests bypass Cloudflare
|
||||
and parsing responses"""
|
||||
"""Class for sending API requests
|
||||
bypass Cloudflare and parsing responses"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
||||
self.session = CloudScraper()
|
||||
self.atsession = ''
|
||||
self.sec = ''
|
||||
self.token = ''
|
||||
|
||||
|
@ -39,12 +39,14 @@ class AternosConnect:
|
|||
"""Parses Aternos ajax token that
|
||||
is needed for most requests
|
||||
|
||||
:raises RuntimeWarning: If the parser
|
||||
can not find <head> tag in HTML response
|
||||
:raises CredentialsError: If the parser
|
||||
is unable to extract ajax token in HTML
|
||||
:return: Aternos ajax token
|
||||
:rtype: str
|
||||
Raises:
|
||||
RuntimeWarning: If the parser can not
|
||||
find `<head>` tag in HTML response
|
||||
TokenError: If the parser is unable
|
||||
to extract ajax token from HTML
|
||||
|
||||
Returns:
|
||||
Aternos ajax token
|
||||
"""
|
||||
|
||||
loginpage = self.request_cloudflare(
|
||||
|
@ -71,7 +73,10 @@ class AternosConnect:
|
|||
try:
|
||||
text = pagehead.decode('utf-8', 'replace')
|
||||
js_code = re.findall(r'\(\(\)(.*?)\)\(\);', text)
|
||||
token_func = js_code[1] if len(js_code) > 1 else js_code[0]
|
||||
|
||||
token_func = js_code[0]
|
||||
if len(js_code) > 1:
|
||||
token_func = js_code[1]
|
||||
|
||||
ctx = atjsparse.exec_js(token_func)
|
||||
self.token = ctx.window['AJAX_TOKEN']
|
||||
|
@ -88,8 +93,8 @@ class AternosConnect:
|
|||
"""Generates Aternos SEC token which
|
||||
is also needed for most API requests
|
||||
|
||||
:return: Random SEC key:value string
|
||||
:rtype: str
|
||||
Returns:
|
||||
Random SEC `key:value` string
|
||||
"""
|
||||
|
||||
randkey = self.generate_aternos_rand()
|
||||
|
@ -107,10 +112,11 @@ class AternosConnect:
|
|||
"""Generates a random string using
|
||||
Aternos algorithm from main.js file
|
||||
|
||||
:param randlen: Random string length, defaults to 16
|
||||
:type randlen: int, optional
|
||||
:return: Random string for SEC token
|
||||
:rtype: str
|
||||
Args:
|
||||
randlen (int, optional): Random string length
|
||||
|
||||
Returns:
|
||||
Random string for SEC token
|
||||
"""
|
||||
|
||||
# a list with randlen+1 empty strings:
|
||||
|
@ -129,16 +135,15 @@ class AternosConnect:
|
|||
|
||||
"""Converts an integer to specified base
|
||||
|
||||
:param num: Integer in any base to convert.
|
||||
If it is a float started with `0,`,
|
||||
zero and comma will be removed to get int
|
||||
:type num: Union[int,float,str]
|
||||
:param base: New base
|
||||
:type base: int
|
||||
:param frombase: Given number base, defaults to 10
|
||||
:type frombase: int, optional
|
||||
:return: Number converted to a specified base
|
||||
:rtype: str
|
||||
Args:
|
||||
num (Union[int,float,str]): Integer in any base to convert.
|
||||
If it is a float starting with `0.`,
|
||||
zero and point will be removed to get int
|
||||
base (int): New base
|
||||
frombase (int, optional): Given number base
|
||||
|
||||
Returns:
|
||||
Number converted to a specified base
|
||||
"""
|
||||
|
||||
if isinstance(num, str):
|
||||
|
@ -159,40 +164,35 @@ class AternosConnect:
|
|||
|
||||
def request_cloudflare(
|
||||
self, url: str, method: str,
|
||||
params: Optional[dict] = None,
|
||||
data: Optional[dict] = None,
|
||||
headers: Optional[dict] = None,
|
||||
reqcookies: Optional[dict] = None,
|
||||
params: Optional[Dict[Any, Any]] = None,
|
||||
data: Optional[Dict[Any, Any]] = None,
|
||||
headers: Optional[Dict[Any, Any]] = None,
|
||||
reqcookies: Optional[Dict[Any, Any]] = None,
|
||||
sendtoken: bool = False,
|
||||
retry: int = 5) -> Response:
|
||||
retry: int = 5) -> requests.Response:
|
||||
|
||||
"""Sends a request to Aternos API bypass Cloudflare
|
||||
|
||||
:param url: Request URL
|
||||
:type url: str
|
||||
:param method: Request method, must be GET or POST
|
||||
:type method: str
|
||||
:param params: URL parameters, defaults to None
|
||||
:type params: Optional[dict], optional
|
||||
:param data: POST request data, if the method is GET,
|
||||
this dict will be combined with params, defaults to None
|
||||
:type data: Optional[dict], optional
|
||||
:param headers: Custom headers, defaults to None
|
||||
:type headers: Optional[dict], optional
|
||||
:param reqcookies: Cookies only for this request, defaults to None
|
||||
:type reqcookies: Optional[dict], optional
|
||||
:param sendtoken: If the ajax and SEC token
|
||||
should be sent, defaults to False
|
||||
:type sendtoken: bool, optional
|
||||
:param retry: How many times parser must retry
|
||||
connection to API bypass Cloudflare, defaults to 5
|
||||
:type retry: int, optional
|
||||
:raises CloudflareError:
|
||||
When the parser has exceeded retries count
|
||||
:raises NotImplementedError:
|
||||
When the specified method is not GET or POST
|
||||
:return: API response
|
||||
:rtype: requests.Response
|
||||
Args:
|
||||
url (str): Request URL
|
||||
method (str): Request method, must be GET or POST
|
||||
params (Optional[Dict[Any, Any]], optional): URL parameters
|
||||
data (Optional[Dict[Any, Any]], optional): POST request data,
|
||||
if the method is GET, this dict will be combined with params
|
||||
headers (Optional[Dict[Any, Any]], optional): Custom headers
|
||||
reqcookies (Optional[Dict[Any, Any]], optional):
|
||||
Cookies only for this request
|
||||
sendtoken (bool, optional): If the ajax and SEC token
|
||||
should be sent
|
||||
retry (int, optional): How many times parser must retry
|
||||
connection to API bypass Cloudflare
|
||||
|
||||
Raises:
|
||||
CloudflareError: When the parser has exceeded retries count
|
||||
NotImplementedError: When the specified method is not GET or POST
|
||||
|
||||
Returns:
|
||||
API response
|
||||
"""
|
||||
|
||||
if retry <= 0:
|
||||
|
@ -202,12 +202,6 @@ class AternosConnect:
|
|||
self.session = CloudScraper()
|
||||
self.session.cookies.update(old_cookies)
|
||||
|
||||
try:
|
||||
self.atsession = self.session.cookies['ATERNOS_SESSION']
|
||||
except KeyError:
|
||||
# don't rewrite atsession value
|
||||
pass
|
||||
|
||||
params = params or {}
|
||||
data = data or {}
|
||||
headers = headers or {}
|
||||
|
@ -276,3 +270,17 @@ class AternosConnect:
|
|||
|
||||
req.raise_for_status()
|
||||
return req
|
||||
|
||||
@property
|
||||
def atsession(self) -> str:
|
||||
|
||||
"""Aternos session cookie,
|
||||
empty string if not logged in
|
||||
|
||||
Returns:
|
||||
Session cookie
|
||||
"""
|
||||
|
||||
return self.session.cookies.get(
|
||||
'ATERNOS_SESSION', ''
|
||||
)
|
||||
|
|
|
@ -28,27 +28,24 @@ class TokenError(AternosError):
|
|||
|
||||
class ServerError(AternosError):
|
||||
|
||||
"""Common class for server errors
|
||||
|
||||
:param reason: Code which contains error reason
|
||||
:type reason: str
|
||||
:param message: Error message, defaults to ''
|
||||
:type message: str, optional
|
||||
"""
|
||||
"""Common class for server errors"""
|
||||
|
||||
def __init__(self, reason: str, message: str = '') -> None:
|
||||
|
||||
"""Common class for server errors
|
||||
|
||||
Args:
|
||||
reason (str): Code which contains error reason
|
||||
message (str, optional): Error message
|
||||
"""
|
||||
|
||||
self.reason = reason
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class ServerStartError(AternosError):
|
||||
|
||||
"""Raised when Aternos can not start Minecraft server
|
||||
|
||||
:param reason: Code which contains error reason
|
||||
:type reason: str
|
||||
"""
|
||||
"""Raised when Aternos can not start Minecraft server"""
|
||||
|
||||
MESSAGE: Final = 'Unable to start server, code: {}'
|
||||
reason_msg = {
|
||||
|
@ -57,22 +54,31 @@ class ServerStartError(AternosError):
|
|||
'EULA was not accepted. '
|
||||
'Use start(accepteula=True)',
|
||||
|
||||
'already': 'Server is already running',
|
||||
'already': 'Server has already started',
|
||||
'wrongversion': 'Incorrect software version installed',
|
||||
|
||||
'file':
|
||||
'File server is unavailbale, '
|
||||
'view https://status.aternos.gmbh',
|
||||
|
||||
'size': 'Available storage size limit (4 GB) was reached'
|
||||
'size': 'Available storage size limit (4 GB) has been reached'
|
||||
}
|
||||
|
||||
def __init__(self, reason: str) -> None:
|
||||
|
||||
"""Raised when Aternos
|
||||
can not start Minecraft server
|
||||
|
||||
Args:
|
||||
reason (str):
|
||||
Code which contains error reason
|
||||
"""
|
||||
|
||||
super().__init__(
|
||||
reason,
|
||||
self.reason_msg.get(
|
||||
reason, self.MESSAGE.format(reason)
|
||||
reason,
|
||||
self.MESSAGE.format(reason)
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -19,109 +19,214 @@ class FileType(enum.IntEnum):
|
|||
|
||||
file = 0
|
||||
directory = 1
|
||||
dir = 1
|
||||
|
||||
|
||||
class AternosFile:
|
||||
|
||||
"""File class which contains info about its path, type and size
|
||||
|
||||
:param atserv: :class:`python_aternos.atserver.AternosServer` instance
|
||||
:type atserv: python_aternos.atserver.AternosServer
|
||||
:param path: Path to the file
|
||||
:type path: str
|
||||
:param name: Filename
|
||||
:type name: str
|
||||
:param ftype: File or directory
|
||||
:type ftype: python_aternos.atfile.FileType
|
||||
:param size: File size, defaults to 0
|
||||
:type size: Union[int,float], optional
|
||||
"""
|
||||
"""File class which contains info
|
||||
about its path, type and size"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
atserv: 'AternosServer',
|
||||
path: str, name: str,
|
||||
path: str, rmable: bool,
|
||||
dlable: bool, editable: bool,
|
||||
ftype: FileType = FileType.file,
|
||||
size: Union[int, float] = 0) -> None:
|
||||
|
||||
"""File class which contains info
|
||||
about its path, type and size
|
||||
|
||||
Args:
|
||||
atserv (python_aternos.atserver.AternosServer):
|
||||
atserver.AternosServer instance
|
||||
path (str): Absolute path to the file
|
||||
rmable (bool): Is the file deleteable (removeable)
|
||||
dlable (bool): Is the file downloadable
|
||||
ftype (python_aternos.atfile.FileType): File or directory
|
||||
size (Union[int,float], optional): File size
|
||||
"""
|
||||
|
||||
path = path.lstrip('/')
|
||||
path = '/' + path
|
||||
|
||||
self.atserv = atserv
|
||||
self._path = path.lstrip('/')
|
||||
self._name = name
|
||||
self._full = path + name
|
||||
|
||||
self._path = path
|
||||
self._name = path[path.rfind('/') + 1:]
|
||||
self._dirname = path[:path.rfind('/')]
|
||||
|
||||
self._deleteable = rmable
|
||||
self._downloadable = dlable
|
||||
self._editable = editable
|
||||
|
||||
self._ftype = ftype
|
||||
self._size = float(size)
|
||||
|
||||
def create(
|
||||
self,
|
||||
name: str,
|
||||
ftype: FileType = FileType.file) -> None:
|
||||
|
||||
"""Creates a file or a directory inside this one
|
||||
|
||||
Args:
|
||||
name (str): Filename
|
||||
ftype (FileType, optional): File type
|
||||
|
||||
Raises:
|
||||
RuntimeWarning: Messages about probabilty of FileError
|
||||
(if `self` file object is not a directory)
|
||||
FileError: If Aternos denied file creation
|
||||
"""
|
||||
|
||||
if self.is_file:
|
||||
raise RuntimeWarning(
|
||||
'Creating files only available '
|
||||
'inside directories'
|
||||
)
|
||||
|
||||
name = name.strip().replace('/', '_')
|
||||
req = self.atserv.atserver_request(
|
||||
'https://aternos.org/panel/ajax/files/create.php',
|
||||
'POST', data={
|
||||
'file': f'{self._path}/{name}',
|
||||
'type': 'file'
|
||||
if ftype == FileType.file
|
||||
else 'directory'
|
||||
}
|
||||
)
|
||||
|
||||
if req.content == b'{"success":false}':
|
||||
raise FileError('Unable to create a file')
|
||||
|
||||
def delete(self) -> None:
|
||||
|
||||
"""Deletes the file"""
|
||||
"""Deletes the file
|
||||
|
||||
self.atserv.atserver_request(
|
||||
Raises:
|
||||
RuntimeWarning: Message about probability of FileError
|
||||
FileError: If deleting this file is disallowed by Aternos
|
||||
"""
|
||||
|
||||
if not self._deleteable:
|
||||
raise RuntimeWarning(
|
||||
'The file seems to be protected (undeleteable). '
|
||||
'Always check it before calling delete()'
|
||||
)
|
||||
|
||||
req = self.atserv.atserver_request(
|
||||
'https://aternos.org/panel/ajax/delete.php',
|
||||
'POST', data={'file': self._full},
|
||||
'POST', data={'file': self._path},
|
||||
sendtoken=True
|
||||
)
|
||||
|
||||
if req.content == b'{"success":false}':
|
||||
raise FileError('Unable to delete the file')
|
||||
|
||||
def get_content(self) -> bytes:
|
||||
|
||||
"""Requests file content in bytes (downloads it)
|
||||
|
||||
:raises FileError: If downloading
|
||||
the file is not allowed by Aternos
|
||||
:return: File content
|
||||
:rtype: bytes
|
||||
Raises:
|
||||
RuntimeWarning: Message about probability of FileError
|
||||
FileError: If downloading this file is disallowed by Aternos
|
||||
|
||||
Returns:
|
||||
File content
|
||||
"""
|
||||
|
||||
if not self._downloadable:
|
||||
raise RuntimeWarning(
|
||||
'The file seems to be undownloadable. '
|
||||
'Always check it before calling get_content()'
|
||||
)
|
||||
|
||||
file = self.atserv.atserver_request(
|
||||
'https://aternos.org/panel/ajax/files/download.php',
|
||||
'GET', params={
|
||||
'file': self._full
|
||||
'file': self._path
|
||||
}
|
||||
)
|
||||
|
||||
if file.content == b'{"success":false}':
|
||||
raise FileError('Unable to download the file. Try to get text')
|
||||
raise FileError(
|
||||
'Unable to download the file. '
|
||||
'Try to get text'
|
||||
)
|
||||
|
||||
return file.content
|
||||
|
||||
def set_content(self, value: bytes) -> None:
|
||||
|
||||
"""Modifies the file content
|
||||
"""Modifies file content
|
||||
|
||||
:param value: New content
|
||||
:type value: bytes
|
||||
Args:
|
||||
value (bytes): New content
|
||||
|
||||
Raises:
|
||||
FileError: If Aternos denied file saving
|
||||
"""
|
||||
|
||||
self.atserv.atserver_request(
|
||||
req = self.atserv.atserver_request(
|
||||
'https://aternos.org/panel/ajax/save.php',
|
||||
'POST', data={
|
||||
'file': self._full,
|
||||
'file': self._path,
|
||||
'content': value
|
||||
}, sendtoken=True
|
||||
)
|
||||
|
||||
if req.content == b'{"success":false}':
|
||||
raise FileError('Unable to save the file')
|
||||
|
||||
def get_text(self) -> str:
|
||||
|
||||
"""Requests editing the file as a text
|
||||
(try it if downloading is disallowed)
|
||||
|
||||
:return: File text content
|
||||
:rtype: str
|
||||
Raises:
|
||||
RuntimeWarning: Message about probability of FileError
|
||||
FileError: If unable to parse text from response
|
||||
|
||||
Returns:
|
||||
File text content
|
||||
"""
|
||||
|
||||
if not self._editable:
|
||||
raise RuntimeWarning(
|
||||
'The file seems to be uneditable. '
|
||||
'Always check it before calling get_text()'
|
||||
)
|
||||
|
||||
if self.is_dir:
|
||||
raise RuntimeWarning(
|
||||
'Use get_content() to download '
|
||||
'a directory as a ZIP file!'
|
||||
)
|
||||
|
||||
filepath = self._path.lstrip("/")
|
||||
editor = self.atserv.atserver_request(
|
||||
f'https://aternos.org/files/{self._full.lstrip("/")}', 'GET'
|
||||
f'https://aternos.org/files/{filepath}', 'GET'
|
||||
)
|
||||
edittree = lxml.html.fromstring(editor.content)
|
||||
editblock = edittree.xpath('//div[@id="editor"]')
|
||||
|
||||
editblock = edittree.xpath('//div[@id="editor"]')[0]
|
||||
return editblock.text_content()
|
||||
if len(editblock) < 1:
|
||||
raise FileError(
|
||||
'Unable to open editor. '
|
||||
'Try to get file content'
|
||||
)
|
||||
|
||||
return editblock[0].text_content()
|
||||
|
||||
def set_text(self, value: str) -> None:
|
||||
|
||||
"""Modifies the file content,
|
||||
but unlike set_content takes
|
||||
a string as a new value
|
||||
but unlike `set_content` takes
|
||||
a string as an argument
|
||||
|
||||
:param value: New content
|
||||
:type value: str
|
||||
Args:
|
||||
value (str): New content
|
||||
"""
|
||||
|
||||
self.set_content(value.encode('utf-8'))
|
||||
|
@ -129,11 +234,12 @@ class AternosFile:
|
|||
@property
|
||||
def path(self) -> str:
|
||||
|
||||
"""Path to a directory which
|
||||
contains the file, without leading slash
|
||||
"""Abslute path to the file
|
||||
without leading slash
|
||||
including filename
|
||||
|
||||
:return: Full path to directory
|
||||
:rtype: str
|
||||
Returns:
|
||||
Full path to the file
|
||||
"""
|
||||
|
||||
return self._path
|
||||
|
@ -141,60 +247,105 @@ class AternosFile:
|
|||
@property
|
||||
def name(self) -> str:
|
||||
|
||||
"""Filename including extension
|
||||
"""Filename with extension
|
||||
|
||||
:return: Filename
|
||||
:rtype: str
|
||||
Returns:
|
||||
Filename
|
||||
"""
|
||||
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def full(self) -> str:
|
||||
def dirname(self) -> str:
|
||||
|
||||
"""Absolute path to the file,
|
||||
without leading slash
|
||||
"""Full path to the directory
|
||||
which contains the file
|
||||
without leading slash.
|
||||
Empty path means root (`/`)
|
||||
|
||||
:return: Full path
|
||||
:rtype: str
|
||||
Returns:
|
||||
Path to the directory
|
||||
"""
|
||||
|
||||
return self._full
|
||||
return self._dirname
|
||||
|
||||
@property
|
||||
def deleteable(self) -> bool:
|
||||
|
||||
"""True if the file can be deleted,
|
||||
otherwise False
|
||||
|
||||
Returns:
|
||||
Can the file be deleted
|
||||
"""
|
||||
|
||||
return self._deleteable
|
||||
|
||||
@property
|
||||
def downloadable(self) -> bool:
|
||||
|
||||
"""True if the file can be downloaded,
|
||||
otherwise False
|
||||
|
||||
Returns:
|
||||
Can the file be downloaded
|
||||
"""
|
||||
|
||||
return self._downloadable
|
||||
|
||||
@property
|
||||
def editable(self) -> bool:
|
||||
|
||||
"""True if the file can be
|
||||
opened in Aternos editor,
|
||||
otherwise False
|
||||
|
||||
Returns:
|
||||
Can the file be edited
|
||||
"""
|
||||
|
||||
return self._editable
|
||||
|
||||
@property
|
||||
def ftype(self) -> FileType:
|
||||
|
||||
"""File object type: file or directory
|
||||
|
||||
Returns:
|
||||
File type
|
||||
"""
|
||||
|
||||
return self._ftype
|
||||
|
||||
@property
|
||||
def is_dir(self) -> bool:
|
||||
|
||||
"""Check if the file object is a directory
|
||||
|
||||
:return: `True` if the file
|
||||
is a directory, otherwise `False`
|
||||
:rtype: bool
|
||||
Returns:
|
||||
True if it is a directory, otherwise False
|
||||
"""
|
||||
|
||||
if self._ftype == FileType.directory:
|
||||
return True
|
||||
return False
|
||||
return self._ftype == FileType.dir
|
||||
|
||||
@property
|
||||
def is_file(self) -> bool:
|
||||
|
||||
"""Check if the file object is not a directory
|
||||
|
||||
:return: `True` if it is a file, otherwise `False`
|
||||
:rtype: bool
|
||||
Returns:
|
||||
True if it is a file, otherwise False
|
||||
"""
|
||||
|
||||
if self._ftype == FileType.file:
|
||||
return True
|
||||
return False
|
||||
return self._ftype == FileType.file
|
||||
|
||||
@property
|
||||
def size(self) -> float:
|
||||
|
||||
"""File size in bytes
|
||||
|
||||
:return: File size
|
||||
:rtype: float
|
||||
Returns:
|
||||
File size
|
||||
"""
|
||||
|
||||
return self._size
|
||||
|
|
|
@ -12,26 +12,32 @@ if TYPE_CHECKING:
|
|||
|
||||
class FileManager:
|
||||
|
||||
"""Aternos file manager class for viewing files structure
|
||||
|
||||
:param atserv: :class:`python_aternos.atserver.AternosServer` instance
|
||||
:type atserv: python_aternos.atserver.AternosServer
|
||||
"""
|
||||
"""Aternos file manager class
|
||||
for viewing files structure"""
|
||||
|
||||
def __init__(self, atserv: 'AternosServer') -> None:
|
||||
|
||||
"""Aternos file manager class
|
||||
for viewing files structure
|
||||
|
||||
Args:
|
||||
atserv (python_aternos.atserver.AternosServer):
|
||||
atserver.AternosServer instance
|
||||
"""
|
||||
|
||||
self.atserv = atserv
|
||||
|
||||
def listdir(self, path: str = '') -> List[AternosFile]:
|
||||
def list_dir(self, path: str = '') -> List[AternosFile]:
|
||||
|
||||
"""Requests a list of files
|
||||
in the specified directory
|
||||
|
||||
:param path: Directory
|
||||
(an empty string means root), defaults to ''
|
||||
:type path: str, optional
|
||||
:return: List of :class:`python_aternos.atfile.AternosFile`
|
||||
:rtype: List[AternosFile]
|
||||
Args:
|
||||
path (str, optional):
|
||||
Directory (an empty string means root)
|
||||
|
||||
Returns:
|
||||
List of atfile.AternosFile objects
|
||||
"""
|
||||
|
||||
path = path.lstrip('/')
|
||||
|
@ -42,29 +48,35 @@ class FileManager:
|
|||
filestree = lxml.html.fromstring(filesreq.content)
|
||||
|
||||
fileslist = filestree.xpath(
|
||||
'//div[contains(concat(" ",normalize-space(@class)," ")," file ")]'
|
||||
'//div[@class="file" or @class="file clickable"]'
|
||||
)
|
||||
|
||||
files = []
|
||||
for f in fileslist:
|
||||
|
||||
ftype_raw = f.xpath('@data-type')[0]
|
||||
ftype = FileType.file \
|
||||
if ftype_raw == 'file' \
|
||||
else FileType.directory
|
||||
|
||||
fsize = self.extract_size(
|
||||
f.xpath('./div[@class="filesize"]')
|
||||
)
|
||||
|
||||
fullpath = f.xpath('@data-path')[0]
|
||||
filepath = fullpath[:fullpath.rfind('/')]
|
||||
filename = fullpath[fullpath.rfind('/'):]
|
||||
rm_btn = f.xpath('./div[contains(@class,"js-delete-file")]')
|
||||
dl_btn = f.xpath('./div[contains(@class,"js-download-file")]')
|
||||
clickable = 'clickable' in f.classes
|
||||
is_config = ('server.properties' in path) or ('level.dat' in path)
|
||||
|
||||
files.append(
|
||||
AternosFile(
|
||||
self.atserv,
|
||||
filepath, filename,
|
||||
ftype, fsize
|
||||
atserv=self.atserv,
|
||||
path=f.xpath('@data-path')[0],
|
||||
|
||||
rmable=(len(rm_btn) > 0),
|
||||
dlable=(len(dl_btn) > 0),
|
||||
editable=(clickable and not is_config),
|
||||
|
||||
ftype={'file': FileType.file}.get(
|
||||
ftype_raw, FileType.dir
|
||||
),
|
||||
size=fsize
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -74,10 +86,11 @@ class FileManager:
|
|||
|
||||
"""Parses file size from the LXML tree
|
||||
|
||||
:param fsize_raw: XPath method result
|
||||
:type fsize_raw: List[Any]
|
||||
:return: File size in bytes
|
||||
:rtype: float
|
||||
Args:
|
||||
fsize_raw (List[Any]): XPath parsing result
|
||||
|
||||
Returns:
|
||||
File size in bytes
|
||||
"""
|
||||
|
||||
if len(fsize_raw) > 0:
|
||||
|
@ -87,7 +100,10 @@ class FileManager:
|
|||
fsize_msr = fsize_text[fsize_text.rfind(' ') + 1:]
|
||||
|
||||
try:
|
||||
return self.convert_size(float(fsize_num), fsize_msr)
|
||||
return self.convert_size(
|
||||
float(fsize_num),
|
||||
fsize_msr
|
||||
)
|
||||
except ValueError:
|
||||
return -1.0
|
||||
|
||||
|
@ -100,12 +116,12 @@ class FileManager:
|
|||
|
||||
"""Converts "human" file size to size in bytes
|
||||
|
||||
:param num: Size
|
||||
:type num: Union[int,float]
|
||||
:param measure: Units (B, kB, MB, GB)
|
||||
:type measure: str
|
||||
:return: Size in bytes
|
||||
:rtype: float
|
||||
Args:
|
||||
num (Union[int,float]): Size
|
||||
measure (str): Units (B, kB, MB, GB)
|
||||
|
||||
Returns:
|
||||
Size in bytes
|
||||
"""
|
||||
|
||||
measure_match = {
|
||||
|
@ -121,30 +137,35 @@ class FileManager:
|
|||
"""Returns :class:`python_aternos.atfile.AternosFile`
|
||||
instance by its path
|
||||
|
||||
:param path: Path to file including its filename
|
||||
:type path: str
|
||||
:return: _description_
|
||||
:rtype: Optional[AternosFile]
|
||||
Args:
|
||||
path (str): Path to the file including its filename
|
||||
|
||||
Returns:
|
||||
atfile.AternosFile object
|
||||
if file has been found,
|
||||
otherwise None
|
||||
"""
|
||||
|
||||
filepath = path[:path.rfind('/')]
|
||||
filedir = path[:path.rfind('/')]
|
||||
filename = path[path.rfind('/'):]
|
||||
|
||||
filedir = self.listdir(filepath)
|
||||
for file in filedir:
|
||||
if file.name == filename:
|
||||
return file
|
||||
files = self.list_dir(filedir)
|
||||
|
||||
return None
|
||||
return {
|
||||
'file': f
|
||||
for f in files
|
||||
if f.name == filename
|
||||
}.get('file', None)
|
||||
|
||||
def dl_file(self, path: str) -> bytes:
|
||||
|
||||
"""Returns the file content in bytes (downloads it)
|
||||
|
||||
:param path: Path to file including its filename
|
||||
:type path: str
|
||||
:return: File content
|
||||
:rtype: bytes
|
||||
Args:
|
||||
path (str): Path to file including its filename
|
||||
|
||||
Returns:
|
||||
File content
|
||||
"""
|
||||
|
||||
file = self.atserv.atserver_request( # type: ignore
|
||||
|
@ -161,10 +182,11 @@ class FileManager:
|
|||
"""Returns the world zip file content
|
||||
by its name (downloads it)
|
||||
|
||||
:param world: Name of world, defaults to 'world'
|
||||
:type world: str, optional
|
||||
:return: Zip file content
|
||||
:rtype: bytes
|
||||
Args:
|
||||
world (str, optional): Name of world
|
||||
|
||||
Returns:
|
||||
ZIP file content
|
||||
"""
|
||||
|
||||
resp = self.atserv.atserver_request( # type: ignore
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
"""Parsing and executing JavaScript code"""
|
||||
|
||||
import base64
|
||||
|
||||
from typing import Any
|
||||
|
||||
import regex
|
||||
import js2py
|
||||
|
||||
|
@ -13,12 +10,14 @@ arrowexp = regex.compile(r'\w[^\}]*+')
|
|||
|
||||
def to_ecma5_function(f: str) -> str:
|
||||
|
||||
"""Converts a ECMA6 function to ECMA5 format (without arrow expressions)
|
||||
"""Converts a ECMA6 function
|
||||
to ECMA5 format (without arrow expressions)
|
||||
|
||||
:param f: ECMA6 function
|
||||
:type f: str
|
||||
:return: ECMA5 function
|
||||
:rtype: str
|
||||
Args:
|
||||
f (str): ECMA6 function
|
||||
|
||||
Returns:
|
||||
ECMA5 function
|
||||
"""
|
||||
|
||||
f = regex.sub(r'/\*.+?\*/', '', f)
|
||||
|
@ -35,23 +34,25 @@ def atob(s: str) -> str:
|
|||
|
||||
"""Decodes base64 string
|
||||
|
||||
:param s: Encoded data
|
||||
:type s: str
|
||||
:return: Decoded string
|
||||
:rtype: str
|
||||
Args:
|
||||
s (str): Encoded data
|
||||
|
||||
Returns:
|
||||
Decoded string
|
||||
"""
|
||||
|
||||
return base64.standard_b64decode(str(s)).decode('utf-8')
|
||||
|
||||
|
||||
def exec_js(f: str) -> Any:
|
||||
def exec_js(f: str) -> js2py.EvalJs:
|
||||
|
||||
"""Executes a JavaScript function
|
||||
|
||||
:param f: ECMA6 function
|
||||
:type f: str
|
||||
:return: JavaScript interpreter context
|
||||
:rtype: Any
|
||||
Args:
|
||||
f (str): ECMA6 function
|
||||
|
||||
Returns:
|
||||
JavaScript interpreter context
|
||||
"""
|
||||
|
||||
ctx = js2py.EvalJs({'atob': atob})
|
||||
|
|
|
@ -25,26 +25,33 @@ class Lists(enum.Enum):
|
|||
|
||||
class PlayersList:
|
||||
|
||||
"""Class for managing operators, whitelist and banned players lists
|
||||
|
||||
:param lst: Players list type, must be
|
||||
:class:`python_aternos.atplayers.Lists` enum value
|
||||
:type lst: Union[str,Lists]
|
||||
:param atserv: :class:`python_aternos.atserver.AternosServer` instance
|
||||
:type atserv: python_aternos.atserver.AternosServer
|
||||
"""
|
||||
"""Class for managing operators,
|
||||
whitelist and banned players lists"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
lst: Union[str, Lists],
|
||||
atserv: 'AternosServer') -> None:
|
||||
|
||||
"""Class for managing operators,
|
||||
whitelist and banned players lists
|
||||
|
||||
Args:
|
||||
lst (Union[str,Lists]): Players list type, must be
|
||||
atplayers.Lists enum value
|
||||
atserv (python_aternos.atserver.AternosServer):
|
||||
atserver.AternosServer instance
|
||||
"""
|
||||
|
||||
self.atserv = atserv
|
||||
self.lst = Lists(lst)
|
||||
|
||||
# Fix for #30 issue
|
||||
# whl_je = whitelist for java
|
||||
# whl_be = whitelist for bedrock
|
||||
# whl = common whitelist
|
||||
common_whl = (self.lst == Lists.whl)
|
||||
# 1 is atserver.Edition.bedrock
|
||||
bedrock = (atserv.edition == 1)
|
||||
bedrock = (atserv.is_bedrock)
|
||||
|
||||
if common_whl and bedrock:
|
||||
self.lst = Lists.whl_be
|
||||
|
@ -56,11 +63,12 @@ class PlayersList:
|
|||
|
||||
"""Parse a players list
|
||||
|
||||
:param cache: If the function can return
|
||||
cached list (highly recommended), defaults to True
|
||||
:type cache: bool, optional
|
||||
:return: List of players nicknames
|
||||
:rtype: List[str]
|
||||
Args:
|
||||
cache (bool, optional): If the function should
|
||||
return cached list (highly recommended)
|
||||
|
||||
Returns:
|
||||
List of players' nicknames
|
||||
"""
|
||||
|
||||
if cache and self.parsed:
|
||||
|
@ -88,8 +96,8 @@ class PlayersList:
|
|||
|
||||
"""Appends a player to the list by the nickname
|
||||
|
||||
:param name: Player's nickname
|
||||
:type name: str
|
||||
Args:
|
||||
name (str): Player's nickname
|
||||
"""
|
||||
|
||||
self.atserv.atserver_request(
|
||||
|
@ -106,8 +114,8 @@ class PlayersList:
|
|||
|
||||
"""Removes a player from the list by the nickname
|
||||
|
||||
:param name: Player's nickname
|
||||
:type name: str
|
||||
Args:
|
||||
name (str): Player's nickname
|
||||
"""
|
||||
|
||||
self.atserv.atserver_request(
|
||||
|
|
|
@ -3,8 +3,10 @@
|
|||
import enum
|
||||
import json
|
||||
|
||||
from typing import Optional, List
|
||||
from requests import Response
|
||||
from typing import Optional
|
||||
from typing import List, Dict, Any
|
||||
|
||||
import requests
|
||||
|
||||
from .atconnect import AternosConnect
|
||||
from .aterrors import ServerStartError
|
||||
|
@ -17,7 +19,7 @@ from .atwss import AternosWss
|
|||
|
||||
class Edition(enum.IntEnum):
|
||||
|
||||
"""Server edition type enum"""
|
||||
"""Server edition type enum (java, bedrock)"""
|
||||
|
||||
java = 0
|
||||
bedrock = 1
|
||||
|
@ -32,32 +34,36 @@ class Status(enum.IntEnum):
|
|||
|
||||
off = 0
|
||||
on = 1
|
||||
|
||||
starting = 2
|
||||
shutdown = 3
|
||||
unknown = 6
|
||||
|
||||
loading = 6
|
||||
error = 7
|
||||
|
||||
preparing = 10
|
||||
confirm = 10
|
||||
|
||||
|
||||
class AternosServer:
|
||||
|
||||
"""Class for controlling your Aternos Minecraft server
|
||||
|
||||
:param servid: Unique server IDentifier
|
||||
:type servid: str
|
||||
:param atconn: :class:`python_aternos.atconnect.AternosConnect`
|
||||
instance with initialized Aternos session
|
||||
:type atconn: python_aternos.atconnect.AternosConnect
|
||||
:param reqinfo: Automatically call AternosServer.fetch()
|
||||
to get all info, defaults to `True`
|
||||
:type reqinfo: bool, optional
|
||||
"""
|
||||
"""Class for controlling your Aternos Minecraft server"""
|
||||
|
||||
def __init__(
|
||||
self, servid: str,
|
||||
atconn: AternosConnect,
|
||||
reqinfo: bool = True) -> None:
|
||||
|
||||
"""Class for controlling your Aternos Minecraft server
|
||||
|
||||
Args:
|
||||
servid (str): Unique server IDentifier
|
||||
atconn (AternosConnect):
|
||||
AternosConnect instance with initialized Aternos session
|
||||
reqinfo (bool, optional): Automatically call
|
||||
`fetch()` to get all info
|
||||
"""
|
||||
|
||||
self.servid = servid
|
||||
self.atconn = atconn
|
||||
if reqinfo:
|
||||
|
@ -75,15 +81,17 @@ class AternosServer:
|
|||
|
||||
def wss(self, autoconfirm: bool = False) -> AternosWss:
|
||||
|
||||
"""Returns :class:`python_aternos.atwss.AternosWss`
|
||||
instance for listening server streams in real-time
|
||||
"""Returns AternosWss instance for
|
||||
listening server streams in real-time
|
||||
|
||||
:param autoconfirm: Automatically start server status listener
|
||||
when AternosWss connects to API to confirm
|
||||
server launching, defaults to `False`
|
||||
:type autoconfirm: bool, optional
|
||||
:return: :class:`python_aternos.atwss.AternosWss` object
|
||||
:rtype: python_aternos.atwss.AternosWss
|
||||
Args:
|
||||
autoconfirm (bool, optional):
|
||||
Automatically start server status listener
|
||||
when AternosWss connects to API to confirm
|
||||
server launching
|
||||
|
||||
Returns:
|
||||
AternosWss object
|
||||
"""
|
||||
|
||||
return AternosWss(self, autoconfirm)
|
||||
|
@ -95,14 +103,16 @@ class AternosServer:
|
|||
|
||||
"""Starts a server
|
||||
|
||||
:param headstart: Start a server in the headstart mode
|
||||
which allows you to skip all queue, defaults to `False`
|
||||
:type headstart: bool, optional
|
||||
:param accepteula: Automatically accept
|
||||
the Mojang EULA, defaults to `True`
|
||||
:type accepteula: bool, optional
|
||||
:raises ServerStartError: When Aternos
|
||||
is unable to start the server
|
||||
Args:
|
||||
headstart (bool, optional): Start a server in
|
||||
the headstart mode which allows
|
||||
you to skip all queue
|
||||
accepteula (bool, optional):
|
||||
Automatically accept the Mojang EULA
|
||||
|
||||
Raises:
|
||||
ServerStartError: When Aternos
|
||||
is unable to start the server
|
||||
"""
|
||||
|
||||
startreq = self.atserver_request(
|
||||
|
@ -171,66 +181,64 @@ class AternosServer:
|
|||
|
||||
def files(self) -> FileManager:
|
||||
|
||||
"""Returns :class:`python_aternos.atfm.FileManager`
|
||||
instance for file operations
|
||||
"""Returns FileManager instance
|
||||
for file operations
|
||||
|
||||
:return: :class:`python_aternos.atfm.FileManager` object
|
||||
:rtype: python_aternos.atfm.FileManager
|
||||
Returns:
|
||||
FileManager object
|
||||
"""
|
||||
|
||||
return FileManager(self)
|
||||
|
||||
def config(self) -> AternosConfig:
|
||||
|
||||
"""Returns :class:`python_aternos.atconf.AternosConfig`
|
||||
instance for editing server settings
|
||||
"""Returns AternosConfig instance
|
||||
for editing server settings
|
||||
|
||||
:return: :class:`python_aternos.atconf.AternosConfig` object
|
||||
:rtype: python_aternos.atconf.AternosConfig
|
||||
Returns:
|
||||
AternosConfig object
|
||||
"""
|
||||
|
||||
return AternosConfig(self)
|
||||
|
||||
def players(self, lst: Lists) -> PlayersList:
|
||||
|
||||
"""Returns :class:`python_aternos.atplayers.PlayersList`
|
||||
instance for managing operators, whitelist and banned players lists
|
||||
"""Returns PlayersList instance
|
||||
for managing operators, whitelist
|
||||
and banned players lists
|
||||
|
||||
:param lst: Players list type, must be
|
||||
the :class:`python_aternos.atplayers.Lists` enum value
|
||||
:type lst: python_aternos.atplayers.Lists
|
||||
:return: :class:`python_aternos.atplayers.PlayersList`
|
||||
:rtype: python_aternos.atplayers.PlayersList
|
||||
Args:
|
||||
lst (Lists): Players list type,
|
||||
must be the atplayers.Lists enum value
|
||||
|
||||
Returns:
|
||||
PlayersList object
|
||||
"""
|
||||
|
||||
return PlayersList(lst, self)
|
||||
|
||||
def atserver_request(
|
||||
self, url: str, method: str,
|
||||
params: Optional[dict] = None,
|
||||
data: Optional[dict] = None,
|
||||
headers: Optional[dict] = None,
|
||||
sendtoken: bool = False) -> Response:
|
||||
params: Optional[Dict[Any, Any]] = None,
|
||||
data: Optional[Dict[Any, Any]] = None,
|
||||
headers: Optional[Dict[Any, Any]] = None,
|
||||
sendtoken: bool = False) -> requests.Response:
|
||||
|
||||
"""Sends a request to Aternos API
|
||||
with server IDenitfier parameter
|
||||
|
||||
:param url: Request URL
|
||||
:type url: str
|
||||
:param method: Request method, must be GET or POST
|
||||
:type method: str
|
||||
:param params: URL parameters, defaults to None
|
||||
:type params: Optional[dict], optional
|
||||
:param data: POST request data, if the method is GET,
|
||||
this dict will be combined with params, defaults to None
|
||||
:type data: Optional[dict], optional
|
||||
:param headers: Custom headers, defaults to None
|
||||
:type headers: Optional[dict], optional
|
||||
:param sendtoken: If the ajax and SEC token
|
||||
should be sent, defaults to False
|
||||
:type sendtoken: bool, optional
|
||||
:return: API response
|
||||
:rtype: requests.Response
|
||||
Args:
|
||||
url (str): Request URL
|
||||
method (str): Request method, must be GET or POST
|
||||
params (Optional[Dict[Any, Any]], optional): URL parameters
|
||||
data (Optional[Dict[Any, Any]], optional): POST request data,
|
||||
if the method is GET, this dict
|
||||
will be combined with params
|
||||
headers (Optional[Dict[Any, Any]], optional): Custom headers
|
||||
sendtoken (bool, optional): If the ajax and SEC token should be sent
|
||||
|
||||
Returns:
|
||||
API response
|
||||
"""
|
||||
|
||||
return self.atconn.request_cloudflare(
|
||||
|
@ -246,10 +254,11 @@ class AternosServer:
|
|||
@property
|
||||
def subdomain(self) -> str:
|
||||
|
||||
"""Server subdomain (part of domain before `.aternos.me`)
|
||||
"""Server subdomain
|
||||
(the part of domain before `.aternos.me`)
|
||||
|
||||
:return: Subdomain
|
||||
:rtype: str
|
||||
Returns:
|
||||
Subdomain
|
||||
"""
|
||||
|
||||
atdomain = self.domain
|
||||
|
@ -258,10 +267,10 @@ class AternosServer:
|
|||
@subdomain.setter
|
||||
def subdomain(self, value: str) -> None:
|
||||
|
||||
"""Set new subdomain for your server
|
||||
"""Set a new subdomain for your server
|
||||
|
||||
:param value: Subdomain
|
||||
:type value: str
|
||||
Args:
|
||||
value (str): Subdomain
|
||||
"""
|
||||
|
||||
self.atserver_request(
|
||||
|
@ -273,11 +282,12 @@ class AternosServer:
|
|||
@property
|
||||
def motd(self) -> str:
|
||||
|
||||
"""Server message of the day,
|
||||
which is shown below its name in the servers list
|
||||
"""Server message of the day
|
||||
which is shown below its name
|
||||
in the Minecraft servers list
|
||||
|
||||
:return: MOTD
|
||||
:rtype: str
|
||||
Returns:
|
||||
MOTD
|
||||
"""
|
||||
|
||||
return self._info['motd']
|
||||
|
@ -285,10 +295,10 @@ class AternosServer:
|
|||
@motd.setter
|
||||
def motd(self, value: str) -> None:
|
||||
|
||||
"""Set new message of the day
|
||||
"""Set a new message of the day
|
||||
|
||||
:param value: MOTD
|
||||
:type value: str
|
||||
Args:
|
||||
value (str): New MOTD
|
||||
"""
|
||||
|
||||
self.atserver_request(
|
||||
|
@ -300,10 +310,11 @@ class AternosServer:
|
|||
@property
|
||||
def address(self) -> str:
|
||||
|
||||
"""Full server address including domain and port
|
||||
"""Full server address
|
||||
including domain and port
|
||||
|
||||
:return: Server address
|
||||
:rtype: str
|
||||
Returns:
|
||||
Server address
|
||||
"""
|
||||
|
||||
return self._info['displayAddress']
|
||||
|
@ -311,11 +322,11 @@ class AternosServer:
|
|||
@property
|
||||
def domain(self) -> str:
|
||||
|
||||
"""Server domain (test.aternos.me),
|
||||
address without port number
|
||||
"""Server domain (e.g. `test.aternos.me`).
|
||||
In other words, address without port number
|
||||
|
||||
:return: Domain
|
||||
:rtype: str
|
||||
Returns:
|
||||
Domain
|
||||
"""
|
||||
|
||||
return self._info['ip']
|
||||
|
@ -325,8 +336,8 @@ class AternosServer:
|
|||
|
||||
"""Server port number
|
||||
|
||||
:return: Port
|
||||
:rtype: int
|
||||
Returns:
|
||||
Port
|
||||
"""
|
||||
|
||||
return self._info['port']
|
||||
|
@ -336,20 +347,42 @@ class AternosServer:
|
|||
|
||||
"""Server software edition: Java or Bedrock
|
||||
|
||||
:return: Software edition
|
||||
:rtype: Edition
|
||||
Returns:
|
||||
Software edition
|
||||
"""
|
||||
|
||||
soft_type = self._info['bedrock']
|
||||
return Edition(soft_type)
|
||||
|
||||
@property
|
||||
def is_java(self) -> bool:
|
||||
|
||||
"""Check if server software is Java Edition
|
||||
|
||||
Returns:
|
||||
Is it Minecraft JE
|
||||
"""
|
||||
|
||||
return not self._info['bedrock']
|
||||
|
||||
@property
|
||||
def is_bedrock(self) -> bool:
|
||||
|
||||
"""Check if server software is Bedrock Edition
|
||||
|
||||
Returns:
|
||||
Is it Minefcraft BE
|
||||
"""
|
||||
|
||||
return bool(self._info['bedrock'])
|
||||
|
||||
@property
|
||||
def software(self) -> str:
|
||||
|
||||
"""Server software name (e.g. `Vanilla`)
|
||||
|
||||
:return: Software name
|
||||
:rtype: str
|
||||
Returns:
|
||||
Software name
|
||||
"""
|
||||
|
||||
return self._info['software']
|
||||
|
@ -357,33 +390,50 @@ class AternosServer:
|
|||
@property
|
||||
def version(self) -> str:
|
||||
|
||||
"""Server software version (e.g. `1.16.5`)
|
||||
"""Server software version (1.16.5)
|
||||
|
||||
:return: Software version
|
||||
:rtype: str
|
||||
Returns:
|
||||
Software version
|
||||
"""
|
||||
|
||||
return self._info['version']
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
def css_class(self) -> str:
|
||||
|
||||
"""Server status string (offline, loading)
|
||||
"""CSS class for
|
||||
server status block
|
||||
on official web site
|
||||
(offline, loading,
|
||||
loading starting, queueing)
|
||||
|
||||
:return: Status string
|
||||
:rtype: str
|
||||
Returns:
|
||||
CSS class
|
||||
"""
|
||||
|
||||
return self._info['class']
|
||||
|
||||
@property
|
||||
def status_num(self) -> int:
|
||||
def status(self) -> str:
|
||||
|
||||
"""Server numeric status. It is highly recommended
|
||||
to use status string instead of a number.
|
||||
"""Server status string
|
||||
(offline, loading, preparing)
|
||||
|
||||
:return: Status code
|
||||
:rtype: Status
|
||||
Returns:
|
||||
Status string
|
||||
"""
|
||||
|
||||
return self._info['lang']
|
||||
|
||||
@property
|
||||
def status_num(self) -> Status:
|
||||
|
||||
"""Server numeric status.
|
||||
It is highly recommended to use
|
||||
status string instead of a number
|
||||
|
||||
Returns:
|
||||
Status code
|
||||
"""
|
||||
|
||||
return Status(self._info['status'])
|
||||
|
@ -391,10 +441,10 @@ class AternosServer:
|
|||
@property
|
||||
def players_list(self) -> List[str]:
|
||||
|
||||
"""List of connected players nicknames
|
||||
"""List of connected players' nicknames
|
||||
|
||||
:return: Connected players
|
||||
:rtype: List[str]
|
||||
Returns:
|
||||
Connected players
|
||||
"""
|
||||
|
||||
return self._info['playerlist']
|
||||
|
@ -402,10 +452,10 @@ class AternosServer:
|
|||
@property
|
||||
def players_count(self) -> int:
|
||||
|
||||
"""How many connected players
|
||||
"""How many players are connected
|
||||
|
||||
:return: Connected players count
|
||||
:rtype: int
|
||||
Returns:
|
||||
Connected players count
|
||||
"""
|
||||
|
||||
return int(self._info['players'])
|
||||
|
@ -413,10 +463,11 @@ class AternosServer:
|
|||
@property
|
||||
def slots(self) -> int:
|
||||
|
||||
"""Server slots, how many players can connect
|
||||
"""Server slots, how many
|
||||
players **can** connect
|
||||
|
||||
:return: Slots count
|
||||
:rtype: int
|
||||
Returns:
|
||||
Slots count
|
||||
"""
|
||||
|
||||
return int(self._info['slots'])
|
||||
|
@ -426,8 +477,8 @@ class AternosServer:
|
|||
|
||||
"""Server used RAM in MB
|
||||
|
||||
:return: Used RAM
|
||||
:rtype: int
|
||||
Returns:
|
||||
Used RAM
|
||||
"""
|
||||
|
||||
return int(self._info['ram'])
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
"""Connects to Aternos API websocket
|
||||
"""Connects to Aternos WebSocket API
|
||||
for real-time information"""
|
||||
|
||||
import enum
|
||||
|
@ -35,27 +35,31 @@ class Streams(enum.Enum):
|
|||
none = (-1, None)
|
||||
|
||||
def __init__(self, num: int, stream: str) -> None:
|
||||
|
||||
self.num = num
|
||||
self.stream = stream
|
||||
|
||||
|
||||
class AternosWss:
|
||||
|
||||
"""Class for managing websocket connection
|
||||
|
||||
:param atserv: :class:`python_aternos.atserver.AternosServer` instance
|
||||
:type atserv: python_aternos.atserver.AternosServer
|
||||
:param autoconfirm: Automatically start server status listener
|
||||
when AternosWss connects to API to confirm
|
||||
server launching, defaults to `False`
|
||||
:type autoconfirm: bool, optional
|
||||
"""
|
||||
"""Class for managing websocket connection"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
atserv: 'AternosServer',
|
||||
autoconfirm: bool = False) -> None:
|
||||
|
||||
"""Class for managing websocket connection
|
||||
|
||||
Args:
|
||||
atserv (AternosServer):
|
||||
atserver.AternosServer instance
|
||||
autoconfirm (bool, optional):
|
||||
Automatically start server status listener
|
||||
when AternosWss connects to API to confirm
|
||||
server launching
|
||||
"""
|
||||
|
||||
self.atserv = atserv
|
||||
self.servid = atserv.servid
|
||||
|
||||
|
@ -73,7 +77,9 @@ class AternosWss:
|
|||
|
||||
async def confirm(self) -> None:
|
||||
|
||||
"""Simple way to call AternosServer.confirm from this class"""
|
||||
"""Simple way to call
|
||||
`AternosServer.confirm`
|
||||
from this class"""
|
||||
|
||||
self.atserv.confirm()
|
||||
|
||||
|
@ -86,12 +92,12 @@ class AternosWss:
|
|||
When websocket receives message from the specified stream,
|
||||
it calls all listeners created with this decorator.
|
||||
|
||||
:param stream: Stream that your function should listen
|
||||
:type stream: python_aternos.atwss.Streams
|
||||
:param args: Arguments which will be passed to your function
|
||||
:type args: tuple, optional
|
||||
:return: ...
|
||||
:rtype: Callable[[Callable[[Any], Coroutine[Any, Any, None]]], Any]
|
||||
Args:
|
||||
stream (Streams): Stream that your function should listen
|
||||
*args (tuple, optional): Arguments which will be passed to your function
|
||||
|
||||
Returns:
|
||||
...
|
||||
"""
|
||||
|
||||
def decorator(func: FunctionT) -> None:
|
||||
|
@ -100,7 +106,8 @@ class AternosWss:
|
|||
|
||||
async def connect(self) -> None:
|
||||
|
||||
"""Connect to the websocket server and start all stream listeners"""
|
||||
"""Connects to the websocket server
|
||||
and starts all stream listeners"""
|
||||
|
||||
headers = [
|
||||
('Host', 'aternos.org'),
|
||||
|
@ -118,9 +125,14 @@ class AternosWss:
|
|||
)
|
||||
|
||||
@self.wssreceiver(Streams.status)
|
||||
async def confirmfunc(msg):
|
||||
async def confirmfunc(msg: Dict[str, Any]) -> None:
|
||||
|
||||
"""Automatically confirm Minecraft server launching"""
|
||||
"""Automatically confirm
|
||||
Minecraft server launching
|
||||
|
||||
Args:
|
||||
msg (Dict[str, Any]): Server info dictionary
|
||||
"""
|
||||
|
||||
if not self.autoconfirm:
|
||||
return
|
||||
|
@ -130,13 +142,14 @@ class AternosWss:
|
|||
confirmation = in_queue and pending
|
||||
|
||||
if confirmation and not self.confirmed:
|
||||
self.confirm()
|
||||
await self.confirm()
|
||||
|
||||
@self.wssreceiver(Streams.status)
|
||||
async def streamsfunc(msg):
|
||||
async def streamsfunc(msg: Dict[str, Any]) -> None:
|
||||
|
||||
"""Automatically starts streams. Detailed description:
|
||||
|
||||
https://github.com/DarkCat09/python-aternos/issues/22#issuecomment-1146788496
|
||||
According to the websocket messages from the web site,
|
||||
Aternos can't receive any data from a stream (e.g. console) until
|
||||
it requests this stream via the special message
|
||||
|
@ -148,7 +161,9 @@ class AternosWss:
|
|||
these data is sent from API by default, so there's None value in
|
||||
the second item of its stream type tuple
|
||||
(`<Streams.status: (0, None)>`).
|
||||
https://github.com/DarkCat09/python-aternos/issues/22#issuecomment-1146788496
|
||||
|
||||
Args:
|
||||
msg (Dict[str, Any]): Server info dictionary
|
||||
"""
|
||||
|
||||
if msg['status'] == 2:
|
||||
|
@ -159,7 +174,7 @@ class AternosWss:
|
|||
continue
|
||||
|
||||
if strm.stream:
|
||||
logging.debug(f'Enabling {strm.stream} stream')
|
||||
logging.debug(f'Requesting {strm.stream} stream')
|
||||
await self.send({
|
||||
'stream': strm.stream,
|
||||
'type': 'start'
|
||||
|
@ -180,8 +195,9 @@ class AternosWss:
|
|||
|
||||
"""Sends a message to websocket server
|
||||
|
||||
:param obj: Message, may be a string or a dict
|
||||
:type obj: Union[Dict[str, Any],str]
|
||||
Args:
|
||||
obj (Union[Dict[str, Any],str]):
|
||||
Message, may be a string or a dict
|
||||
"""
|
||||
|
||||
if isinstance(obj, dict):
|
||||
|
|
Reference in a new issue