diff --git a/README.md b/README.md index b0c7305..b2105e4 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,37 @@ # Python Aternos API An unofficial Aternos API written in Python. -It uses requests, cloudscraper and lxml to parse data from [aternos.org](https://aternos.org/). -> Note for vim: if you have a problem like `IndentationError: unindent does not match any outer indentation level`, try out `retab`. +It uses [aternos](https://aternos.org/)' private API and html parsing. -## Using -First you need to install the module: +## Installation ```bash pip install python-aternos ``` +> Note for Windows users: +Install `lxml` package from [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#lxml) if you have a problem with it, +and then execute `pip install --no-deps python-aternos` -To use Aternos API in your Python script, import it and -login with your username and password (or MD5 hash of password). -> Note: Logging in with Google or Facebook account is not supported yet. +## Usage +To use Aternos API in your Python script, import it +and login with your username and password/MD5. -Then get the servers list using the `servers` field. +Then request the servers list using `list_servers()`. You can start/stop your Aternos server now, calling `start()` or `stop()`. -Here is an example how to use the Aternos API: +Here is an example how to use the API: ```python # Import from python_aternos import Client # Log in -#aternos = Client('USERNAME', password='PASSWORD') -aternos = Client('example', password='test123') +aternos = Client.from_credentials('example', 'test123') # ----OR---- -# password is the 1st parameter, -# so you don't have to specify its name -aternos = Client('example', 'test123') -# ----OR---- -#aternos = Client('USERNAME', md5='HASHED_PASSWORD') -aternos = Client('example', md5='cc03e747a6afbbcbf8be7668acfebee5') +aternos = Client.from_hashed('example', 'cc03e747a6afbbcbf8be7668acfebee5') # Returns AternosServer list -atservers = aternos.servers +servs = aternos.list_servers() -# If you have only one server, get it by the 0 index -myserv = atservers[0] +# Get the first server by the 0 index +myserv = servs[0] # Start myserv.start() @@ -45,7 +40,7 @@ myserv.stop() # You can also find server by IP testserv = None -for serv in atservers: +for serv in servs: if serv.address == 'test.aternos.org': testserv = serv if testserv != None: @@ -55,21 +50,11 @@ if testserv != None: # Starts server testserv.start() ``` -You can find full documentation on the [Project Wiki](https://github.com/DarkCat09/python-aternos/wiki). +~~You can find full documentation on the [Project Wiki](https://github.com/DarkCat09/python-aternos/wiki).~~ + +## [More examples](/examples) ## Changelog - |Version|Description| |:-----:|:-----------| |v0.1|The first release.| @@ -77,7 +62,7 @@ You can find full documentation on the [Project Wiki](https://github.com/DarkCat |v0.3|Implemented files API, added typization.| |v0.4|Implemented configuration API, some bugfixes.| |v0.5|The API was updated corresponding to new Aternos security methods. Huge thanks to [lusm554](https://github.com/lusm554).| -|v0.6|Preventing detecting automated access is planned.| +|v0.6|Code refactoring, unit-tests, websocket API and session saving to prevent detecting automation access.| |v0.7|Full implementation of config API and Google Drive backups is planned.| |v0.8|Shared access API and permission management is planned.| |v0.9.x|A long debugging before stable release, SemVer version code.| @@ -99,4 +84,4 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ``` -You **don't** need to attribute me, if you are just using this module installed from PIP. +You **don't** need to attribute me, if you are just using this module installed from PIP or wheel. diff --git a/examples/files_example.py b/examples/files_example.py new file mode 100644 index 0000000..121d175 --- /dev/null +++ b/examples/files_example.py @@ -0,0 +1,38 @@ +from getpass import getpass +from python_aternos import Client, atserver + +user = input('Username: ') +pswd = getpass('Password: ') +aternos = Client.from_credentials(user, pswd) + +s = aternos.list_servers()[0] +files = s.files() + +while True: + + cmd = input('> ').strip().lower() + + if cmd == 'help': + print( + '''Commands list: + help - show this message + quit - exit from the script + world - download the world + list [path] - show directory (or root) contents''' + ) + + if cmd == 'quit': + break + + if cmd.startswith('list'): + path = cmd.removeprefix('list').strip() + directory = files.listdir(path) + + print(path, 'contains:') + for file in directory: + print('\t' + file.name) + + if cmd == 'world': + file = files.get_file('/world') + with open('world.zip', 'wb') as f: + f.write(file.get_content()) diff --git a/examples/info_example.py b/examples/info_example.py new file mode 100644 index 0000000..c504979 --- /dev/null +++ b/examples/info_example.py @@ -0,0 +1,18 @@ +from getpass import getpass +from python_aternos import Client, atserver + +user = input('Username: ') +pswd = getpass('Password: ') +aternos = Client.from_credentials(user, pswd) + +srvs = aternos.list_servers() +for srv in srvs: + print('*** ' + srv.domain + ' ***') + print(srv.motd) + print('*** Status:', srv.status) + print('*** Full address:', srv.address) + print('*** Port:', srv.port) + print('*** Name:', srv.subdomain) + print('*** Minecraft:', srv.software, srv.version) + print('*** IsBedrock:', srv.edition == atserver.Edition.bedrock) + print('*** IsJava:', srv.edition == atserver.Edition.java) diff --git a/examples/start_example.py b/examples/start_example.py index b434acb..5718e84 100644 --- a/examples/start_example.py +++ b/examples/start_example.py @@ -5,7 +5,7 @@ user = input('Username: ') pswd = getpass('Password: ') aternos = Client.from_credentials(user, pswd) -srvs = aternos.servers +srvs = aternos.list_servers() print(srvs) s = srvs[0] diff --git a/examples/websocket_example.py b/examples/websocket_example.py new file mode 100644 index 0000000..ba3c5e5 --- /dev/null +++ b/examples/websocket_example.py @@ -0,0 +1,15 @@ +from getpass import getpass +from python_aternos import Client, atwss + +user = input('Username: ') +pswd = getpass('Password: ') +aternos = Client.from_credentials(user, pswd) + +s = aternos.list_servers()[0] +socket = s.wss() + +@socket.wssreceiver(atwss.Streams.console) +def console(msg): + print('Received: ' + msg) + +s.start() diff --git a/python_aternos/__init__.py b/python_aternos/__init__.py index 89760c5..e9877a4 100644 --- a/python_aternos/__init__.py +++ b/python_aternos/__init__.py @@ -1,3 +1,5 @@ +import os +import re import hashlib import lxml.html from typing import List @@ -28,11 +30,10 @@ class Client: loginreq = atconn.request_cloudflare( f'https://aternos.org/panel/ajax/account/login.php', - 'POST', data=credentials, - sendtoken=True + 'POST', data=credentials, sendtoken=True ) - if loginreq.cookies.get('ATERNOS_SESSION', None) == None: + if 'ATERNOS_SESSION' not in loginreq.cookies: raise CredentialsError( 'Check your username and password' ) @@ -42,9 +43,7 @@ class Client: @classmethod def from_credentials(cls, username:str, password:str): - pswd_bytes = password.encode('utf-8') - md5 = hashlib.md5(pswd_bytes).hexdigest().lower() - + md5 = Client.md5encode(password) return cls.from_hashed(username, md5) @classmethod @@ -56,18 +55,29 @@ class Client: atconn.generate_sec() return cls(atconn) + + @classmethod + def restore_session(cls, file:str='~/.aternos'): + file = os.path.expanduser(file) + with open(file, 'rt') as f: + session = f.read().strip() + return cls.from_session(session) + @staticmethod - def google() -> str: + def md5encode(passwd:str) -> str: - atconn = AternosConnect() - auth = atconn.request_cloudflare( - 'https://aternos.org/auth/google-login', - 'GET', redirect=False - ) - return auth.headers['Location'] + encoded = hashlib.md5(passwd.encode('utf-8')) + return encoded.hexdigest().lower() + + def save_session(self, file:str='~/.aternos') -> None: + + file = os.path.expanduser(file) + with open(file, 'wt') as f: + f.write(self.atconn.atsession) + + def list_servers(self) -> List[AternosServer]: - def list_servers(self) -> List[atserver.AternosServer]: serverspage = self.atconn.request_cloudflare( 'https://aternos.org/servers/', 'GET' ) @@ -80,3 +90,37 @@ class Client: servers.append(AternosServer(servid, self.atconn)) return servers + + def get_server(self, servid:str) -> AternosServer: + + return AternosServer(servid, self.atconn) + + def change_username(self, value:str) -> None: + + self.atconn.request_cloudflare( + 'https://aternos.org/panel/ajax/account/username.php', + 'POST', data={'username': value} + ) + + def change_email(self, value:str) -> None: + + email = re.compile(r'^[A-Za-z0-9\-_+.]+@[A-Za-z0-9\-_+.]+\.[A-Za-z0-9\-]+$|^$') + if not email.match(value): + raise ValueError('Invalid e-mail!') + + self.atconn.request_cloudflare( + 'https://aternos.org/panel/ajax/account/email.php', + 'POST', data={'email': value} + ) + + def change_password(self, old:str, new:str) -> None: + + old = Client.md5encode(old) + new = Client.md5encode(new) + self.atconn.request_cloudflare( + 'https://aternos.org/panel/ajax/account/password.php', + 'POST', data={ + 'oldpassword': old, + 'newpassword': new + } + ) diff --git a/python_aternos/atconnect.py b/python_aternos/atconnect.py index 8033984..2aa8ab1 100644 --- a/python_aternos/atconnect.py +++ b/python_aternos/atconnect.py @@ -138,7 +138,4 @@ class AternosConnect: f'{method} completed with {req.status_code} status' ) - with open('debug.html', 'wb') as f: - f.write(req.content) - return req diff --git a/python_aternos/atfile.py b/python_aternos/atfile.py index 10d9b92..792fc84 100644 --- a/python_aternos/atfile.py +++ b/python_aternos/atfile.py @@ -59,16 +59,12 @@ class AternosFile: def get_text(self) -> str: editor = self.atserv.atserver_request( - f'https://aternos.org/files/{self._full}', 'GET' + f'https://aternos.org/files/{self._full.lstrip("/")}', 'GET' ) edittree = lxml.html.fromstring(editor.content) - - editlines = edittree.xpath('//div[@class="ace_line"]') - rawlines = [] - - for line in editlines: - rawlines.append(line.text) - return rawlines + + editblock = edittree.xpath('//div[@id="editor"]')[0] + return editblock.text_content() def set_text(self, value:str) -> None: