Docstrings, logging in websocket example
This commit is contained in:
parent
72135f41fc
commit
9f0610e5a7
9 changed files with 423 additions and 58 deletions
|
@ -1,9 +1,15 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import logging
|
||||||
from getpass import getpass
|
from getpass import getpass
|
||||||
from python_aternos import Client, atwss
|
from python_aternos import Client, atwss
|
||||||
|
|
||||||
user = input('Username: ')
|
user = input('Username: ')
|
||||||
pswd = getpass('Password: ')
|
pswd = getpass('Password: ')
|
||||||
|
|
||||||
|
logs = input('Show detailed logs? (y/n) ').strip().lower() == 'y'
|
||||||
|
if logs:
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
aternos = Client.from_credentials(user, pswd)
|
aternos = Client.from_credentials(user, pswd)
|
||||||
|
|
||||||
s = aternos.list_servers()[0]
|
s = aternos.list_servers()[0]
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
import enum
|
import enum
|
||||||
import re
|
import re
|
||||||
import lxml.html
|
import lxml.html
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Union, Optional
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .atserver import AternosServer
|
from .atserver import AternosServer
|
||||||
|
|
||||||
#
|
|
||||||
# server.options
|
|
||||||
class ServerOpts(enum.Enum):
|
class ServerOpts(enum.Enum):
|
||||||
|
|
||||||
|
"""server.options file"""
|
||||||
|
|
||||||
players = 'max-players'
|
players = 'max-players'
|
||||||
gm = 'gamemode'
|
gm = 'gamemode'
|
||||||
difficulty = 'difficulty'
|
difficulty = 'difficulty'
|
||||||
|
@ -31,15 +32,19 @@ class ServerOpts(enum.Enum):
|
||||||
DAT_PREFIX = 'Data:'
|
DAT_PREFIX = 'Data:'
|
||||||
DAT_GR_PREFIX = 'Data:GameRules:'
|
DAT_GR_PREFIX = 'Data:GameRules:'
|
||||||
|
|
||||||
# level.dat
|
|
||||||
class WorldOpts(enum.Enum):
|
class WorldOpts(enum.Enum):
|
||||||
|
|
||||||
|
"""level.dat file"""
|
||||||
|
|
||||||
seed12 = 'randomseed'
|
seed12 = 'randomseed'
|
||||||
seed = 'seed'
|
seed = 'seed'
|
||||||
hardcore = 'hardcore'
|
hardcore = 'hardcore'
|
||||||
difficulty = 'Difficulty'
|
difficulty = 'Difficulty'
|
||||||
|
|
||||||
# /gamerule
|
|
||||||
class WorldRules(enum.Enum):
|
class WorldRules(enum.Enum):
|
||||||
|
|
||||||
|
"""/gamerule list"""
|
||||||
|
|
||||||
advs = 'announceAdvancements'
|
advs = 'announceAdvancements'
|
||||||
univanger = 'universalAnger'
|
univanger = 'universalAnger'
|
||||||
cmdout = 'commandBlockOutput'
|
cmdout = 'commandBlockOutput'
|
||||||
|
@ -76,41 +81,24 @@ class WorldRules(enum.Enum):
|
||||||
spectchunkgen = 'spectatorsGenerateChunks'
|
spectchunkgen = 'spectatorsGenerateChunks'
|
||||||
cmdfb = 'sendCommandFeedback'
|
cmdfb = 'sendCommandFeedback'
|
||||||
|
|
||||||
DAT_TYPE_WORLD = 0
|
|
||||||
DAT_TYPE_GR = 1
|
|
||||||
|
|
||||||
class Gamemode(enum.IntEnum):
|
class Gamemode(enum.IntEnum):
|
||||||
|
|
||||||
|
"""/gamemode numeric list"""
|
||||||
|
|
||||||
survival = 0
|
survival = 0
|
||||||
creative = 1
|
creative = 1
|
||||||
adventure = 2
|
adventure = 2
|
||||||
spectator = 3
|
spectator = 3
|
||||||
|
|
||||||
class Difficulty(enum.IntEnum):
|
class Difficulty(enum.IntEnum):
|
||||||
|
|
||||||
|
"""/difficulty numeric list"""
|
||||||
|
|
||||||
peaceful = 0
|
peaceful = 0
|
||||||
easy = 1
|
easy = 1
|
||||||
normal = 2
|
normal = 2
|
||||||
hard = 3
|
hard = 3
|
||||||
|
|
||||||
#
|
|
||||||
# jre types for set_java
|
|
||||||
javatype = {
|
|
||||||
'jdk': 'openjdk:{ver}',
|
|
||||||
'openj9-1': 'adoptopenjdk:{ver}-jre-openj9-bionic',
|
|
||||||
'openj9-2': 'ibm-semeru-runtimes:open-{ver}-jre'
|
|
||||||
}
|
|
||||||
# checking java version format
|
|
||||||
javacheck = re.compile(
|
|
||||||
''.join(
|
|
||||||
list(
|
|
||||||
map(
|
|
||||||
# create a regexp for each jre type,
|
|
||||||
# e.g.: (^openjdk:\d+$)|
|
|
||||||
lambda i: '(^' + javatype[i].format(ver=r'\d+') + '$)|',
|
|
||||||
javatype
|
|
||||||
)
|
|
||||||
)
|
|
||||||
).rstrip('|')
|
|
||||||
)
|
|
||||||
# checking timezone format
|
# checking timezone format
|
||||||
tzcheck = re.compile(r'(^[A-Z]\w+\/[A-Z]\w+$)|^UTC$')
|
tzcheck = re.compile(r'(^[A-Z]\w+\/[A-Z]\w+$)|^UTC$')
|
||||||
# options types converting
|
# options types converting
|
||||||
|
@ -120,15 +108,26 @@ convert = {
|
||||||
'config-option-toggle': bool
|
'config-option-toggle': bool
|
||||||
}
|
}
|
||||||
|
|
||||||
# MAIN CLASS
|
|
||||||
class AternosConfig:
|
class AternosConfig:
|
||||||
|
|
||||||
|
"""Class for editing server settings
|
||||||
|
|
||||||
|
:param atserv: :class:`python_aternos.atserver.AternosServer` object
|
||||||
|
:type atserv: python_aternos.atserver.AternosServer
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, atserv:'AternosServer') -> None:
|
def __init__(self, atserv:'AternosServer') -> None:
|
||||||
|
|
||||||
self.atserv = atserv
|
self.atserv = atserv
|
||||||
|
|
||||||
def get_timezone(self) -> str:
|
def get_timezone(self) -> str:
|
||||||
|
|
||||||
|
"""Parses timezone from options page
|
||||||
|
|
||||||
|
:return: Area/Location
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
|
||||||
optreq = self.atserv.atserver_request(
|
optreq = self.atserv.atserver_request(
|
||||||
'https://aternos.org/options', 'GET'
|
'https://aternos.org/options', 'GET'
|
||||||
)
|
)
|
||||||
|
@ -140,6 +139,14 @@ class AternosConfig:
|
||||||
|
|
||||||
def set_timezone(self, value:str) -> None:
|
def set_timezone(self, value:str) -> None:
|
||||||
|
|
||||||
|
"""Sets new timezone
|
||||||
|
|
||||||
|
:param value: New timezone
|
||||||
|
:type value: str
|
||||||
|
:raises ValueError: If given string
|
||||||
|
doesn't match Area/Location format
|
||||||
|
"""
|
||||||
|
|
||||||
matches_tz = tzcheck.search(value)
|
matches_tz = tzcheck.search(value)
|
||||||
if not matches_tz:
|
if not matches_tz:
|
||||||
raise ValueError('Timezone must match zoneinfo format: Area/Location')
|
raise ValueError('Timezone must match zoneinfo format: Area/Location')
|
||||||
|
@ -150,7 +157,13 @@ class AternosConfig:
|
||||||
sendtoken=True
|
sendtoken=True
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_java(self) -> str:
|
def get_java(self) -> int:
|
||||||
|
|
||||||
|
"""Parses Java version from options page
|
||||||
|
|
||||||
|
:return: Java image version
|
||||||
|
:rtype: int
|
||||||
|
"""
|
||||||
|
|
||||||
optreq = self.atserv.atserver_request(
|
optreq = self.atserv.atserver_request(
|
||||||
'https://aternos.org/options', 'GET'
|
'https://aternos.org/options', 'GET'
|
||||||
|
@ -158,17 +171,21 @@ class AternosConfig:
|
||||||
opttree = lxml.html.fromstring(optreq)
|
opttree = lxml.html.fromstring(optreq)
|
||||||
imgopt = opttree.xpath('//div[@class="options-other-input image-switch"]')[0]
|
imgopt = opttree.xpath('//div[@class="options-other-input image-switch"]')[0]
|
||||||
imgver = imgopt.xpath('.//div[@class="option current"]/@data-value')[0]
|
imgver = imgopt.xpath('.//div[@class="option current"]/@data-value')[0]
|
||||||
return imgver
|
|
||||||
|
|
||||||
def set_java(self, value:str) -> None:
|
jdkver = str(imgver or '').removeprefix('openjdk:')
|
||||||
|
return int(jdkver)
|
||||||
|
|
||||||
|
def set_java(self, value:int) -> None:
|
||||||
|
|
||||||
matches_jdkver = javacheck.search(value)
|
"""Sets new Java version
|
||||||
if not matches_jdkver:
|
|
||||||
raise ValueError('Incorrect Java image version format!')
|
:param value: New Java image version
|
||||||
|
:type value: int
|
||||||
|
"""
|
||||||
|
|
||||||
self.atserv.atserver_request(
|
self.atserv.atserver_request(
|
||||||
'https://aternos.org/panel/ajax/image.php',
|
'https://aternos.org/panel/ajax/image.php',
|
||||||
'POST', data={'image': value},
|
'POST', data={'image': f'openjdk:{value}'},
|
||||||
sendtoken=True
|
sendtoken=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -176,15 +193,42 @@ class AternosConfig:
|
||||||
# server.properties
|
# server.properties
|
||||||
#
|
#
|
||||||
def set_server_prop(self, option:str, value:Any) -> None:
|
def set_server_prop(self, option:str, value:Any) -> None:
|
||||||
|
|
||||||
|
"""Sets server.properties option
|
||||||
|
|
||||||
|
:param option: Option name
|
||||||
|
:type option: str
|
||||||
|
:param value: New value
|
||||||
|
:type value: Any
|
||||||
|
"""
|
||||||
|
|
||||||
self.__set_prop(
|
self.__set_prop(
|
||||||
'/server.properties',
|
'/server.properties',
|
||||||
option, value
|
option, value
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_server_props(self, proptyping:bool=True) -> Dict[str,Any]:
|
def get_server_props(self, proptyping:bool=True) -> Dict[str,Any]:
|
||||||
|
|
||||||
|
"""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]
|
||||||
|
"""
|
||||||
|
|
||||||
return self.__get_all_props('https://aternos.org/options', proptyping)
|
return self.__get_all_props('https://aternos.org/options', proptyping)
|
||||||
|
|
||||||
def set_server_props(self, props:Dict[str,Any]) -> None:
|
def set_server_props(self, props:Dict[str,Any]) -> None:
|
||||||
|
|
||||||
|
"""Updates server.properties options with the given dict
|
||||||
|
|
||||||
|
:param props: Dict with properties `{key:value}`
|
||||||
|
:type props: Dict[str,Any]
|
||||||
|
"""
|
||||||
|
|
||||||
for key in props:
|
for key in props:
|
||||||
self.set_server_prop(key, props[key])
|
self.set_server_prop(key, props[key])
|
||||||
|
|
||||||
|
@ -192,11 +236,26 @@ class AternosConfig:
|
||||||
# level.dat
|
# level.dat
|
||||||
#
|
#
|
||||||
def set_world_prop(
|
def set_world_prop(
|
||||||
self, option:str, value:Any,
|
self, option:Union[WorldOpts,WorldRules],
|
||||||
proptype:int, world:str='world') -> None:
|
value:Any, gamerule:bool=False,
|
||||||
|
world:str='world') -> None:
|
||||||
|
|
||||||
|
"""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
|
||||||
|
"""
|
||||||
|
|
||||||
prefix = DAT_PREFIX
|
prefix = DAT_PREFIX
|
||||||
if proptype == DAT_TYPE_GR:
|
if gamerule:
|
||||||
prefix = DAT_GR_PREFIX
|
prefix = DAT_GR_PREFIX
|
||||||
|
|
||||||
self.__set_prop(
|
self.__set_prop(
|
||||||
|
@ -209,6 +268,18 @@ class AternosConfig:
|
||||||
self, world:str='world',
|
self, world:str='world',
|
||||||
proptyping:bool=True) -> Dict[str,Any]:
|
proptyping:bool=True) -> Dict[str,Any]:
|
||||||
|
|
||||||
|
"""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]
|
||||||
|
"""
|
||||||
|
|
||||||
self.__get_all_props(
|
self.__get_all_props(
|
||||||
f'https://aternos.org/files/{world}/level.dat',
|
f'https://aternos.org/files/{world}/level.dat',
|
||||||
proptyping, [DAT_PREFIX, DAT_GR_PREFIX]
|
proptyping, [DAT_PREFIX, DAT_GR_PREFIX]
|
||||||
|
|
|
@ -12,6 +12,10 @@ REQUA = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko)
|
||||||
|
|
||||||
class AternosConnect:
|
class AternosConnect:
|
||||||
|
|
||||||
|
"""
|
||||||
|
Class for sending API requests bypass Cloudflare
|
||||||
|
and parsing responses"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
|
|
||||||
self.session = CloudScraper()
|
self.session = CloudScraper()
|
||||||
|
@ -19,6 +23,17 @@ class AternosConnect:
|
||||||
|
|
||||||
def parse_token(self) -> str:
|
def parse_token(self) -> str:
|
||||||
|
|
||||||
|
"""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
|
||||||
|
"""
|
||||||
|
|
||||||
loginpage = self.request_cloudflare(
|
loginpage = self.request_cloudflare(
|
||||||
f'https://aternos.org/go/', 'GET'
|
f'https://aternos.org/go/', 'GET'
|
||||||
).content
|
).content
|
||||||
|
@ -55,6 +70,13 @@ class AternosConnect:
|
||||||
|
|
||||||
def generate_sec(self) -> str:
|
def generate_sec(self) -> str:
|
||||||
|
|
||||||
|
"""Generates Aternos SEC token which
|
||||||
|
is also needed for most API requests
|
||||||
|
|
||||||
|
:return: Random SEC key:value string
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
|
||||||
randkey = self.generate_aternos_rand()
|
randkey = self.generate_aternos_rand()
|
||||||
randval = self.generate_aternos_rand()
|
randval = self.generate_aternos_rand()
|
||||||
self.sec = f'{randkey}:{randval}'
|
self.sec = f'{randkey}:{randval}'
|
||||||
|
@ -67,6 +89,15 @@ class AternosConnect:
|
||||||
|
|
||||||
def generate_aternos_rand(self, randlen:int=16) -> str:
|
def generate_aternos_rand(self, randlen:int=16) -> str:
|
||||||
|
|
||||||
|
"""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
|
||||||
|
"""
|
||||||
|
|
||||||
# a list with randlen+1 empty strings:
|
# a list with randlen+1 empty strings:
|
||||||
# generate a string with spaces,
|
# generate a string with spaces,
|
||||||
# then split it by space
|
# then split it by space
|
||||||
|
@ -81,6 +112,20 @@ class AternosConnect:
|
||||||
self, num:Union[int,float,str],
|
self, num:Union[int,float,str],
|
||||||
base:int, frombase:int=10) -> str:
|
base:int, frombase:int=10) -> str:
|
||||||
|
|
||||||
|
"""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
|
||||||
|
"""
|
||||||
|
|
||||||
if isinstance(num, str):
|
if isinstance(num, str):
|
||||||
num = int(num, frombase)
|
num = int(num, frombase)
|
||||||
|
|
||||||
|
@ -101,9 +146,41 @@ class AternosConnect:
|
||||||
self, url:str, method:str,
|
self, url:str, method:str,
|
||||||
params:Optional[dict]=None, data:Optional[dict]=None,
|
params:Optional[dict]=None, data:Optional[dict]=None,
|
||||||
headers:Optional[dict]=None, reqcookies:Optional[dict]=None,
|
headers:Optional[dict]=None, reqcookies:Optional[dict]=None,
|
||||||
sendtoken:bool=False, redirect:bool=True, retry:int=0) -> Response:
|
sendtoken:bool=False, redirect:bool=True, retry:int=3) -> Response:
|
||||||
|
|
||||||
if retry > 3:
|
"""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 redirect: If requests lib should follow
|
||||||
|
Location header in 3xx responses, defaults to True
|
||||||
|
:type redirect: bool, optional
|
||||||
|
:param retry: How many times parser must retry
|
||||||
|
connection to API bypass Cloudflare, defaults to 3
|
||||||
|
: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
|
||||||
|
"""
|
||||||
|
|
||||||
|
if retry <= 0:
|
||||||
raise CloudflareError('Unable to bypass Cloudflare protection')
|
raise CloudflareError('Unable to bypass Cloudflare protection')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -157,7 +234,7 @@ class AternosConnect:
|
||||||
params, data,
|
params, data,
|
||||||
headers, reqcookies,
|
headers, reqcookies,
|
||||||
sendtoken, redirect,
|
sendtoken, redirect,
|
||||||
retry + 1
|
retry - 1
|
||||||
)
|
)
|
||||||
|
|
||||||
logging.info(
|
logging.info(
|
||||||
|
|
|
@ -9,14 +9,31 @@ if TYPE_CHECKING:
|
||||||
from .atserver import AternosServer
|
from .atserver import AternosServer
|
||||||
|
|
||||||
class FileType(enum.IntEnum):
|
class FileType(enum.IntEnum):
|
||||||
|
|
||||||
|
"""File or dierctory"""
|
||||||
|
|
||||||
file = 0
|
file = 0
|
||||||
directory = 1
|
directory = 1
|
||||||
|
|
||||||
class AternosFile:
|
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
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, atserv:'AternosServer',
|
self, atserv:'AternosServer',
|
||||||
path:str, name:str, ftype:int=FileType.file,
|
path:str, name:str, ftype:FileType=FileType.file,
|
||||||
size:Union[int,float]=0) -> None:
|
size:Union[int,float]=0) -> None:
|
||||||
|
|
||||||
self.atserv = atserv
|
self.atserv = atserv
|
||||||
|
@ -28,6 +45,8 @@ class AternosFile:
|
||||||
|
|
||||||
def delete(self) -> None:
|
def delete(self) -> None:
|
||||||
|
|
||||||
|
"""Deletes the file"""
|
||||||
|
|
||||||
self.atserv.atserver_request(
|
self.atserv.atserver_request(
|
||||||
'https://aternos.org/panel/ajax/delete.php',
|
'https://aternos.org/panel/ajax/delete.php',
|
||||||
'POST', data={'file': self._full},
|
'POST', data={'file': self._full},
|
||||||
|
@ -36,6 +55,14 @@ class AternosFile:
|
||||||
|
|
||||||
def get_content(self) -> bytes:
|
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
|
||||||
|
"""
|
||||||
|
|
||||||
file = self.atserv.atserver_request(
|
file = self.atserv.atserver_request(
|
||||||
'https://aternos.org/panel/ajax/files/download.php',
|
'https://aternos.org/panel/ajax/files/download.php',
|
||||||
'GET', params={
|
'GET', params={
|
||||||
|
@ -48,6 +75,12 @@ class AternosFile:
|
||||||
|
|
||||||
def set_content(self, value:bytes) -> None:
|
def set_content(self, value:bytes) -> None:
|
||||||
|
|
||||||
|
"""Modifies the file content
|
||||||
|
|
||||||
|
:param value: New content
|
||||||
|
:type value: bytes
|
||||||
|
"""
|
||||||
|
|
||||||
self.atserv.atserver_request(
|
self.atserv.atserver_request(
|
||||||
f'https://aternos.org/panel/ajax/save.php',
|
f'https://aternos.org/panel/ajax/save.php',
|
||||||
'POST', data={
|
'POST', data={
|
||||||
|
@ -58,6 +91,13 @@ class AternosFile:
|
||||||
|
|
||||||
def get_text(self) -> str:
|
def get_text(self) -> str:
|
||||||
|
|
||||||
|
"""Requests editing the file as a text
|
||||||
|
(try it if downloading is disallowed)
|
||||||
|
|
||||||
|
:return: File text content
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
|
||||||
editor = self.atserv.atserver_request(
|
editor = self.atserv.atserver_request(
|
||||||
f'https://aternos.org/files/{self._full.lstrip("/")}', 'GET'
|
f'https://aternos.org/files/{self._full.lstrip("/")}', 'GET'
|
||||||
)
|
)
|
||||||
|
@ -68,6 +108,14 @@ class AternosFile:
|
||||||
|
|
||||||
def set_text(self, value:str) -> None:
|
def set_text(self, value:str) -> None:
|
||||||
|
|
||||||
|
"""Modifies the file content,
|
||||||
|
but unlike set_content takes
|
||||||
|
a string as a new value
|
||||||
|
|
||||||
|
:param value: New content
|
||||||
|
:type value: str
|
||||||
|
"""
|
||||||
|
|
||||||
self.set_content(value.encode('utf-8'))
|
self.set_content(value.encode('utf-8'))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import lxml.html
|
import lxml.html
|
||||||
from typing import Union, List
|
from typing import Union, Optional, List
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from .atfile import AternosFile, FileType
|
from .atfile import AternosFile, FileType
|
||||||
|
@ -8,12 +8,28 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
class FileManager:
|
class FileManager:
|
||||||
|
|
||||||
|
"""Aternos file manager class for viewing files structure
|
||||||
|
|
||||||
|
:param atserv: :class:`python_aternos.atserver.AternosServer` instance
|
||||||
|
:type atserv: python_aternos.atserver.AternosServer
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, atserv:'AternosServer') -> None:
|
def __init__(self, atserv:'AternosServer') -> None:
|
||||||
|
|
||||||
self.atserv = atserv
|
self.atserv = atserv
|
||||||
|
|
||||||
def listdir(self, path:str='') -> List[AternosFile]:
|
def listdir(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]
|
||||||
|
"""
|
||||||
|
|
||||||
path = path.lstrip('/')
|
path = path.lstrip('/')
|
||||||
filesreq = self.atserv.atserver_request(
|
filesreq = self.atserv.atserver_request(
|
||||||
f'https://aternos.org/files/{path}', 'GET'
|
f'https://aternos.org/files/{path}', 'GET'
|
||||||
|
@ -57,6 +73,16 @@ class FileManager:
|
||||||
|
|
||||||
def convert_size(self, num:Union[int,float], measure:str) -> float:
|
def convert_size(self, num:Union[int,float], measure:str) -> float:
|
||||||
|
|
||||||
|
"""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
|
||||||
|
"""
|
||||||
|
|
||||||
measure_match = {
|
measure_match = {
|
||||||
'B': 1,
|
'B': 1,
|
||||||
'kB': 1000,
|
'kB': 1000,
|
||||||
|
@ -68,7 +94,16 @@ class FileManager:
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
def get_file(self, path:str) -> Union[AternosFile,None]:
|
def get_file(self, path:str) -> Optional[AternosFile]:
|
||||||
|
|
||||||
|
"""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]
|
||||||
|
"""
|
||||||
|
|
||||||
filepath = path[:path.rfind('/')]
|
filepath = path[:path.rfind('/')]
|
||||||
filename = path[path.rfind('/'):]
|
filename = path[path.rfind('/'):]
|
||||||
|
@ -82,6 +117,14 @@ class FileManager:
|
||||||
|
|
||||||
def dl_file(self, path:str) -> bytes:
|
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
|
||||||
|
"""
|
||||||
|
|
||||||
file = self.atserv.atserver_request(
|
file = self.atserv.atserver_request(
|
||||||
f'https://aternos.org/panel/ajax/files/download.php?' + \
|
f'https://aternos.org/panel/ajax/files/download.php?' + \
|
||||||
f'file={path.replace("/","%2F")}',
|
f'file={path.replace("/","%2F")}',
|
||||||
|
@ -92,6 +135,15 @@ class FileManager:
|
||||||
|
|
||||||
def dl_world(self, world:str='world') -> bytes:
|
def dl_world(self, world:str='world') -> bytes:
|
||||||
|
|
||||||
|
"""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
|
||||||
|
"""
|
||||||
|
|
||||||
world = self.atserv.atserver_request(
|
world = self.atserv.atserver_request(
|
||||||
f'https://aternos.org/panel/ajax/worlds/download.php?' + \
|
f'https://aternos.org/panel/ajax/worlds/download.php?' + \
|
||||||
f'world={world.replace("/","%2F")}',
|
f'world={world.replace("/","%2F")}',
|
||||||
|
|
|
@ -7,6 +7,15 @@ from typing import Any
|
||||||
arrowexp = regex.compile(r'\w[^\}]*+')
|
arrowexp = regex.compile(r'\w[^\}]*+')
|
||||||
|
|
||||||
def to_ecma5_function(f:str) -> str:
|
def to_ecma5_function(f:str) -> str:
|
||||||
|
|
||||||
|
"""Converts a ECMA6 function to ECMA5 format (without arrow expressions)
|
||||||
|
|
||||||
|
:param f: ECMA6 function
|
||||||
|
:type f: str
|
||||||
|
:return: ECMA5 function
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
|
||||||
match = arrowexp.search(f)
|
match = arrowexp.search(f)
|
||||||
conv = '(function(){' + match.group(0) + '})()'
|
conv = '(function(){' + match.group(0) + '})()'
|
||||||
return regex.sub(
|
return regex.sub(
|
||||||
|
@ -19,6 +28,15 @@ def atob(s:str) -> str:
|
||||||
return base64.standard_b64decode(str(s)).decode('utf-8')
|
return base64.standard_b64decode(str(s)).decode('utf-8')
|
||||||
|
|
||||||
def exec(f:str) -> Any:
|
def exec(f:str) -> Any:
|
||||||
|
|
||||||
|
"""Executes a JavaScript function
|
||||||
|
|
||||||
|
:param f: ECMA6 function
|
||||||
|
:type f: str
|
||||||
|
:return: JavaScript interpreter context
|
||||||
|
:rtype: Any
|
||||||
|
"""
|
||||||
|
|
||||||
ctx = js2py.EvalJs({'atob': atob})
|
ctx = js2py.EvalJs({'atob': atob})
|
||||||
ctx.execute('window.document = { };')
|
ctx.execute('window.document = { };')
|
||||||
ctx.execute('window.Map = function(_i){ };')
|
ctx.execute('window.Map = function(_i){ };')
|
||||||
|
|
|
@ -15,6 +15,15 @@ class Lists(enum.Enum):
|
||||||
|
|
||||||
class PlayersList:
|
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
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, lst:Union[str,Lists], atserv:'AternosServer') -> None:
|
def __init__(self, lst:Union[str,Lists], atserv:'AternosServer') -> None:
|
||||||
|
|
||||||
self.atserv = atserv
|
self.atserv = atserv
|
||||||
|
@ -24,6 +33,14 @@ class PlayersList:
|
||||||
|
|
||||||
def list_players(self, cache:bool=True) -> List[str]:
|
def list_players(self, cache:bool=True) -> List[str]:
|
||||||
|
|
||||||
|
"""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]
|
||||||
|
"""
|
||||||
|
|
||||||
if cache and self.parsed:
|
if cache and self.parsed:
|
||||||
return self.players
|
return self.players
|
||||||
|
|
||||||
|
@ -47,6 +64,12 @@ class PlayersList:
|
||||||
|
|
||||||
def add(self, name:str) -> None:
|
def add(self, name:str) -> None:
|
||||||
|
|
||||||
|
"""Appends a player to the list by the nickname
|
||||||
|
|
||||||
|
:param name: Player's nickname
|
||||||
|
:type name: str
|
||||||
|
"""
|
||||||
|
|
||||||
self.atserv.atserver_request(
|
self.atserv.atserver_request(
|
||||||
'https://aternos.org/panel/ajax/players/add.php',
|
'https://aternos.org/panel/ajax/players/add.php',
|
||||||
'POST', data={
|
'POST', data={
|
||||||
|
@ -58,6 +81,12 @@ class PlayersList:
|
||||||
self.players.append(name)
|
self.players.append(name)
|
||||||
|
|
||||||
def remove(self, name:str) -> None:
|
def remove(self, name:str) -> None:
|
||||||
|
|
||||||
|
"""Removes a player from the list by the nickname
|
||||||
|
|
||||||
|
:param name: Player's nickname
|
||||||
|
:type name: str
|
||||||
|
"""
|
||||||
|
|
||||||
self.atserv.atserver_request(
|
self.atserv.atserver_request(
|
||||||
'https://aternos.org/panel/ajax/players/remove.php',
|
'https://aternos.org/panel/ajax/players/remove.php',
|
||||||
|
|
|
@ -208,7 +208,7 @@ class AternosServer:
|
||||||
def config(self) -> AternosConfig:
|
def config(self) -> AternosConfig:
|
||||||
|
|
||||||
"""Returns :class:`python_aternos.atconf.AternosConfig`
|
"""Returns :class:`python_aternos.atconf.AternosConfig`
|
||||||
instance for changing server settings
|
instance for editing server settings
|
||||||
|
|
||||||
:return: :class:`python_aternos.atconf.AternosConfig` object
|
:return: :class:`python_aternos.atconf.AternosConfig` object
|
||||||
:rtype: python_aternos.atconf.AternosConfig
|
:rtype: python_aternos.atconf.AternosConfig
|
||||||
|
@ -219,7 +219,7 @@ class AternosServer:
|
||||||
def players(self, lst:Lists) -> PlayersList:
|
def players(self, lst:Lists) -> PlayersList:
|
||||||
|
|
||||||
"""Returns :class:`python_aternos.atplayers.PlayersList`
|
"""Returns :class:`python_aternos.atplayers.PlayersList`
|
||||||
instance for managing operators, whitelist or banned players list
|
instance for managing operators, whitelist and banned players lists
|
||||||
|
|
||||||
:param lst: Players list type, must be
|
:param lst: Players list type, must be
|
||||||
the :class:`python_aternos.atplayers.Lists` enum value
|
the :class:`python_aternos.atplayers.Lists` enum value
|
||||||
|
@ -231,7 +231,7 @@ class AternosServer:
|
||||||
return PlayersList(lst, self)
|
return PlayersList(lst, self)
|
||||||
|
|
||||||
def atserver_request(
|
def atserver_request(
|
||||||
self, url:str, method:int,
|
self, url:str, method:str,
|
||||||
params:Optional[dict]=None,
|
params:Optional[dict]=None,
|
||||||
data:Optional[dict]=None,
|
data:Optional[dict]=None,
|
||||||
headers:Optional[dict]=None,
|
headers:Optional[dict]=None,
|
||||||
|
@ -244,15 +244,16 @@ class AternosServer:
|
||||||
:type url: str
|
:type url: str
|
||||||
:param method: Request method, must be GET or POST
|
:param method: Request method, must be GET or POST
|
||||||
:type method: str
|
:type method: str
|
||||||
:param params: URL parameters, defaults to an empty dictionary
|
:param params: URL parameters, defaults to None
|
||||||
:type params: dict, optional
|
:type params: Optional[dict], optional
|
||||||
:param data: POST request data. If the method is set to GET,
|
:param data: POST request data, if the method is GET,
|
||||||
it will be combined with params. Defaults to an empty dictionary
|
this dict will be combined with params, defaults to None
|
||||||
:type data: dict, optional
|
:type data: Optional[dict], optional
|
||||||
:param headers: Custom headers, defaults to an empty dictionary
|
:param headers: Custom headers, defaults to None
|
||||||
:type headers: dict, optional
|
:type headers: Optional[dict], optional
|
||||||
:param sendtoken: Send ajax token in params
|
:param sendtoken: If the ajax and SEC token
|
||||||
:type sendtoken: bool
|
should be sent, defaults to False
|
||||||
|
:type sendtoken: bool, optional
|
||||||
:return: API response
|
:return: API response
|
||||||
:rtype: requests.Response
|
:rtype: requests.Response
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -12,6 +12,8 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
class Streams(enum.Enum):
|
class Streams(enum.Enum):
|
||||||
|
|
||||||
|
"""WebSocket streams types"""
|
||||||
|
|
||||||
status = (0,None)
|
status = (0,None)
|
||||||
queue = (1,None)
|
queue = (1,None)
|
||||||
console = (2,'console')
|
console = (2,'console')
|
||||||
|
@ -24,6 +26,15 @@ class Streams(enum.Enum):
|
||||||
|
|
||||||
class AternosWss:
|
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
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, atserv:'AternosServer', autoconfirm:bool=False) -> None:
|
def __init__(self, atserv:'AternosServer', autoconfirm:bool=False) -> None:
|
||||||
|
|
||||||
self.atserv = atserv
|
self.atserv = atserv
|
||||||
|
@ -36,14 +47,31 @@ class AternosWss:
|
||||||
|
|
||||||
async def confirm(self) -> None:
|
async def confirm(self) -> None:
|
||||||
|
|
||||||
|
"""Simple way to call AternosServer.confirm from this class"""
|
||||||
|
|
||||||
self.atserv.confirm()
|
self.atserv.confirm()
|
||||||
|
|
||||||
def wssreceiver(self, stream:Streams, *args:Any) -> Callable[[Callable[[Any],Coroutine[Any,Any,None]]],Any]:
|
def wssreceiver(self, stream:Streams, *args:Any) -> Callable[[Callable[[Any],Coroutine[Any,Any,None]]],Any]:
|
||||||
|
|
||||||
|
"""Decorator that marks your function as a stream receiver.
|
||||||
|
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]
|
||||||
|
"""
|
||||||
|
|
||||||
def decorator(func:Callable[[Any],Coroutine[Any,Any,None]]) -> None:
|
def decorator(func:Callable[[Any],Coroutine[Any,Any,None]]) -> None:
|
||||||
self.recv[stream] = (func, args)
|
self.recv[stream] = (func, args)
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
async def connect(self) -> None:
|
async def connect(self) -> None:
|
||||||
|
|
||||||
|
"""Connect to the websocket server and start all stream listeners"""
|
||||||
|
|
||||||
headers = [
|
headers = [
|
||||||
('Host', 'aternos.org'),
|
('Host', 'aternos.org'),
|
||||||
|
@ -62,7 +90,9 @@ class AternosWss:
|
||||||
|
|
||||||
@self.wssreceiver(Streams.status)
|
@self.wssreceiver(Streams.status)
|
||||||
async def confirmfunc(msg):
|
async def confirmfunc(msg):
|
||||||
# Autoconfirm
|
|
||||||
|
"""Automatically confirm Minecraft server launching"""
|
||||||
|
|
||||||
if not self.autoconfirm:
|
if not self.autoconfirm:
|
||||||
return
|
return
|
||||||
if msg['class'] == 'queueing' \
|
if msg['class'] == 'queueing' \
|
||||||
|
@ -72,6 +102,22 @@ class AternosWss:
|
||||||
|
|
||||||
@self.wssreceiver(Streams.status)
|
@self.wssreceiver(Streams.status)
|
||||||
async def streamsfunc(msg):
|
async def streamsfunc(msg):
|
||||||
|
|
||||||
|
"""Automatically starts streams. Detailed description:
|
||||||
|
|
||||||
|
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 to the websocket server:
|
||||||
|
`{"stream":"console","type":"start"}`
|
||||||
|
on which the server responses with: `{"type":"connected"}`
|
||||||
|
Also, there are RAM (used heap) and TPS (ticks per second)
|
||||||
|
streams that must be enabled before trying to get information.
|
||||||
|
Enabling the stream for listening the server status is not needed,
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
|
||||||
if msg['status'] == 2:
|
if msg['status'] == 2:
|
||||||
# Automatically start streams
|
# Automatically start streams
|
||||||
for strm in self.recv:
|
for strm in self.recv:
|
||||||
|
@ -87,6 +133,8 @@ class AternosWss:
|
||||||
await self.wssworker()
|
await self.wssworker()
|
||||||
|
|
||||||
async def close(self) -> None:
|
async def close(self) -> None:
|
||||||
|
|
||||||
|
"""Closes websocket connection and stops all listeners"""
|
||||||
|
|
||||||
self.keep.cancel()
|
self.keep.cancel()
|
||||||
self.msgs.cancel()
|
self.msgs.cancel()
|
||||||
|
@ -95,6 +143,12 @@ class AternosWss:
|
||||||
|
|
||||||
async def send(self, obj:Union[Dict[str, Any],str]) -> None:
|
async def send(self, obj:Union[Dict[str, Any],str]) -> None:
|
||||||
|
|
||||||
|
"""Sends a message to websocket server
|
||||||
|
|
||||||
|
:param obj: Message, may be a string or a dict
|
||||||
|
:type obj: Union[Dict[str, Any],str]
|
||||||
|
"""
|
||||||
|
|
||||||
if isinstance(obj, dict):
|
if isinstance(obj, dict):
|
||||||
obj = json.dumps(obj)
|
obj = json.dumps(obj)
|
||||||
|
|
||||||
|
@ -102,11 +156,17 @@ class AternosWss:
|
||||||
|
|
||||||
async def wssworker(self) -> None:
|
async def wssworker(self) -> None:
|
||||||
|
|
||||||
|
"""Starts async tasks in background
|
||||||
|
for receiving websocket messages
|
||||||
|
and sending keepalive ping"""
|
||||||
|
|
||||||
self.keep = asyncio.create_task(self.keepalive())
|
self.keep = asyncio.create_task(self.keepalive())
|
||||||
self.msgs = asyncio.create_task(self.receiver())
|
self.msgs = asyncio.create_task(self.receiver())
|
||||||
|
|
||||||
async def keepalive(self) -> None:
|
async def keepalive(self) -> None:
|
||||||
|
|
||||||
|
"""Each 49 seconds sends keepalive ping to websocket server"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(49)
|
await asyncio.sleep(49)
|
||||||
|
@ -117,6 +177,9 @@ class AternosWss:
|
||||||
|
|
||||||
async def receiver(self) -> None:
|
async def receiver(self) -> None:
|
||||||
|
|
||||||
|
"""Receives messages from websocket servers
|
||||||
|
and calls user's streams listeners"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
data = await self.socket.recv()
|
data = await self.socket.recv()
|
||||||
|
|
Reference in a new issue