From 61460b2f74ce7dceab59c889c3b04e013f44f79f Mon Sep 17 00:00:00 2001 From: Andrey <50486086+DarkCat09@users.noreply.github.com> Date: Fri, 15 Oct 2021 15:12:45 +0400 Subject: [PATCH] Server config feauture, players management, bugfix --- python_aternos/atconf.py | 219 ++++++++++++++++++++++++++++++++++++ python_aternos/aterrors.py | 4 + python_aternos/atfile.py | 44 +++++++- python_aternos/atfm.py | 11 +- python_aternos/atplayers.py | 50 ++++++++ python_aternos/atserver.py | 65 ++++++++++- 6 files changed, 383 insertions(+), 10 deletions(-) create mode 100644 python_aternos/atconf.py create mode 100644 python_aternos/atplayers.py diff --git a/python_aternos/atconf.py b/python_aternos/atconf.py new file mode 100644 index 0000000..1d87616 --- /dev/null +++ b/python_aternos/atconf.py @@ -0,0 +1,219 @@ +import re +import lxml.html +from typing import Any, Dict, List + +from . import atserver +from . import atconnect + +OPT_PLAYERS = 'max-players' +OPT_GAMEMODE = 'gamemode' +OPT_DIFFICULTY = 'difficulty' +OPT_WHITELIST = 'white-list' +OPT_ONLINE = 'online-mode' +OPT_PVP = 'pvp' +OPT_CMDBLOCK = 'enable-command-block' +OPT_FLIGHT = 'allow-flight' +OPT_ANIMALS = 'spawn-animals' +OPT_MONSTERS = 'spawn-monsters' +OPT_VILLAGERS = 'spawn-npcs' +OPT_NETHER = 'allow-nether' +OPT_FORCEGM = 'force-gamemode' +OPT_SPAWNLOCK = 'spawn-protection' +OPT_CHEATS = 'allow-cheats' +OPT_RESOURCEPACK = 'resource-pack' + +DAT_PREFIX = 'Data:' +DAT_SEED = 'RandomSeed' +DAT_HARDCORE = 'hardcore' +DAT_DIFFICULTY = 'Difficulty' + +DAT_GR_PREFIX = 'Data:GameRules:' +DAT_GR_ADVS = 'announceAdvancements' +DAT_GR_CMDOUT = 'commandBlockOutput' +DAT_GR_ELYTRA = 'disableElytraMovementCheck' +DAT_GR_DAYLIGHT = 'doDaylightCycle' +DAT_GR_ENTDROPS = 'doEntityDrops' +DAT_GR_FIRETICK = 'doFireTick' +DAT_GR_LIMITCRAFT = 'doLimitedCrafting' +DAT_GR_MOBLOOT = 'doMobLoot' +DAT_GR_MOBS = 'doMobSpawning' +DAT_GR_TILEDROPS = 'doTileDrops' +DAT_GR_WEATHER = 'doWeatherCycle' +DAT_GR_KEEPINV = 'keepInventory' +DAT_GR_DEATHMSG = 'showDeathMessages' +DAT_GR_ADMINCMDLOG = 'logAdminCommands' +DAT_GR_CMDLEN = 'maxCommandChainLength' +DAT_GR_ENTCRAM = 'maxEntityCramming' +DAT_GR_MOBGRIEF = 'mobGriefing' +DAT_GR_REGEN = 'naturalRegeneration' +DAT_GR_RNDTICK = 'randomTickSpeed' +DAT_GR_SPAWNRADIUS = 'spawnRadius' +DAT_GR_REDUCEDF3 = 'reducedDebugInfo' +DAT_GR_SPECTCHUNK = 'spectatorsGenerateChunks' +DAT_GR_CMDFB = 'sendCommandFeedback' + +DAT_TYPE_WORLD = 0 +DAT_TYPE_GR = 1 + +GM_SURVIVAL = 0 +GM_CREATIVE = 1 +GM_ADVENTURE = 2 +GM_SPECTATOR = 3 + +DF_PEACEFUL = 0 +DF_EASY = 1 +DF_NORMAL = 2 +DF_HARD = 3 + +JAVA_JDK = 'openjdk:{}' +JAVA_OPENJ9 = 'adoptopenjdk:{}-jre-openj9-bionic' + +FLAG_PROP_TYPE = 1 + +class AternosConfig: + + def __init__(self, atserv:atserver.AternosServer) -> None: + + self.atserv = atserv + + @property + def timezone(self) -> str: + optreq = self.atserv.atserver_request( + 'https://aternos.org/options', + atconnect.REQGET + ) + opttree = lxml.html.fromstring(optreq) + + tzopt = opttree.xpath('//div[@class="options-other-input timezone-switch"]')[0] + tztext = tzopt.xpath('.//div[@class="option current"]')[0].text + return tztext.strip() + + @timezone.setter + def timezone(self, value:str) -> None: + matches_tz = re.search(r'(?:^[A-Z]\w+\/[A-Z]\w+$)|^UTC$', value) + if matches_tz == None: + raise AttributeError('Timezone must match zoneinfo format: Area/Location') + + self.atserv.atserver_request( + 'https://aternos.org/panel/ajax/timezone.php', + atconnect.REQPOST, data={'timezone': value}, + sendtoken=True + ) + + @property + def java_version(self) -> str: + optreq = self.atserv.atserver_request( + 'https://aternos.org/options', + atconnect.REQGET + ) + opttree = lxml.html.fromstring(optreq) + + imgopt = opttree.xpath('//div[@class="options-other-input image-switch"]')[0] + imgver = imgopt.xpath('.//div[@class="option current"]/@data-value')[0] + return imgver + + @java_version.setter + def java_version(self, value:str) -> None: + matches_jdkver = re.search(r'^(?:adopt)*openjdk:(\d+)(?:-jre-openj9-bionic)*$', value) + if matches_jdkver == None: + raise AttributeError('Java image version must match "[adopt]openjdk:%d[-jre-openj9-bionic]" format') + + self.atserv.atserver_request( + 'https://aternos.org/panel/ajax/image.php', + atconnect.REQPOST, data={'image': value}, + sendtoken=True + ) + + # + # server.properties + # + def set_server_prop(self, option:str, value:Any) -> None: + self.__set_prop( + '/server.properties', + option, value + ) + + def get_server_props(self, flags:int=FLAG_PROP_TYPE) -> Dict[str,Any]: + return self.__get_all_props('https://aternos.org/options', flags) + + def set_server_props(self, props:Dict[str,Any]) -> None: + for key in props: + set_server_prop(key, props[key]) + + # + # level.dat + # + def set_world_prop( + self, option:str, value:Any, + proptype:int, world:str='world') -> None: + prefix = DAT_PREFIX + if proptype == DAT_TYPE_GR: + prefix = DAT_GR_PREFIX + + self.__set_prop( + f'/{world}/level.dat', + f'{prefix}{option}', + value + ) + + def get_world_props( + self, world:str='world', + flags:int=FLAG_PROP_TYPE) -> Dict[str,Any]: + self.__get_all_props( + f'https://aternos.org/files/{world}/level.dat', + flags, [DAT_PREFIX, DAT_GR_PREFIX] + ) + + def set_world_props(self, props:Dict[str,Any]) -> None: + for key in props: + set_world_prop(key, prop[key]) + + # + # helpers + # + def __set_prop(self, file:str, option:str, value:Any) -> None: + + self.atserv.atserver_request( + 'https://aternos.org/panel/ajax/config.php', + atconnect.REQPOST, data={ + 'file': file, + 'option': option, + 'value': value + }, sendtoken=True + ) + + def __get_all_props( + self, url:str, flags:int=FLAG_PROP_TYPE, + prefixes:Optional[List[str]]=None) -> Dict[str,Any]: + + optreq = self.atserv.atserver_request( + 'https://aternos.org/options', + atconnect.REQGET + ) + opttree = lxml.html.fromstring(optreq.content) + + configs = opttree.xpath('//div[@class="config-options"]') + for i in range(len(configs)): + + conf = configs[i] + opts = conf.xpath('/div[contains(@class,"config-option ")]') + result = {} + + for opt in opts: + + key = opt.xpath('.//span[@class="config-option-output-key"]')[0].text + value = opt.xpath('.//span[@class="config-option-output-value"]')[0].text + if prefixes != None: + key = f'{prefixes[i]}{key}' + + opttype = opt.xpath('/@class').split(' ')[1] + if flags == FLAG_PROP_TYPE: + + if opttype == 'config-option-number'\ + or opttype == 'config-option-select': + value = int(value) + + elif opttype == 'config-option-toggle': + value = bool(value) + result[key] = value + return result diff --git a/python_aternos/aterrors.py b/python_aternos/aterrors.py index fc91984..224e884 100644 --- a/python_aternos/aterrors.py +++ b/python_aternos/aterrors.py @@ -9,3 +9,7 @@ class AternosCredentialsError(AternosError): class AternosServerStartError(AternosError): pass + +class AternosIOError(AternosError): + + pass diff --git a/python_aternos/atfile.py b/python_aternos/atfile.py index 4cc1818..a3cb782 100644 --- a/python_aternos/atfile.py +++ b/python_aternos/atfile.py @@ -12,12 +12,14 @@ class AternosFile: def __init__( atserv:atserver.AternosServer, path:str, name:str, ftype:int=FTYPE_FILE, - size:Union[]=0) -> None: + size:Union[int,float]=0, dlallowed:bool=False) -> None: self.atserv = atserv + self._path = path self._name = name self._ftype = ftype self._size = float(size) + self._dlallowed = dlallowed def delete(self) -> None: @@ -27,6 +29,24 @@ class AternosFile: sendtoken=True ) + @property + def content(self) -> bytes: + file = self.atserv.atserver_request( + f'https://aternos.org/panel/ajax/files/download.php', + atconnect.REQGET, params={'file': self.path.replace('/','%2F')} + ) + if not self._dlallowed: + raise AternosIOError('Downloading this file is not allowed. Try to get text') + return file.content + + @content.setter + def content(self, value:bytes) -> None: + self.atserv.atserver_request( + f'https://aternos.org/panel/ajax/save.php', + atconnect.REQPOST, data={'content': value}, + sendtoken=True + ) + @property def text(self) -> str: editor = self.atserv.atserver_request( @@ -44,21 +64,37 @@ class AternosFile: return rawlines @text.setter - def text(self, value:Union[str,bytes]) -> None: + def text(self, value:str) -> None: self.atserv.atserver_request( f'https://aternos.org/panel/ajax/save.php', atconnect.REQPOST, data={'content': value}, sendtoken=True ) + @property + def path(self): + return self._path + @property def name(self) -> str: return self._name @property - def ftype(self) -> int: - return self._ftype + def is_dir(self) -> bool: + if self._ftype == FTYPE_DIR: + return True + return False + + @property + def is_file(self) -> bool: + if self._ftype == FTYPE_FILE: + return True + return False @property def size(self) -> float: return self._size + + @property + def dlallowed(self): + return self._dlallowed diff --git a/python_aternos/atfm.py b/python_aternos/atfm.py index 27ec9f7..5083e3e 100644 --- a/python_aternos/atfm.py +++ b/python_aternos/atfm.py @@ -44,6 +44,11 @@ class AternosFileManager: except ValueError: fsize = -1 + dlbutton = f.xpath('/div[@class="js-download-file btn btn-main btn-small btn-notext btn-no-margin"]') + dlallowed = False + if len(dlbutton) > 0: + dlallowed = True + fullpath = f.xpath('/@data-path')[0] filepath = fullpath[:fullpath.rfind('/')] filename = fullpath[fullpath.rfind('/'):] @@ -51,7 +56,7 @@ class AternosFileManager: atfile.AternosFile( self.atserv, filepath, filename, - ftype, fsize + ftype, fsize, dlallowed ) ) @@ -87,7 +92,7 @@ class AternosFileManager: file = self.atserv.atserver_request( f'https://aternos.org/panel/ajax/files/download.php?' + \ - f'file={path.replace('/','%2F')}', + f'file={path.replace("/","%2F")}', atconnect.REQGET ) @@ -97,7 +102,7 @@ class AternosFileManager: world = self.atserv.atserver_request( f'https://aternos.org/panel/ajax/worlds/download.php?' + \ - f'world={world.replace('/','%2F')}', + f'world={world.replace("/","%2F")}', atconnect.REQGET ) diff --git a/python_aternos/atplayers.py b/python_aternos/atplayers.py new file mode 100644 index 0000000..9a7563c --- /dev/null +++ b/python_aternos/atplayers.py @@ -0,0 +1,50 @@ +import lxml.html +from typing import List + +from . import atserver +from . import atconnect + +class AternosPlayersList: + + def __init__(self, lst:str, atserv:atserver.AternosServer) -> None: + + self.atserv = atserv + self.lst = lst + + def add(self, name:str) -> None: + + self.atserv.atserver_request( + 'https://aternos.org/panel/ajax/players/add.php', + atconnect.REQPOST, data={ + 'list': self.lst, + 'name': name + } + ) + + def remove(self, name:str) -> None: + + self.atserv.atserver_request( + 'https://aternos.org/panel/ajax/players/remove.php', + atconnect.REQPOST, data={ + 'list': self.lst, + 'name': name + } + ) + + @property + def players(self) -> List[str]: + listreq = atserv.atserver_request( + f'https://aternos.org/players/{lst}', + atconnect.REQGET + ) + listtree = lxml.html.fromstring(listreq.content) + + items = listtree.xpath( + '//div[@class="player-list"]' + \ + '/div[@class="list-item-container"]' + \ + '/div[@class="list-item"]' + ) + result = [] + for i in items: + name = i.xpath('./div[@class="list-name"]') + result.append(name) diff --git a/python_aternos/atserver.py b/python_aternos/atserver.py index 99628c6..07640bf 100644 --- a/python_aternos/atserver.py +++ b/python_aternos/atserver.py @@ -7,6 +7,22 @@ from typing import Optional, Dict from . import atconnect from . import aterrors from . import atfm +from . import atconf +from . import atplayers + +SOFTWARE_JAVA = 0 +SOFTWARE_BEDROCK = 1 + +PLAYERS_ALLOWED = 'whitelist' +PLAYERS_OPS = 'ops' +PLAYERS_BANNED = 'banned-players' +PLAYERS_IPS = 'banned-ips' + +STATUS_OFFLINE = 0 +STATUS_ONLINE = 1 +STATUS_LOADING = 2 +STATUS_SHUTDOWN = 3 +STATUS_ERROR = 7 class AternosServer: @@ -98,6 +114,14 @@ class AternosServer: return atfm.AternosFileManager(self) + def config(self) -> atconf.AternosConfig: + + return atconf.AternosConfig(self) + + def players(self, lst:str) -> atplayers.AternosPlayersList: + + return atplayers.AternosPlayersList(lst, self) + def atserver_request( self, url:str, method:int, params:Optional[dict]=None, @@ -116,8 +140,27 @@ class AternosServer: ) @property - def info(self) -> dict: - return self._info + def subdomain(self) -> str: + atdomain = self.domain + return atdomain[:atdomain.find('.')] + + @subdomain.setter + def subdomain(self, value:str) -> None: + self.atserver_request( + 'https://aternos.org/panel/ajax/options/subdomain.php', + atconnect.REQGET, params={'subdomain': value} + ) + + @property + def motd(self) -> str: + return self._info['motd'] + + @motd.setter + def motd(self, value:str) -> None: + self.atserver_request( + 'https://aternos.org/panel/ajax/options/motd.php', + atconnect.REQPOST, data={'motd': value} + ) @property def address(self) -> str: @@ -125,12 +168,20 @@ class AternosServer: @property def domain(self) -> str: - return self._info['displayAddress'] + return self._info['ip'] @property def port(self) -> int: return self._info['port'] + @property + def edition(self) -> int: + soft_type = self._info['bedrock'] + if soft_type == True: + return SOFTWARE_BEDROCK + else: + return SOFTWARE_JAVA + @property def software(self) -> str: return self._info['software'] @@ -138,3 +189,11 @@ class AternosServer: @property def version(self) -> str: return self._info['version'] + + @property + def status(self) -> int: + return int(self._info['status']) + + @property + def ram(self) -> int: + return int(self._info['ram'])