Account actions, session, examples and readme
This commit is contained in:
parent
75b10e6e8c
commit
03ca4098bc
8 changed files with 155 additions and 62 deletions
57
README.md
57
README.md
|
@ -1,42 +1,37 @@
|
||||||
# Python Aternos API
|
# Python Aternos API
|
||||||
An unofficial Aternos API written in Python.
|
An unofficial Aternos API written in Python.
|
||||||
It uses requests, cloudscraper and lxml to parse data from [aternos.org](https://aternos.org/).
|
It uses [aternos](https://aternos.org/)' private API and html parsing.
|
||||||
> Note for vim: if you have a problem like `IndentationError: unindent does not match any outer indentation level`, try out `retab`.
|
|
||||||
|
|
||||||
## Using
|
## Installation
|
||||||
First you need to install the module:
|
|
||||||
```bash
|
```bash
|
||||||
pip install python-aternos
|
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
|
## Usage
|
||||||
login with your username and password (or MD5 hash of password).
|
To use Aternos API in your Python script, import it
|
||||||
> Note: Logging in with Google or Facebook account is not supported yet.
|
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()`.
|
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
|
```python
|
||||||
# Import
|
# Import
|
||||||
from python_aternos import Client
|
from python_aternos import Client
|
||||||
|
|
||||||
# Log in
|
# Log in
|
||||||
#aternos = Client('USERNAME', password='PASSWORD')
|
aternos = Client.from_credentials('example', 'test123')
|
||||||
aternos = Client('example', password='test123')
|
|
||||||
# ----OR----
|
# ----OR----
|
||||||
# password is the 1st parameter,
|
aternos = Client.from_hashed('example', 'cc03e747a6afbbcbf8be7668acfebee5')
|
||||||
# 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')
|
|
||||||
|
|
||||||
# Returns AternosServer list
|
# Returns AternosServer list
|
||||||
atservers = aternos.servers
|
servs = aternos.list_servers()
|
||||||
|
|
||||||
# If you have only one server, get it by the 0 index
|
# Get the first server by the 0 index
|
||||||
myserv = atservers[0]
|
myserv = servs[0]
|
||||||
|
|
||||||
# Start
|
# Start
|
||||||
myserv.start()
|
myserv.start()
|
||||||
|
@ -45,7 +40,7 @@ myserv.stop()
|
||||||
|
|
||||||
# You can also find server by IP
|
# You can also find server by IP
|
||||||
testserv = None
|
testserv = None
|
||||||
for serv in atservers:
|
for serv in servs:
|
||||||
if serv.address == 'test.aternos.org':
|
if serv.address == 'test.aternos.org':
|
||||||
testserv = serv
|
testserv = serv
|
||||||
if testserv != None:
|
if testserv != None:
|
||||||
|
@ -55,21 +50,11 @@ if testserv != None:
|
||||||
# Starts server
|
# Starts server
|
||||||
testserv.start()
|
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
|
## Changelog
|
||||||
<!--
|
|
||||||
* v0.1 - the first release.
|
|
||||||
* v0.2 - fixed import problem.
|
|
||||||
* 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 - implementation of Google Drive backups API is planned.
|
|
||||||
* v0.7 - full implementation of config API is planned.
|
|
||||||
* v0.8 - shared access API and permission management is planned.
|
|
||||||
* v0.9.x - a long debugging before stable release, SemVer version code.
|
|
||||||
-->
|
|
||||||
|Version|Description|
|
|Version|Description|
|
||||||
|:-----:|:-----------|
|
|:-----:|:-----------|
|
||||||
|v0.1|The first release.|
|
|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.3|Implemented files API, added typization.|
|
||||||
|v0.4|Implemented configuration API, some bugfixes.|
|
|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.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.7|Full implementation of config API and Google Drive backups is planned.|
|
||||||
|v0.8|Shared access API and permission management is planned.|
|
|v0.8|Shared access API and permission management is planned.|
|
||||||
|v0.9.x|A long debugging before stable release, SemVer version code.|
|
|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
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
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.
|
||||||
|
|
38
examples/files_example.py
Normal file
38
examples/files_example.py
Normal file
|
@ -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())
|
18
examples/info_example.py
Normal file
18
examples/info_example.py
Normal file
|
@ -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)
|
|
@ -5,7 +5,7 @@ user = input('Username: ')
|
||||||
pswd = getpass('Password: ')
|
pswd = getpass('Password: ')
|
||||||
aternos = Client.from_credentials(user, pswd)
|
aternos = Client.from_credentials(user, pswd)
|
||||||
|
|
||||||
srvs = aternos.servers
|
srvs = aternos.list_servers()
|
||||||
print(srvs)
|
print(srvs)
|
||||||
|
|
||||||
s = srvs[0]
|
s = srvs[0]
|
||||||
|
|
15
examples/websocket_example.py
Normal file
15
examples/websocket_example.py
Normal file
|
@ -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()
|
|
@ -1,3 +1,5 @@
|
||||||
|
import os
|
||||||
|
import re
|
||||||
import hashlib
|
import hashlib
|
||||||
import lxml.html
|
import lxml.html
|
||||||
from typing import List
|
from typing import List
|
||||||
|
@ -28,11 +30,10 @@ class Client:
|
||||||
|
|
||||||
loginreq = atconn.request_cloudflare(
|
loginreq = atconn.request_cloudflare(
|
||||||
f'https://aternos.org/panel/ajax/account/login.php',
|
f'https://aternos.org/panel/ajax/account/login.php',
|
||||||
'POST', data=credentials,
|
'POST', data=credentials, sendtoken=True
|
||||||
sendtoken=True
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if loginreq.cookies.get('ATERNOS_SESSION', None) == None:
|
if 'ATERNOS_SESSION' not in loginreq.cookies:
|
||||||
raise CredentialsError(
|
raise CredentialsError(
|
||||||
'Check your username and password'
|
'Check your username and password'
|
||||||
)
|
)
|
||||||
|
@ -42,9 +43,7 @@ class Client:
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_credentials(cls, username:str, password:str):
|
def from_credentials(cls, username:str, password:str):
|
||||||
|
|
||||||
pswd_bytes = password.encode('utf-8')
|
md5 = Client.md5encode(password)
|
||||||
md5 = hashlib.md5(pswd_bytes).hexdigest().lower()
|
|
||||||
|
|
||||||
return cls.from_hashed(username, md5)
|
return cls.from_hashed(username, md5)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -56,18 +55,29 @@ class Client:
|
||||||
atconn.generate_sec()
|
atconn.generate_sec()
|
||||||
|
|
||||||
return cls(atconn)
|
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
|
@staticmethod
|
||||||
def google() -> str:
|
def md5encode(passwd:str) -> str:
|
||||||
|
|
||||||
atconn = AternosConnect()
|
encoded = hashlib.md5(passwd.encode('utf-8'))
|
||||||
auth = atconn.request_cloudflare(
|
return encoded.hexdigest().lower()
|
||||||
'https://aternos.org/auth/google-login',
|
|
||||||
'GET', redirect=False
|
def save_session(self, file:str='~/.aternos') -> None:
|
||||||
)
|
|
||||||
return auth.headers['Location']
|
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(
|
serverspage = self.atconn.request_cloudflare(
|
||||||
'https://aternos.org/servers/', 'GET'
|
'https://aternos.org/servers/', 'GET'
|
||||||
)
|
)
|
||||||
|
@ -80,3 +90,37 @@ class Client:
|
||||||
servers.append(AternosServer(servid, self.atconn))
|
servers.append(AternosServer(servid, self.atconn))
|
||||||
|
|
||||||
return servers
|
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
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
|
@ -138,7 +138,4 @@ class AternosConnect:
|
||||||
f'{method} completed with {req.status_code} status'
|
f'{method} completed with {req.status_code} status'
|
||||||
)
|
)
|
||||||
|
|
||||||
with open('debug.html', 'wb') as f:
|
|
||||||
f.write(req.content)
|
|
||||||
|
|
||||||
return req
|
return req
|
||||||
|
|
|
@ -59,16 +59,12 @@ class AternosFile:
|
||||||
def get_text(self) -> str:
|
def get_text(self) -> str:
|
||||||
|
|
||||||
editor = self.atserv.atserver_request(
|
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)
|
edittree = lxml.html.fromstring(editor.content)
|
||||||
|
|
||||||
editlines = edittree.xpath('//div[@class="ace_line"]')
|
editblock = edittree.xpath('//div[@id="editor"]')[0]
|
||||||
rawlines = []
|
return editblock.text_content()
|
||||||
|
|
||||||
for line in editlines:
|
|
||||||
rawlines.append(line.text)
|
|
||||||
return rawlines
|
|
||||||
|
|
||||||
def set_text(self, value:str) -> None:
|
def set_text(self, value:str) -> None:
|
||||||
|
|
||||||
|
|
Reference in a new issue