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
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -72,6 +72,9 @@ instance/
|
||||||
# Sphinx documentation
|
# Sphinx documentation
|
||||||
docs/_build/
|
docs/_build/
|
||||||
|
|
||||||
|
# MkDocs
|
||||||
|
site/
|
||||||
|
|
||||||
# PyBuilder
|
# PyBuilder
|
||||||
.pybuilder/
|
.pybuilder/
|
||||||
target/
|
target/
|
||||||
|
|
7
Makefile
7
Makefile
|
@ -5,8 +5,11 @@ upload:
|
||||||
python -m twine upload dist/*
|
python -m twine upload dist/*
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf dist/ python_aternos.egg-info/
|
rm -rf dist python_aternos.egg-info
|
||||||
rm -rf .mypy_cache/ python_aternos/__pycache__/
|
rm -rf python_aternos/__pycache__
|
||||||
|
rm -rf examples/__pycache__
|
||||||
|
rm -rf tests/__pycache__
|
||||||
|
rm -rf site .mypy_cache
|
||||||
|
|
||||||
check:
|
check:
|
||||||
chmod +x test.sh
|
chmod +x test.sh
|
||||||
|
|
2
NOTICE
2
NOTICE
|
@ -1,4 +1,4 @@
|
||||||
Copyright 2021-2022 Chechkenev Andrey, lusm554, ghrlt, NotNalin
|
Copyright 2021-2022 All contributors
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
98
README.md
98
README.md
|
@ -1,23 +1,77 @@
|
||||||
![Python-Aternos Logo](https://i.ibb.co/3RXcXJ1/aternos-400.png)
|
<div align="center">
|
||||||
***
|
<img src="https://i.ibb.co/3RXcXJ1/aternos-400.png" alt="Python Aternos Logo">
|
||||||
# Python Aternos
|
<h1>
|
||||||
|
Python Aternos
|
||||||
|
<div>
|
||||||
|
<a href="https://pypi.org/project/python-aternos/">
|
||||||
|
<img src="https://img.shields.io/pypi/v/python-aternos">
|
||||||
|
</a>
|
||||||
|
<a href="https://www.apache.org/licenses/LICENSE-2.0.html">
|
||||||
|
<img src="https://img.shields.io/pypi/l/python-aternos">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/DarkCat09/python-aternos/commits">
|
||||||
|
<img src="https://img.shields.io/github/last-commit/DarkCat09/python-aternos">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/DarkCat09/python-aternos/issues">
|
||||||
|
<img src="https://img.shields.io/github/issues/DarkCat09/python-aternos">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
An unofficial Aternos API written in Python.
|
An unofficial Aternos API written in Python.
|
||||||
It uses [aternos](https://aternos.org/)' private API and html parsing.
|
It uses [aternos](https://aternos.org/)' private API and html parsing.
|
||||||
|
|
||||||
## Installing
|
Python Aternos supports:
|
||||||
|
|
||||||
|
- Logging in to account with password (plain or hashed) or `ATERNOS_SESSION` cookie value.
|
||||||
|
- Saving session to the file and restoring.
|
||||||
|
- Changing username, email and password.
|
||||||
|
- Parsing Minecraft servers list.
|
||||||
|
- Parsing server info by its ID.
|
||||||
|
- Starting/stoping server, restarting, confirming/cancelling launch.
|
||||||
|
- Updating server info in real-time (view WebSocket API).
|
||||||
|
- Changing server subdomain and MOTD (message-of-the-day).
|
||||||
|
- Managing files, settings, players (whitelist, operators, etc.)
|
||||||
|
|
||||||
|
> **Warning**
|
||||||
|
>
|
||||||
|
> According to the Aternos' [Terms of Service §5.2e](https://aternos.gmbh/en/aternos/terms#:~:text=Automatically%20accessing%20our%20website%20or%20automating%20actions%20on%20our%20website.),
|
||||||
|
> you must not use any software or APIs for automated access,
|
||||||
|
> beacuse they don't receive money from advertisting in this case.
|
||||||
|
>
|
||||||
|
> I always try to hide automated python-aternos requests
|
||||||
|
> using browser-specific headers/cookies,
|
||||||
|
> but you should make backups to restore your world
|
||||||
|
> if your account will be banned
|
||||||
|
> (view [#16](https://github.com/DarkCat09/python-aternos/issues/16)
|
||||||
|
> and [#46](https://github.com/DarkCat09/python-aternos/issues/46)).
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
### Common
|
||||||
```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 problems with it, and then execute
|
||||||
|
> `pip install --no-deps python-aternos`
|
||||||
|
|
||||||
|
### Development
|
||||||
|
```bash
|
||||||
|
$ git clone https://github.com/DarkCat09/python-aternos.git
|
||||||
|
$ cd python-aternos
|
||||||
|
$ pip install -e .
|
||||||
```
|
```
|
||||||
> 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`
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
To use Aternos API in your Python script, import it
|
To use Aternos API in your Python script, import it
|
||||||
and login with your username and password/MD5.
|
and login with your username and password or MD5.
|
||||||
|
|
||||||
Then request the servers list using `list_servers()`.
|
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, calling `start()` or `stop()`.
|
||||||
|
|
||||||
Here is an example how to use the API:
|
Here is an example how to use the API:
|
||||||
```python
|
```python
|
||||||
|
@ -47,16 +101,20 @@ testserv = None
|
||||||
for serv in servs:
|
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 is not None:
|
||||||
# Prints a server softaware and its version
|
# Prints a server softaware and its version
|
||||||
# (for example, "Vanilla 1.12.2")
|
# (for example, "Vanilla 1.12.2")
|
||||||
print(testserv.software, testserv.version)
|
print(testserv.software, testserv.version)
|
||||||
# Starts server
|
# Starts server
|
||||||
testserv.start()
|
testserv.start()
|
||||||
```
|
```
|
||||||
The documentation have not made yet. View examples and ask in the issues.
|
|
||||||
|
|
||||||
## [More examples](https://codeberg.org/DarkCat09/python-aternos/src/branch/main/examples) / [on GitHub](https://github.com/DarkCat09/python-aternos/tree/main/examples)
|
## [More examples](https://github.com/DarkCat09/python-aternos/tree/main/examples)
|
||||||
|
|
||||||
|
## [Documentation](https://darkcat09.codeberg.page/aternos-docs/)
|
||||||
|
|
||||||
|
## [How-To Guide](https://darkcat09.codeberg.page/aternos-docs/howto/auth)
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|Version|Description |
|
|Version|Description |
|
||||||
|
@ -66,17 +124,20 @@ The documentation have not made yet. View examples and ask in the issues.
|
||||||
|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/v1.0.0|Code refactoring, websockets API and session saving to prevent detecting automation access.|
|
|**v0.6/v1.0.0**|Code refactoring, websockets API and session saving to prevent detecting automation access.|
|
||||||
|v1.0.x|Lots of bugfixes, changed versioning (SemVer).|
|
|v1.0.x|Lots of bugfixes, changed versioning (SemVer).|
|
||||||
|v1.1.x|Documentation, unit tests, pylint, bugfixes, changes in atwss.|
|
|v1.1.x|Documentation, unit tests, pylint, bugfixes, changes in atwss.|
|
||||||
|v1.2.x|Solution for #25|
|
|**v1.1.2/v2.0.0**|Solution for [#25](https://github.com/DarkCat09/python-aternos/issues/25) (Cloudflare bypassing), bugfixes in JS parser.|
|
||||||
|v1.3.x|Full implementation of config and software API.|
|
|v2.0.x|Documentation, automatically saving/restoring session, improvements in Files API.|
|
||||||
|v1.4.x|Shared access API and Google Drive backups.|
|
|v2.1.x|Fixes in the implementation of websockets API.|
|
||||||
|
|**v2.2.x**|Using Node.js as a JS interpreter if it's installed.|
|
||||||
|
|v3.0.x|Full implementation of config and software API.|
|
||||||
|
|v3.1.x|Shared access API and Google Drive backups.|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
[License Notice](NOTICE):
|
[License Notice](NOTICE):
|
||||||
```
|
```
|
||||||
Copyright 2021-2022 Chechkenev Andrey, lusm554, ghrlt, NotNalin
|
Copyright 2021-2022 All contributors
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -90,4 +151,3 @@ 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 or wheel.
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
Very interesting information
|
|
165
docs/howto/auth.md
Normal file
165
docs/howto/auth.md
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
# How-To 1: Logging in
|
||||||
|
|
||||||
|
## Intro
|
||||||
|
Firstly, let's install the library using the command from ["Common install" section](../#common).
|
||||||
|
```bash
|
||||||
|
pip install python-aternos
|
||||||
|
```
|
||||||
|
|
||||||
|
Also, [register](https://aternos.org/go/) an Aternos account if you haven't one.
|
||||||
|
Now you are ready.
|
||||||
|
|
||||||
|
## Authorization with password
|
||||||
|
Import python-aternos module:
|
||||||
|
```python
|
||||||
|
from python_aternos import Client
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, you can log in to your account using from_credentials method
|
||||||
|
specifying your username and password.
|
||||||
|
```python
|
||||||
|
at = Client.from_credentials('username', 'password')
|
||||||
|
```
|
||||||
|
This line will create Client object and save it to `at` variable.
|
||||||
|
|
||||||
|
Okay, we are logged in. What's next?
|
||||||
|
|
||||||
|
## Servers list
|
||||||
|
Request the list of your servers:
|
||||||
|
```python
|
||||||
|
servers = at.list_servers()
|
||||||
|
```
|
||||||
|
|
||||||
|
This variable must contain something like:
|
||||||
|
```python
|
||||||
|
[<python_aternos.atserver.AternosServer object at 0x7f97bd8b5690>]
|
||||||
|
```
|
||||||
|
|
||||||
|
If you have only one server in your account,
|
||||||
|
get it by the zero index:
|
||||||
|
```python
|
||||||
|
serv = servers[0]
|
||||||
|
```
|
||||||
|
|
||||||
|
Otherwise, iterate over the list to find it by IP or subdomain:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 1st way: For-loop
|
||||||
|
# Our server: test.aternos.me
|
||||||
|
|
||||||
|
# Find by IP (domain)
|
||||||
|
serv = None
|
||||||
|
for s in servers:
|
||||||
|
if s.domain == 'test.aternos.me':
|
||||||
|
serv = s
|
||||||
|
|
||||||
|
# Or find by subdomain
|
||||||
|
# (part before .aternos.me)
|
||||||
|
serv = None
|
||||||
|
for s in servers:
|
||||||
|
if s.subdomain == 'test':
|
||||||
|
serv = s
|
||||||
|
|
||||||
|
# Important check
|
||||||
|
if serv is None:
|
||||||
|
print('Not found!')
|
||||||
|
exit()
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 2nd way: Dict comprehension
|
||||||
|
|
||||||
|
serv = {
|
||||||
|
'serv': s
|
||||||
|
for s in servers
|
||||||
|
if s.subdomain == 'test'
|
||||||
|
}.get('serv', None)
|
||||||
|
|
||||||
|
if serv is None:
|
||||||
|
print('Not found!')
|
||||||
|
exit()
|
||||||
|
```
|
||||||
|
|
||||||
|
`serv` is an AternosServer object. I'll explain it more detailed in the next part.
|
||||||
|
Now, let's just try to start and stop server:
|
||||||
|
```python
|
||||||
|
# Start
|
||||||
|
serv.start()
|
||||||
|
|
||||||
|
# Stop
|
||||||
|
serv.stop()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Saving session
|
||||||
|
In the version `v2.0.1` and above,
|
||||||
|
python-aternos automatically saves and restores session cookie,
|
||||||
|
so you don't need to do it by yourself now.
|
||||||
|
|
||||||
|
Before, you should save session manually:
|
||||||
|
```python
|
||||||
|
# This code is useless in new versions,
|
||||||
|
# because they do it automatically.
|
||||||
|
|
||||||
|
from python_aternos import Client
|
||||||
|
|
||||||
|
at = Client.from_credentails('username', 'password')
|
||||||
|
myserv = at.list_servers()[0]
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
at.save_session()
|
||||||
|
|
||||||
|
# Closing python interpreter
|
||||||
|
# and opening it again
|
||||||
|
|
||||||
|
from python_aternos import Client
|
||||||
|
|
||||||
|
at = Client.restore_session()
|
||||||
|
myserv = at.list_servers()[0]
|
||||||
|
|
||||||
|
...
|
||||||
|
```
|
||||||
|
Function `save_session()` writes session cookie and cached servers list to `.aternos` file in your home directory.
|
||||||
|
`restore_session()` creates Client object from session cookie and restores servers list.
|
||||||
|
This feature reduces the count of network requests and allows you to log in and request servers much faster.
|
||||||
|
|
||||||
|
If you created a new server, but it doesn't appear in `list_servers` result, call it with `cache=False` argument.
|
||||||
|
```python
|
||||||
|
# Refreshing list
|
||||||
|
servers = at.list_servers(cache=False)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Username, email, password
|
||||||
|
Change them using the corresponding methods:
|
||||||
|
```python
|
||||||
|
at.change_username('new1cool2username3')
|
||||||
|
at.change_password('old_password', 'new_password')
|
||||||
|
at.change_email('new@email.com')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hashing passwords
|
||||||
|
For security reasons, Aternos API takes MD5 hashed passwords, not plain.
|
||||||
|
|
||||||
|
`from_credentials` hashes your credentials and passes to `from_hashed` classmethod.
|
||||||
|
`change_password` also hashes passwords and calls `change_password_hashed`.
|
||||||
|
And you can use these methods too.
|
||||||
|
Python-Aternos contains a handy function `Client.md5encode` that can help you with it.
|
||||||
|
|
||||||
|
```python
|
||||||
|
>>> from python_aternos import Client
|
||||||
|
>>> Client.md5encode('old_password')
|
||||||
|
'0512f08120c4fef707bd5e2259c537d0'
|
||||||
|
>>> Client.md5encode('new_password')
|
||||||
|
'88162595c58939c4ae0b35f39892e6e7'
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
from python_aternos import Client
|
||||||
|
|
||||||
|
my_passwd = '0512f08120c4fef707bd5e2259c537d0'
|
||||||
|
new_passwd = '88162595c58939c4ae0b35f39892e6e7'
|
||||||
|
|
||||||
|
at = Client.from_hashed('username', my_passwd)
|
||||||
|
|
||||||
|
at.change_password_hashed(my_passwd, new_passwd)
|
||||||
|
```
|
1
docs/howto/config.md
Normal file
1
docs/howto/config.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
## Coming soon
|
1
docs/howto/discord.md
Normal file
1
docs/howto/discord.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
## Coming soon
|
233
docs/howto/files.md
Normal file
233
docs/howto/files.md
Normal file
|
@ -0,0 +1,233 @@
|
||||||
|
# How-To 4: Files
|
||||||
|
|
||||||
|
## Intro
|
||||||
|
In python-aternos, all files on your Minecraft server
|
||||||
|
are represented as atfile.AternosFile objects.
|
||||||
|
|
||||||
|
They can be accessed through atfm.FileManager instance,
|
||||||
|
let's assign it to `fm` variable:
|
||||||
|
```python
|
||||||
|
>>> fm = serv.files()
|
||||||
|
```
|
||||||
|
|
||||||
|
## List directory contents
|
||||||
|
```python
|
||||||
|
>>> root = fm.list_dir('/')
|
||||||
|
[<python_aternos.atfile.AternosFile object at 0x7f1b0...>, ...]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Get file by its path
|
||||||
|
```python
|
||||||
|
>>> myfile = fm.get_file('/server.properties')
|
||||||
|
<python_aternos.atfile.AternosFile object at 0x7f1b0...>
|
||||||
|
```
|
||||||
|
|
||||||
|
## File info
|
||||||
|
AternosFile object can point to
|
||||||
|
both a file and a directory
|
||||||
|
and contain almost the same properties and methods.
|
||||||
|
(So it's more correct to call it "Object in the server's filesystem",
|
||||||
|
but I chose an easier name for the class.)
|
||||||
|
|
||||||
|
- `path` - Full path to the file **including trailing** slash and **without leading** slash.
|
||||||
|
- `name` - Filename with extension **without trailing** slash.
|
||||||
|
- `dirname` - Full path to the directory which contains the file **without leading** slash.
|
||||||
|
- `is_file` and `is_dir` - File type in boolean.
|
||||||
|
- `ftype` - File type in `FileType` enum value:
|
||||||
|
- `FileType.file`
|
||||||
|
- `FileType.dir` and `FileType.directory`
|
||||||
|
- `size` - File size in bytes, float.
|
||||||
|
`0.0` for directories and
|
||||||
|
`-1.0` when error occures.
|
||||||
|
- `deleteable`, `downloadable` and `editable` are explained in the next section.
|
||||||
|
|
||||||
|
### File
|
||||||
|
```python
|
||||||
|
>>> f = root[5]
|
||||||
|
|
||||||
|
>>> f.path
|
||||||
|
'/server.properties'
|
||||||
|
>>> f.name
|
||||||
|
'server.properties'
|
||||||
|
>>> f.dirname
|
||||||
|
''
|
||||||
|
|
||||||
|
>>> f.is_file
|
||||||
|
False
|
||||||
|
>>> f.is_dir
|
||||||
|
True
|
||||||
|
|
||||||
|
>>> from python_aternos import FileType
|
||||||
|
>>> f.ftype == FileType.file
|
||||||
|
True
|
||||||
|
>>> f.ftype == FileType.directory
|
||||||
|
False
|
||||||
|
|
||||||
|
>>> f.size
|
||||||
|
1240.0
|
||||||
|
|
||||||
|
>>> f.deleteable
|
||||||
|
False
|
||||||
|
>>> f.downloadable
|
||||||
|
False
|
||||||
|
>>> f.editable
|
||||||
|
False
|
||||||
|
```
|
||||||
|
|
||||||
|
### Directory
|
||||||
|
```python
|
||||||
|
>>> f = root[2]
|
||||||
|
|
||||||
|
>>> f.path
|
||||||
|
'/config'
|
||||||
|
>>> f.name
|
||||||
|
'config'
|
||||||
|
>>> f.dirname
|
||||||
|
''
|
||||||
|
|
||||||
|
>>> f.is_file
|
||||||
|
False
|
||||||
|
>>> f.is_dir
|
||||||
|
True
|
||||||
|
|
||||||
|
>>> from python_aternos import FileType
|
||||||
|
>>> f.ftype == FileType.file
|
||||||
|
False
|
||||||
|
>>> f.ftype == FileType.directory
|
||||||
|
True
|
||||||
|
>>> f.ftype == FileType.dir
|
||||||
|
True
|
||||||
|
|
||||||
|
>>> f.size
|
||||||
|
0.0
|
||||||
|
|
||||||
|
>>> f.deleteable
|
||||||
|
False
|
||||||
|
>>> f.downloadable
|
||||||
|
True
|
||||||
|
>>> f.editable
|
||||||
|
False
|
||||||
|
```
|
||||||
|
|
||||||
|
## Methods
|
||||||
|
|
||||||
|
- `get_text` returns the file content from the Aternos editor page
|
||||||
|
(opens when you click on the file on web site).
|
||||||
|
- `set_text` is the same as "Save" button in the Aternos editor.
|
||||||
|
- `get_content` requests file downloading and
|
||||||
|
returns file content in `bytes` (not `str`).
|
||||||
|
If it is a directory, Aternos returns its content in a ZIP file.
|
||||||
|
- `set_content` like `set_text`, but takes `bytes` as an argument.
|
||||||
|
- `delete` removes file.
|
||||||
|
- `create` creates a new file inside this one
|
||||||
|
(if it's a directory, otherwise throws RuntimeWarning).
|
||||||
|
|
||||||
|
### Deletion and downloading rejection
|
||||||
|
In [Aternos Files tab](https://aternos.org/files),
|
||||||
|
some files can be removed with a red button, some of them is protected.
|
||||||
|
You can check if the file is deleteable this way:
|
||||||
|
```python
|
||||||
|
>>> f.deleteable
|
||||||
|
False
|
||||||
|
```
|
||||||
|
`delete()` method will warn you if it's undeleteable,
|
||||||
|
and then you'll probably get `FileError`
|
||||||
|
because of Aternos deletion denial.
|
||||||
|
|
||||||
|
The same thing with `downloadable`.
|
||||||
|
```python
|
||||||
|
>>> f.downloadable
|
||||||
|
True
|
||||||
|
```
|
||||||
|
`get_content()` will warn you if it's undownloadable.
|
||||||
|
And then you'll get `FileError`.
|
||||||
|
|
||||||
|
And `editable` means that you can click on the file
|
||||||
|
in Aternos "Files" tab to open editor.
|
||||||
|
`get_text()` will warn about editing denial.
|
||||||
|
|
||||||
|
### Creating files
|
||||||
|
Calling `create` method only available for directories
|
||||||
|
(check it via `f.is_dir`).
|
||||||
|
It takes two arguments:
|
||||||
|
|
||||||
|
- `name` - name of a new file,
|
||||||
|
- `ftype` - type of a new file, must be `FileType` enum value:
|
||||||
|
- `FileType.file`
|
||||||
|
- `FileType.dir` or `FileType.directory`
|
||||||
|
|
||||||
|
For example, let's create an empty config
|
||||||
|
for some Forge mod, I'll call it "testmod".
|
||||||
|
```python
|
||||||
|
# Import enum
|
||||||
|
from python_aternos import FileType
|
||||||
|
|
||||||
|
# Get configs directory
|
||||||
|
conf = fm.get_file('/config')
|
||||||
|
|
||||||
|
# Create empty file
|
||||||
|
conf.create('testmod.toml', FileType.file)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Editing files
|
||||||
|
Let's edit `ops.json`.
|
||||||
|
It contains operators nicknames,
|
||||||
|
so the code below is the same as [Players API](../players/#list-types).
|
||||||
|
|
||||||
|
```python
|
||||||
|
import json
|
||||||
|
from python_aternos import Client
|
||||||
|
|
||||||
|
at = Client.from_credentials('username', 'password')
|
||||||
|
serv = at.list_servers()[0]
|
||||||
|
|
||||||
|
fm = serv.files()
|
||||||
|
ops = fm.get_file('/ops.json')
|
||||||
|
|
||||||
|
# If editable
|
||||||
|
use_get_text = True
|
||||||
|
|
||||||
|
# Check
|
||||||
|
if not ops.editable:
|
||||||
|
|
||||||
|
# One more check
|
||||||
|
if not ops.downloadable:
|
||||||
|
print('Error')
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
# If downloadable
|
||||||
|
use_get_text = False
|
||||||
|
|
||||||
|
def read():
|
||||||
|
|
||||||
|
if use_get_text:
|
||||||
|
return ops.get_text()
|
||||||
|
else:
|
||||||
|
return ops.get_content().decode('utf-8')
|
||||||
|
|
||||||
|
def write(content):
|
||||||
|
|
||||||
|
# set_text and set_content
|
||||||
|
# uses the same URLs.
|
||||||
|
# I prefer set_content
|
||||||
|
|
||||||
|
# but we need to convert content to bytes
|
||||||
|
content = content.encode('utf-8')
|
||||||
|
|
||||||
|
ops.set_content(content)
|
||||||
|
|
||||||
|
# It contains empty list [] by default
|
||||||
|
oper_raw = read()
|
||||||
|
|
||||||
|
# Convert to Python list
|
||||||
|
oper_lst = json.loads(oper_raw)
|
||||||
|
|
||||||
|
# Add an operator
|
||||||
|
oper_lst.append('DarkCat09')
|
||||||
|
|
||||||
|
# Convert back to JSON
|
||||||
|
oper_new = json.dumps(oper_lst)
|
||||||
|
|
||||||
|
# Write
|
||||||
|
ops.write(oper_new)
|
||||||
|
```
|
57
docs/howto/players.md
Normal file
57
docs/howto/players.md
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
# How-To 3: Players lists
|
||||||
|
You can add a player to operators,
|
||||||
|
include in the whitelist or ban
|
||||||
|
using this feature.
|
||||||
|
|
||||||
|
## Common usage
|
||||||
|
It's pretty easy:
|
||||||
|
```python
|
||||||
|
from python_aternos import Client, Lists
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
whitelist = serv.players(Lists.whl)
|
||||||
|
|
||||||
|
whitelist.add('jeb_')
|
||||||
|
whitelist.remove('Notch')
|
||||||
|
|
||||||
|
whitelist.list_players()
|
||||||
|
# ['DarkCat09', 'jeb_']
|
||||||
|
```
|
||||||
|
|
||||||
|
## List types
|
||||||
|
|
||||||
|
| Name | Enum key |
|
||||||
|
|:----------:|:---------:|
|
||||||
|
| Whitelist |`Lists.whl`|
|
||||||
|
| Operators |`Lists.ops`|
|
||||||
|
| Banned |`Lists.ban`|
|
||||||
|
|Banned by IP|`Lists.ips`|
|
||||||
|
|
||||||
|
For example, I want to ban someone:
|
||||||
|
```python
|
||||||
|
serv.players(Lists.ban).add('someone')
|
||||||
|
```
|
||||||
|
|
||||||
|
And give myself operator rights:
|
||||||
|
```python
|
||||||
|
serv.players(Lists.ops).add('DarkCat09')
|
||||||
|
```
|
||||||
|
|
||||||
|
Unban someone:
|
||||||
|
```python
|
||||||
|
serv.players(Lists.ban).remove('someone')
|
||||||
|
```
|
||||||
|
|
||||||
|
Unban someone who I banned by IP:
|
||||||
|
```python
|
||||||
|
serv.players(Lists.ips).remove('anyone')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Caching
|
||||||
|
If `list_players` doesn't show added players, call it with `cache=False` argument, like list_servers.
|
||||||
|
```python
|
||||||
|
lst = serv.players(Lists.ops)
|
||||||
|
lst.list_players(cache=False)
|
||||||
|
# ['DarkCat09', 'jeb_']
|
||||||
|
```
|
219
docs/howto/server.md
Normal file
219
docs/howto/server.md
Normal file
|
@ -0,0 +1,219 @@
|
||||||
|
# How-To 2: Controlling Minecraft server
|
||||||
|
|
||||||
|
In the previous part we logged into account and started a server.
|
||||||
|
But python-aternos can do much more.
|
||||||
|
|
||||||
|
## Basic methods
|
||||||
|
```python
|
||||||
|
from python_aternos import Client
|
||||||
|
|
||||||
|
at = Client.from_credentials('username', 'password')
|
||||||
|
serv = at.list_servers()[0]
|
||||||
|
|
||||||
|
# Start
|
||||||
|
serv.start()
|
||||||
|
|
||||||
|
# Stop
|
||||||
|
serv.stop()
|
||||||
|
|
||||||
|
# Restart
|
||||||
|
serv.restart()
|
||||||
|
|
||||||
|
# Cancel starting
|
||||||
|
serv.cancel()
|
||||||
|
|
||||||
|
# Confirm starting
|
||||||
|
# at the end of a queue
|
||||||
|
serv.confirm()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Starting
|
||||||
|
### Arguments
|
||||||
|
`start()` can be called with arguments:
|
||||||
|
|
||||||
|
- headstart (bool): Start server in headstart mode
|
||||||
|
which allows you to skip all queue.
|
||||||
|
- accepteula (bool): Automatically accept Mojang EULA.
|
||||||
|
|
||||||
|
If you want to launch your server instantly, use this code:
|
||||||
|
```python
|
||||||
|
serv.start(headstart=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Errors
|
||||||
|
`start()` raises `ServerStartError` if Aternos denies request.
|
||||||
|
This object contains an error code, on which depends an error message.
|
||||||
|
|
||||||
|
- EULA was not accepted (code: `eula`) -
|
||||||
|
remove `accepteula=False` or run `serv.eula()` before startup.
|
||||||
|
- Server is already running (code: `already`) -
|
||||||
|
you don't need to start server, it is online.
|
||||||
|
- Incorrect software version installed (code: `wrongversion`) -
|
||||||
|
if you have *somehow* installed non-existent software version (e.g. `Vanilla 2.16.5`).
|
||||||
|
- File server is unavailable (code: `file`) -
|
||||||
|
problems in Aternos servers, view [https://status.aternos.gmbh](https://status.aternos.gmbh)
|
||||||
|
- Available storage size limit has been reached (code: `size`) -
|
||||||
|
files on your Minecraft server have reached 4GB limit
|
||||||
|
(for exmaple, too much mods or loaded chunks).
|
||||||
|
|
||||||
|
Always wrap `start` into try-catch.
|
||||||
|
```python
|
||||||
|
from python_aternos import ServerStartError
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
try:
|
||||||
|
serv.start()
|
||||||
|
except ServerStartError as err:
|
||||||
|
print(err.code) # already
|
||||||
|
print(err.message) # Server is already running
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cancellation
|
||||||
|
Server launching can be cancelled only when you are waiting in a queue.
|
||||||
|
After queue, when the server starts and writes something to the log,
|
||||||
|
you can just `stop()` it, not `cancel()`.
|
||||||
|
|
||||||
|
## Server info
|
||||||
|
```python
|
||||||
|
>>> serv.address
|
||||||
|
'test.aternos.me:15172'
|
||||||
|
|
||||||
|
>>> serv.domain
|
||||||
|
'test.aternos.me'
|
||||||
|
|
||||||
|
>>> serv.subdomain
|
||||||
|
'test'
|
||||||
|
|
||||||
|
>>> serv.port
|
||||||
|
15172
|
||||||
|
|
||||||
|
>>> from python_aternos import Edition
|
||||||
|
>>> serv.edition
|
||||||
|
0
|
||||||
|
>>> serv.edition == Edition.java
|
||||||
|
True
|
||||||
|
>>> serv.edition == Edition.bedrock
|
||||||
|
False
|
||||||
|
|
||||||
|
>>> serv.software
|
||||||
|
'Forge'
|
||||||
|
>>> serv.version
|
||||||
|
'1.16.5 (36.2.34)'
|
||||||
|
|
||||||
|
>>> serv.players_list
|
||||||
|
['DarkCat09', 'jeb_']
|
||||||
|
>>> serv.players_count
|
||||||
|
2
|
||||||
|
>>> serv.slots
|
||||||
|
20
|
||||||
|
|
||||||
|
>>> print('Online:', serv.players_count, 'of', serv.slots)
|
||||||
|
Online: 2 of 20
|
||||||
|
|
||||||
|
>>> serv.motd
|
||||||
|
'§7Welcome to the §9Test Server§7!'
|
||||||
|
|
||||||
|
>>> from python_aternos import Status
|
||||||
|
>>> serv.css_class
|
||||||
|
'online'
|
||||||
|
>>> serv.status
|
||||||
|
'online'
|
||||||
|
>>> serv.status_num
|
||||||
|
1
|
||||||
|
>>> serv.status_num == Status.on
|
||||||
|
True
|
||||||
|
>>> serv.status_num == Status.off
|
||||||
|
False
|
||||||
|
>>> serv.status_num == Status.starting
|
||||||
|
False
|
||||||
|
|
||||||
|
>>> serv.restart()
|
||||||
|
|
||||||
|
# Title on web site: "Loading"
|
||||||
|
>>> serv.css_class
|
||||||
|
'loading'
|
||||||
|
>>> serv.status
|
||||||
|
'loading'
|
||||||
|
>>> serv.status_num
|
||||||
|
6
|
||||||
|
>>> serv.status_num == Status.loading
|
||||||
|
True
|
||||||
|
>>> serv.status_num == Status.preparing
|
||||||
|
False
|
||||||
|
>>> serv.status_num == Status.starting
|
||||||
|
False
|
||||||
|
|
||||||
|
# Title on web site: "Preparing"
|
||||||
|
>>> serv.css_class
|
||||||
|
'loading'
|
||||||
|
>>> serv.status
|
||||||
|
'preparing'
|
||||||
|
>>> serv.status_num
|
||||||
|
10
|
||||||
|
>>> serv.status_num == Status.preparing
|
||||||
|
True
|
||||||
|
>>> serv.status_num == Status.starting
|
||||||
|
False
|
||||||
|
>>> serv.status_num == Status.on
|
||||||
|
False
|
||||||
|
|
||||||
|
# Title on web site: "Starting"
|
||||||
|
>>> serv.css_class
|
||||||
|
'loading starting'
|
||||||
|
>>> serv.status
|
||||||
|
'starting'
|
||||||
|
>>> serv.status_num
|
||||||
|
2
|
||||||
|
>>> serv.status_num == Status.starting
|
||||||
|
True
|
||||||
|
>>> serv.status_num == Status.on
|
||||||
|
False
|
||||||
|
|
||||||
|
>>> serv.ram
|
||||||
|
2600
|
||||||
|
```
|
||||||
|
|
||||||
|
## Changing subdomain and MOTD
|
||||||
|
To change server subdomain or Message-of-the-Day,
|
||||||
|
just assign a new value to the corresponding fields:
|
||||||
|
```python
|
||||||
|
serv.subdomain = 'new-test-server123'
|
||||||
|
serv.motd = 'Welcome to the New Test Server!'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Updating status
|
||||||
|
python-aternos don't refresh server information by default.
|
||||||
|
This can be done with [WebSockets API](websocket) automatically
|
||||||
|
(but it will be explained later in the 6th part of how-to guide),
|
||||||
|
or with `fetch()` method manually (much easier).
|
||||||
|
|
||||||
|
`fetch()` called also when an AternosServer object is created
|
||||||
|
to get info about the server:
|
||||||
|
|
||||||
|
- full address,
|
||||||
|
- MOTD,
|
||||||
|
- software,
|
||||||
|
- connected players,
|
||||||
|
- status,
|
||||||
|
- etc.
|
||||||
|
|
||||||
|
Use it if you want to see new data one time:
|
||||||
|
```python
|
||||||
|
import time
|
||||||
|
from python_aternos import Client
|
||||||
|
|
||||||
|
at = Client.from_credentials('username', 'password')
|
||||||
|
serv = at.list_servers()[0]
|
||||||
|
|
||||||
|
# Start
|
||||||
|
serv.start()
|
||||||
|
# Wait 10 sec
|
||||||
|
time.sleep(10)
|
||||||
|
# Check
|
||||||
|
serv.fetch()
|
||||||
|
print('Server is', serv.status) # Server is online
|
||||||
|
```
|
||||||
|
But this method is **not** a good choice if you want to get real-time updates.
|
||||||
|
Read [How-To 6: Real-time updates](websocket) about WebSockets API
|
||||||
|
and use it instead of refreshing data in a while-loop.
|
1
docs/howto/websocket.md
Normal file
1
docs/howto/websocket.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
## Coming soon
|
153
docs/index.md
153
docs/index.md
|
@ -1,4 +1,153 @@
|
||||||
![Python-Aternos Logo](https://i.ibb.co/3RXcXJ1/aternos-400.png)
|
<div align="center">
|
||||||
# Python Aternos
|
<img src="https://i.ibb.co/3RXcXJ1/aternos-400.png" alt="Python Aternos Logo">
|
||||||
|
<h1>
|
||||||
|
Python Aternos
|
||||||
|
<div>
|
||||||
|
<a href="https://pypi.org/project/python-aternos/">
|
||||||
|
<img src="https://img.shields.io/pypi/v/python-aternos">
|
||||||
|
</a>
|
||||||
|
<a href="https://www.apache.org/licenses/LICENSE-2.0.html">
|
||||||
|
<img src="https://img.shields.io/pypi/l/python-aternos">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/DarkCat09/python-aternos/commits">
|
||||||
|
<img src="https://img.shields.io/github/last-commit/DarkCat09/python-aternos">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/DarkCat09/python-aternos/issues">
|
||||||
|
<img src="https://img.shields.io/github/issues/DarkCat09/python-aternos">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
An unofficial Aternos API written in Python.
|
An unofficial Aternos API written in Python.
|
||||||
It uses [aternos](https://aternos.org/)' private API and html parsing.
|
It uses [aternos](https://aternos.org/)' private API and html parsing.
|
||||||
|
|
||||||
|
Python Aternos supports:
|
||||||
|
|
||||||
|
- Logging in to account with password (plain or hashed) or `ATERNOS_SESSION` cookie value.
|
||||||
|
- Saving session to the file and restoring.
|
||||||
|
- Changing username, email and password.
|
||||||
|
- Parsing Minecraft servers list.
|
||||||
|
- Parsing server info by its ID.
|
||||||
|
- Starting/stoping server, restarting, confirming/cancelling launch.
|
||||||
|
- Updating server info in real-time (view WebSocket API).
|
||||||
|
- Changing server subdomain and MOTD (message-of-the-day).
|
||||||
|
- Managing files, settings, players (whitelist, operators, etc.)
|
||||||
|
|
||||||
|
> **Warning**
|
||||||
|
>
|
||||||
|
> According to the Aternos' [Terms of Service §5.2e](https://aternos.gmbh/en/aternos/terms#:~:text=Automatically%20accessing%20our%20website%20or%20automating%20actions%20on%20our%20website.),
|
||||||
|
> you must not use any software or APIs for automated access,
|
||||||
|
> beacuse they don't receive money from advertisting in this case.
|
||||||
|
>
|
||||||
|
> I always try to hide automated python-aternos requests
|
||||||
|
> using browser-specific headers/cookies,
|
||||||
|
> but you should make backups to restore your world
|
||||||
|
> if your account will be banned
|
||||||
|
> (view [#16](https://github.com/DarkCat09/python-aternos/issues/16)
|
||||||
|
> and [#46](https://github.com/DarkCat09/python-aternos/issues/46)).
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
### Common
|
||||||
|
```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 problems with it, and then execute
|
||||||
|
> `pip install --no-deps python-aternos`
|
||||||
|
|
||||||
|
### Development
|
||||||
|
```bash
|
||||||
|
$ git clone https://github.com/DarkCat09/python-aternos.git
|
||||||
|
$ cd python-aternos
|
||||||
|
$ pip install -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
To use Aternos API in your Python script, import it
|
||||||
|
and login with your username and password or MD5.
|
||||||
|
|
||||||
|
Then request the servers list using `list_servers()`.
|
||||||
|
You can start/stop your Aternos server, calling `start()` or `stop()`.
|
||||||
|
|
||||||
|
Here is an example how to use the API:
|
||||||
|
```python
|
||||||
|
# Import
|
||||||
|
from python_aternos import Client
|
||||||
|
|
||||||
|
# Log in
|
||||||
|
aternos = Client.from_credentials('example', 'test123')
|
||||||
|
# ----OR----
|
||||||
|
aternos = Client.from_hashed('example', 'cc03e747a6afbbcbf8be7668acfebee5')
|
||||||
|
# ----OR----
|
||||||
|
aternos = Client.restore_session()
|
||||||
|
|
||||||
|
# Returns AternosServer list
|
||||||
|
servs = aternos.list_servers()
|
||||||
|
|
||||||
|
# Get the first server by the 0 index
|
||||||
|
myserv = servs[0]
|
||||||
|
|
||||||
|
# Start
|
||||||
|
myserv.start()
|
||||||
|
# Stop
|
||||||
|
myserv.stop()
|
||||||
|
|
||||||
|
# You can also find server by IP
|
||||||
|
testserv = None
|
||||||
|
for serv in servs:
|
||||||
|
if serv.address == 'test.aternos.org':
|
||||||
|
testserv = serv
|
||||||
|
|
||||||
|
if testserv is not None:
|
||||||
|
# Prints a server softaware and its version
|
||||||
|
# (for example, "Vanilla 1.12.2")
|
||||||
|
print(testserv.software, testserv.version)
|
||||||
|
# Starts server
|
||||||
|
testserv.start()
|
||||||
|
```
|
||||||
|
|
||||||
|
## [More examples](https://github.com/DarkCat09/python-aternos/tree/main/examples)
|
||||||
|
|
||||||
|
## [Documentation](https://darkcat09.codeberg.page/aternos-docs/)
|
||||||
|
|
||||||
|
## [How-To Guide](https://darkcat09.codeberg.page/aternos-docs/howto/auth)
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|Version|Description |
|
||||||
|
|:-----:|:-----------|
|
||||||
|
|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/v1.0.0**|Code refactoring, websockets API and session saving to prevent detecting automation access.|
|
||||||
|
|v1.0.x|Lots of bugfixes, changed versioning (SemVer).|
|
||||||
|
|v1.1.x|Documentation, unit tests, pylint, bugfixes, changes in atwss.|
|
||||||
|
|**v1.1.2/v2.0.0**|Solution for [#25](https://github.com/DarkCat09/python-aternos/issues/25) (Cloudflare bypassing), bugfixes in JS parser.|
|
||||||
|
|v2.0.x|Documentation, automatically saving/restoring session, improvements in Files API.|
|
||||||
|
|v2.1.x|Fixes in the implementation of websockets API.|
|
||||||
|
|**v2.2.x**|Using Node.js as a JS interpreter if it's installed.|
|
||||||
|
|v3.0.x|Full implementation of config and software API.|
|
||||||
|
|v3.1.x|Shared access API and Google Drive backups.|
|
||||||
|
|
||||||
|
## License
|
||||||
|
[License Notice](NOTICE):
|
||||||
|
```
|
||||||
|
Copyright 2021-2022 All contributors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
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.
|
||||||
|
```
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
::: python_aternos.atclient
|
|
||||||
options:
|
|
||||||
show_source: false
|
|
||||||
|
|
||||||
::: python_aternos.atserver
|
|
||||||
options:
|
|
||||||
show_source: false
|
|
||||||
|
|
||||||
::: python_aternos.atplayers
|
|
||||||
options:
|
|
||||||
show_source: false
|
|
||||||
|
|
||||||
::: python_aternos.atconf
|
|
||||||
options:
|
|
||||||
show_source: false
|
|
||||||
|
|
||||||
::: python_aternos.atfm
|
|
||||||
options:
|
|
||||||
show_source: false
|
|
||||||
|
|
||||||
::: python_aternos.atfile
|
|
||||||
options:
|
|
||||||
show_source: false
|
|
||||||
|
|
||||||
::: python_aternos.atwss
|
|
||||||
options:
|
|
||||||
show_source: false
|
|
||||||
|
|
||||||
::: python_aternos.atconnect
|
|
||||||
options:
|
|
||||||
show_source: false
|
|
||||||
|
|
||||||
::: python_aternos.atjsparse
|
|
||||||
options:
|
|
||||||
show_source: false
|
|
||||||
|
|
||||||
::: python_aternos.aterrors
|
|
||||||
options:
|
|
||||||
show_source: false
|
|
2
docs/reference/atclient.md
Normal file
2
docs/reference/atclient.md
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
## `atclient` (Entry point)
|
||||||
|
### ::: python_aternos.atclient
|
2
docs/reference/atconf.md
Normal file
2
docs/reference/atconf.md
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
## atconf
|
||||||
|
### ::: python_aternos.atconf
|
2
docs/reference/atconnect.md
Normal file
2
docs/reference/atconnect.md
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
## atconnect
|
||||||
|
### ::: python_aternos.atconnect
|
2
docs/reference/aterrors.md
Normal file
2
docs/reference/aterrors.md
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
## aterrors
|
||||||
|
### ::: python_aternos.aterrors
|
2
docs/reference/atfile.md
Normal file
2
docs/reference/atfile.md
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
## atfile
|
||||||
|
### ::: python_aternos.atfile
|
2
docs/reference/atfm.md
Normal file
2
docs/reference/atfm.md
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
## atfm
|
||||||
|
### ::: python_aternos.atfm
|
2
docs/reference/atjsparse.md
Normal file
2
docs/reference/atjsparse.md
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
## atjsparse
|
||||||
|
### ::: python_aternos.atjsparse
|
2
docs/reference/atplayers.md
Normal file
2
docs/reference/atplayers.md
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
## `atplayers`
|
||||||
|
### ::: python_aternos.atplayers
|
2
docs/reference/atserver.md
Normal file
2
docs/reference/atserver.md
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
## `atserver`
|
||||||
|
### ::: python_aternos.atserver
|
2
docs/reference/atwss.md
Normal file
2
docs/reference/atwss.md
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
## atwss
|
||||||
|
### ::: python_aternos.atwss
|
|
@ -10,7 +10,8 @@ files = s.files()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
|
||||||
cmd = input('> ').strip().lower()
|
inp = input('> ').strip()
|
||||||
|
cmd = inp.lower()
|
||||||
|
|
||||||
if cmd == 'help':
|
if cmd == 'help':
|
||||||
print(
|
print(
|
||||||
|
@ -25,8 +26,8 @@ while True:
|
||||||
break
|
break
|
||||||
|
|
||||||
if cmd.startswith('list'):
|
if cmd.startswith('list'):
|
||||||
path = cmd.removeprefix('list').strip()
|
path = inp[4:].strip()
|
||||||
directory = files.listdir(path)
|
directory = files.list_dir(path)
|
||||||
|
|
||||||
print(path, 'contains:')
|
print(path, 'contains:')
|
||||||
for file in directory:
|
for file in directory:
|
||||||
|
|
36
mkdocs.yml
36
mkdocs.yml
|
@ -2,12 +2,40 @@ site_name: Python-Aternos
|
||||||
|
|
||||||
theme:
|
theme:
|
||||||
name: readthedocs
|
name: readthedocs
|
||||||
|
sticky_navigation: false
|
||||||
|
include_homepage_in_sidebar: false
|
||||||
|
prev_next_buttons_location: both
|
||||||
|
|
||||||
|
markdown_extensions:
|
||||||
|
- toc:
|
||||||
|
permalink: '#'
|
||||||
|
|
||||||
plugins:
|
plugins:
|
||||||
- search
|
- search
|
||||||
- mkdocstrings
|
- mkdocstrings:
|
||||||
|
handlers:
|
||||||
|
python:
|
||||||
|
options:
|
||||||
|
show_source: false
|
||||||
|
|
||||||
nav:
|
nav:
|
||||||
- index.md
|
- Home: 'index.md'
|
||||||
- howto.md
|
- 'How-To Guide':
|
||||||
- reference.md
|
- 'Logging in': 'howto/auth.md'
|
||||||
|
- 'Servers': 'howto/server.md'
|
||||||
|
- 'Whitelist': 'howto/players.md'
|
||||||
|
- 'Files': 'howto/files.md'
|
||||||
|
- 'Settings': 'howto/config.md'
|
||||||
|
- 'Real-time updates': 'howto/websocket.md'
|
||||||
|
- 'Discord bot': 'howto/discord.md'
|
||||||
|
- 'API Reference':
|
||||||
|
- atclient: 'reference/atclient.md'
|
||||||
|
- atserver: 'reference/atserver.md'
|
||||||
|
- atplayers: 'reference/atplayers.md'
|
||||||
|
- atconf: 'reference/atconf.md'
|
||||||
|
- atfm: 'reference/atfm.md'
|
||||||
|
- atfile: 'reference/atfile.md'
|
||||||
|
- atconnect: 'reference/atconnect.md'
|
||||||
|
- atjsparse: 'reference/atjsparse.md'
|
||||||
|
- aterrors: 'reference/aterrors.md'
|
||||||
|
- atwss: 'reference/atwss.md'
|
||||||
|
|
|
@ -4,6 +4,7 @@ and allows to manage your account"""
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import logging
|
||||||
|
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
|
@ -16,18 +17,24 @@ from .aterrors import CredentialsError
|
||||||
|
|
||||||
class Client:
|
class Client:
|
||||||
|
|
||||||
"""Aternos API Client class object of which contains user's auth data
|
"""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
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
atconn: AternosConnect,
|
atconn: AternosConnect,
|
||||||
servers: Optional[List[str]] = None) -> None:
|
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.atconn = atconn
|
||||||
self.parsed = False
|
self.parsed = False
|
||||||
self.servers: List[AternosServer] = []
|
self.servers: List[AternosServer] = []
|
||||||
|
@ -36,24 +43,38 @@ class Client:
|
||||||
self.refresh_servers(servers)
|
self.refresh_servers(servers)
|
||||||
|
|
||||||
@classmethod
|
@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
|
Args:
|
||||||
:type username: str
|
username (str): Your username
|
||||||
:param md5: Your password hashed with MD5
|
md5 (str): Your password hashed with MD5
|
||||||
:type md5: str
|
sessions_dir (str): Path to the directory
|
||||||
:raises CredentialsError: If the API
|
where session will be automatically saved
|
||||||
doesn't return a valid session cookie
|
|
||||||
:return: Client instance
|
Raises:
|
||||||
:rtype: python_aternos.Client
|
CredentialsError: If the API didn't
|
||||||
|
return a valid session cookie
|
||||||
"""
|
"""
|
||||||
|
|
||||||
atconn = AternosConnect()
|
atconn = AternosConnect()
|
||||||
atconn.parse_token()
|
atconn.parse_token()
|
||||||
atconn.generate_sec()
|
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 = {
|
credentials = {
|
||||||
'user': username,
|
'user': username,
|
||||||
'password': md5
|
'password': md5
|
||||||
|
@ -69,23 +90,36 @@ class Client:
|
||||||
'Check your username and password'
|
'Check your username and password'
|
||||||
)
|
)
|
||||||
|
|
||||||
return cls(atconn)
|
obj = cls(atconn)
|
||||||
|
|
||||||
|
try:
|
||||||
|
obj.save_session(filename)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
@classmethod
|
@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
|
"""Log in to Aternos with a username and a plain password
|
||||||
|
|
||||||
:param username: Your username
|
Args:
|
||||||
:type username: str
|
username (str): Your username
|
||||||
:param password: Your password without any encryption
|
password (str): Your password without any encryption
|
||||||
:type password: str
|
sessions_dir (str): Path to the directory
|
||||||
:return: Client instance
|
where session will be automatically saved
|
||||||
:rtype: python_aternos.Client
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
md5 = Client.md5encode(password)
|
md5 = Client.md5encode(password)
|
||||||
return cls.from_hashed(username, md5)
|
return cls.from_hashed(
|
||||||
|
username, md5,
|
||||||
|
sessions_dir
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_session(
|
def from_session(
|
||||||
|
@ -95,10 +129,8 @@ class Client:
|
||||||
|
|
||||||
"""Log in to Aternos using a session cookie value
|
"""Log in to Aternos using a session cookie value
|
||||||
|
|
||||||
:param session: Value of ATERNOS_SESSION cookie
|
Args:
|
||||||
:type session: str
|
session (str): Value of ATERNOS_SESSION cookie
|
||||||
:return: Client instance
|
|
||||||
:rtype: python_aternos.Client
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
atconn = AternosConnect()
|
atconn = AternosConnect()
|
||||||
|
@ -113,18 +145,28 @@ class Client:
|
||||||
|
|
||||||
"""Log in to Aternos using a saved ATERNOS_SESSION cookie
|
"""Log in to Aternos using a saved ATERNOS_SESSION cookie
|
||||||
|
|
||||||
:param file: File where a session cookie
|
Args:
|
||||||
was saved, deafults to `~/.aternos`
|
file (str, optional): File where a session cookie was saved
|
||||||
:type file: str, optional
|
|
||||||
:return: Client instance
|
|
||||||
:rtype: python_aternos.Client
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
file = os.path.expanduser(file)
|
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:
|
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()
|
session = saved[0].strip()
|
||||||
|
if session == '':
|
||||||
|
raise CredentialsError(
|
||||||
|
'Unable to read session cookie, '
|
||||||
|
'the first line is empty'
|
||||||
|
)
|
||||||
|
|
||||||
if len(saved) > 1:
|
if len(saved) > 1:
|
||||||
return cls.from_session(
|
return cls.from_session(
|
||||||
|
@ -139,15 +181,36 @@ class Client:
|
||||||
|
|
||||||
"""Encodes the given string with MD5
|
"""Encodes the given string with MD5
|
||||||
|
|
||||||
:param passwd: String to encode
|
Args:
|
||||||
:type passwd: str
|
passwd (str): String to encode
|
||||||
:return: Hexdigest hash of the string in lowercase
|
|
||||||
:rtype: str
|
Returns:
|
||||||
|
Hexdigest hash of the string in lowercase
|
||||||
"""
|
"""
|
||||||
|
|
||||||
encoded = hashlib.md5(passwd.encode('utf-8'))
|
encoded = hashlib.md5(passwd.encode('utf-8'))
|
||||||
return encoded.hexdigest().lower()
|
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(
|
def save_session(
|
||||||
self,
|
self,
|
||||||
file: str = '~/.aternos',
|
file: str = '~/.aternos',
|
||||||
|
@ -155,17 +218,16 @@ class Client:
|
||||||
|
|
||||||
"""Saves an ATERNOS_SESSION cookie to a file
|
"""Saves an ATERNOS_SESSION cookie to a file
|
||||||
|
|
||||||
:param file: File where a session cookie
|
Args:
|
||||||
must be saved, defaults to `~/.aternos`
|
file (str, optional): File where a session cookie must be saved
|
||||||
:type file: str, optional
|
incl_servers (bool, optional): If the function
|
||||||
:param incl_servers: If the function
|
|
||||||
should include the servers IDs to
|
should include the servers IDs to
|
||||||
reduce API requests count (recommended),
|
reduce API requests count (recommended)
|
||||||
defaults to True
|
|
||||||
:type incl_servers: bool, optional
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
file = os.path.expanduser(file)
|
file = os.path.expanduser(file)
|
||||||
|
logging.debug(f'Saving session to {file}')
|
||||||
|
|
||||||
with open(file, 'wt', encoding='utf-8') as f:
|
with open(file, 'wt', encoding='utf-8') as f:
|
||||||
|
|
||||||
f.write(self.atconn.atsession + '\n')
|
f.write(self.atconn.atsession + '\n')
|
||||||
|
@ -179,11 +241,12 @@ class Client:
|
||||||
|
|
||||||
"""Parses a list of your servers from Aternos website
|
"""Parses a list of your servers from Aternos website
|
||||||
|
|
||||||
:param cache: If the function should use
|
Args:
|
||||||
cached servers list (recommended), defaults to True
|
cache (bool, optional): If the function should use
|
||||||
:type cache: bool, optional
|
cached servers list (recommended)
|
||||||
:return: List of :class:`python_aternos.atserver.AternosServer` objects
|
|
||||||
:rtype: list
|
Returns:
|
||||||
|
List of AternosServer objects
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if cache and self.parsed:
|
if cache and self.parsed:
|
||||||
|
@ -204,10 +267,10 @@ class Client:
|
||||||
def refresh_servers(self, ids: List[str]) -> None:
|
def refresh_servers(self, ids: List[str]) -> None:
|
||||||
|
|
||||||
"""Replaces cached servers list creating
|
"""Replaces cached servers list creating
|
||||||
:class:`AternosServer` objects by given IDs
|
AternosServer objects by given IDs
|
||||||
|
|
||||||
:param ids: Servers unique identifiers
|
Args:
|
||||||
:type ids: List[str]
|
ids (List[str]): Servers unique identifiers
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.servers = []
|
self.servers = []
|
||||||
|
@ -217,6 +280,7 @@ class Client:
|
||||||
if servid == '':
|
if servid == '':
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
logging.debug(f'Adding server {servid}')
|
||||||
srv = AternosServer(servid, self.atconn)
|
srv = AternosServer(servid, self.atconn)
|
||||||
self.servers.append(srv)
|
self.servers.append(srv)
|
||||||
|
|
||||||
|
@ -225,17 +289,18 @@ class Client:
|
||||||
def get_server(self, servid: str) -> AternosServer:
|
def get_server(self, servid: str) -> AternosServer:
|
||||||
|
|
||||||
"""Creates a server object from the server ID.
|
"""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
|
Returns:
|
||||||
:rtype: python_aternos.atserver.AternosServer
|
AternosServer object
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return AternosServer(servid, self.atconn)
|
return AternosServer(servid, self.atconn)
|
||||||
|
|
||||||
def logout(self) -> None:
|
def logout(self) -> None:
|
||||||
|
|
||||||
"""Logouts from Aternos account"""
|
"""Log out from Aternos account"""
|
||||||
|
|
||||||
self.atconn.request_cloudflare(
|
self.atconn.request_cloudflare(
|
||||||
'https://aternos.org/panel/ajax/account/logout.php',
|
'https://aternos.org/panel/ajax/account/logout.php',
|
||||||
|
@ -246,8 +311,8 @@ class Client:
|
||||||
|
|
||||||
"""Changes a username in your Aternos account
|
"""Changes a username in your Aternos account
|
||||||
|
|
||||||
:param value: New username
|
Args:
|
||||||
:type value: str
|
value (str): New username
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.atconn.request_cloudflare(
|
self.atconn.request_cloudflare(
|
||||||
|
@ -259,10 +324,12 @@ class Client:
|
||||||
|
|
||||||
"""Changes an e-mail in your Aternos account
|
"""Changes an e-mail in your Aternos account
|
||||||
|
|
||||||
:param value: New e-mail
|
Args:
|
||||||
:type value: str
|
value (str): New e-mail
|
||||||
:raises ValueError: If an invalid
|
|
||||||
e-mail address was passed to the function
|
Raises:
|
||||||
|
ValueError: If an invalid e-mail address
|
||||||
|
was passed to the function
|
||||||
"""
|
"""
|
||||||
|
|
||||||
email = re.compile(
|
email = re.compile(
|
||||||
|
@ -280,14 +347,27 @@ class Client:
|
||||||
|
|
||||||
"""Changes a password in your Aternos account
|
"""Changes a password in your Aternos account
|
||||||
|
|
||||||
:param old: Old password
|
Args:
|
||||||
:type old: str
|
old (str): Old password
|
||||||
:param new: New password
|
new (str): New password
|
||||||
:type new: str
|
"""
|
||||||
|
|
||||||
|
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(
|
self.atconn.request_cloudflare(
|
||||||
'https://aternos.org/panel/ajax/account/password.php',
|
'https://aternos.org/panel/ajax/account/password.php',
|
||||||
'POST', data={
|
'POST', data={
|
||||||
|
|
|
@ -121,22 +121,25 @@ convert = {
|
||||||
|
|
||||||
class AternosConfig:
|
class AternosConfig:
|
||||||
|
|
||||||
"""Class for editing server settings
|
"""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:
|
||||||
|
|
||||||
|
"""Class for editing server settings
|
||||||
|
|
||||||
|
Args:
|
||||||
|
atserv (python_aternos.atserver.AternosServer):
|
||||||
|
atserver.AternosServer object
|
||||||
|
"""
|
||||||
|
|
||||||
self.atserv = atserv
|
self.atserv = atserv
|
||||||
|
|
||||||
def get_timezone(self) -> str:
|
def get_timezone(self) -> str:
|
||||||
|
|
||||||
"""Parses timezone from options page
|
"""Parses timezone from options page
|
||||||
|
|
||||||
:return: Area/Location
|
Returns:
|
||||||
:rtype: str
|
Area/Location
|
||||||
"""
|
"""
|
||||||
|
|
||||||
optreq = self.atserv.atserver_request(
|
optreq = self.atserv.atserver_request(
|
||||||
|
@ -154,10 +157,12 @@ class AternosConfig:
|
||||||
|
|
||||||
"""Sets new timezone
|
"""Sets new timezone
|
||||||
|
|
||||||
:param value: New timezone
|
Args:
|
||||||
:type value: str
|
value (str): New timezone
|
||||||
:raises ValueError: If given string
|
|
||||||
doesn't match Area/Location format
|
Raises:
|
||||||
|
ValueError: If given string doesn't
|
||||||
|
match `Area/Location` format
|
||||||
"""
|
"""
|
||||||
|
|
||||||
matches_tz = tzcheck.search(value)
|
matches_tz = tzcheck.search(value)
|
||||||
|
@ -176,8 +181,8 @@ class AternosConfig:
|
||||||
|
|
||||||
"""Parses Java version from options page
|
"""Parses Java version from options page
|
||||||
|
|
||||||
:return: Java image version
|
Returns:
|
||||||
:rtype: int
|
Java image version
|
||||||
"""
|
"""
|
||||||
|
|
||||||
optreq = self.atserv.atserver_request(
|
optreq = self.atserv.atserver_request(
|
||||||
|
@ -198,8 +203,8 @@ class AternosConfig:
|
||||||
|
|
||||||
"""Sets new Java version
|
"""Sets new Java version
|
||||||
|
|
||||||
:param value: New Java image version
|
Args:
|
||||||
:type value: int
|
value (int): New Java image version
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.atserv.atserver_request(
|
self.atserv.atserver_request(
|
||||||
|
@ -215,10 +220,9 @@ class AternosConfig:
|
||||||
|
|
||||||
"""Sets server.properties option
|
"""Sets server.properties option
|
||||||
|
|
||||||
:param option: Option name
|
Args:
|
||||||
:type option: str
|
option (str): Option name
|
||||||
:param value: New value
|
value (Any): New value
|
||||||
:type value: Any
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.__set_prop(
|
self.__set_prop(
|
||||||
|
@ -230,12 +234,15 @@ class AternosConfig:
|
||||||
|
|
||||||
"""Parses all server.properties from options page
|
"""Parses all server.properties from options page
|
||||||
|
|
||||||
:param proptyping: If the returned dict should contain value
|
Args:
|
||||||
that matches property type (e.g. max-players will be int)
|
proptyping (bool, optional):
|
||||||
instead of string, defaults to True
|
If the returned dict should
|
||||||
:type proptyping: bool, optional
|
contain value that matches
|
||||||
:return: Server.properties dict
|
property type (e.g. max-players will be int)
|
||||||
:rtype: Dict[str,Any]
|
instead of string
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
`server.properties` dictionary
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self.__get_all_props('https://aternos.org/options', proptyping)
|
return self.__get_all_props('https://aternos.org/options', proptyping)
|
||||||
|
@ -244,8 +251,9 @@ class AternosConfig:
|
||||||
|
|
||||||
"""Updates server.properties options with the given dict
|
"""Updates server.properties options with the given dict
|
||||||
|
|
||||||
:param props: Dict with properties `{key:value}`
|
Args:
|
||||||
:type props: Dict[str,Any]
|
props (Dict[str,Any]):
|
||||||
|
Dictionary with `{key:value}` properties
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for key in props:
|
for key in props:
|
||||||
|
@ -261,16 +269,12 @@ class AternosConfig:
|
||||||
|
|
||||||
"""Sets level.dat option for specified world
|
"""Sets level.dat option for specified world
|
||||||
|
|
||||||
:param option: Option name
|
Args:
|
||||||
:type option: Union[WorldOpts,WorldRules]
|
option (Union[WorldOpts, WorldRules]): Option name
|
||||||
:param value: New value
|
value (Any): New value
|
||||||
:type value: Any
|
gamerule (bool, optional): If the option is a gamerule
|
||||||
:param gamerule: If the option
|
world (str, optional): Name of the world which
|
||||||
is a gamerule, defaults to False
|
`level.dat` must be edited
|
||||||
: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
|
||||||
|
@ -289,14 +293,16 @@ class AternosConfig:
|
||||||
|
|
||||||
"""Parses level.dat from specified world's options page
|
"""Parses level.dat from specified world's options page
|
||||||
|
|
||||||
:param world: Name of the world, defaults to 'world'
|
Args:
|
||||||
:type world: str, optional
|
world (str, optional): Name of the worl
|
||||||
:param proptyping: If the returned dict should contain the value
|
proptyping (bool, optional):
|
||||||
that matches property type (e.g. randomTickSpeed will be bool)
|
If the returned dict should
|
||||||
instead of string, defaults to True
|
contain the value that matches
|
||||||
:type proptyping: bool, optional
|
property type (e.g. randomTickSpeed will be bool)
|
||||||
:return: Level.dat dict
|
instead of string
|
||||||
:rtype: Dict[str,Any]
|
|
||||||
|
Returns:
|
||||||
|
`level.dat` options dictionary
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self.__get_all_props(
|
return self.__get_all_props(
|
||||||
|
@ -312,11 +318,11 @@ class AternosConfig:
|
||||||
"""Sets level.dat options from
|
"""Sets level.dat options from
|
||||||
the dictionary for the specified world
|
the dictionary for the specified world
|
||||||
|
|
||||||
:param props: Level.dat options
|
Args:
|
||||||
:type props: Dict[Union[WorldOpts, WorldRules], Any]
|
props (Dict[Union[WorldOpts, WorldRules], Any]):
|
||||||
:param world: name of the world which
|
`level.dat` options
|
||||||
level.dat must be edited, defaults to 'world'
|
world (str): name of the world which
|
||||||
:type world: str
|
`level.dat` must be edited
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for key in props:
|
for key in props:
|
||||||
|
|
|
@ -7,7 +7,9 @@ import logging
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
from requests import Response
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
from cloudscraper import CloudScraper
|
from cloudscraper import CloudScraper
|
||||||
|
|
||||||
|
@ -23,14 +25,12 @@ REQUA = \
|
||||||
|
|
||||||
class AternosConnect:
|
class AternosConnect:
|
||||||
|
|
||||||
"""
|
"""Class for sending API requests
|
||||||
Class for sending API requests bypass Cloudflare
|
bypass Cloudflare and parsing responses"""
|
||||||
and parsing responses"""
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
|
|
||||||
self.session = CloudScraper()
|
self.session = CloudScraper()
|
||||||
self.atsession = ''
|
|
||||||
self.sec = ''
|
self.sec = ''
|
||||||
self.token = ''
|
self.token = ''
|
||||||
|
|
||||||
|
@ -39,12 +39,14 @@ class AternosConnect:
|
||||||
"""Parses Aternos ajax token that
|
"""Parses Aternos ajax token that
|
||||||
is needed for most requests
|
is needed for most requests
|
||||||
|
|
||||||
:raises RuntimeWarning: If the parser
|
Raises:
|
||||||
can not find <head> tag in HTML response
|
RuntimeWarning: If the parser can not
|
||||||
:raises CredentialsError: If the parser
|
find `<head>` tag in HTML response
|
||||||
is unable to extract ajax token in HTML
|
TokenError: If the parser is unable
|
||||||
:return: Aternos ajax token
|
to extract ajax token from HTML
|
||||||
:rtype: str
|
|
||||||
|
Returns:
|
||||||
|
Aternos ajax token
|
||||||
"""
|
"""
|
||||||
|
|
||||||
loginpage = self.request_cloudflare(
|
loginpage = self.request_cloudflare(
|
||||||
|
@ -71,7 +73,10 @@ class AternosConnect:
|
||||||
try:
|
try:
|
||||||
text = pagehead.decode('utf-8', 'replace')
|
text = pagehead.decode('utf-8', 'replace')
|
||||||
js_code = re.findall(r'\(\(\)(.*?)\)\(\);', text)
|
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)
|
ctx = atjsparse.exec_js(token_func)
|
||||||
self.token = ctx.window['AJAX_TOKEN']
|
self.token = ctx.window['AJAX_TOKEN']
|
||||||
|
@ -88,8 +93,8 @@ class AternosConnect:
|
||||||
"""Generates Aternos SEC token which
|
"""Generates Aternos SEC token which
|
||||||
is also needed for most API requests
|
is also needed for most API requests
|
||||||
|
|
||||||
:return: Random SEC key:value string
|
Returns:
|
||||||
:rtype: str
|
Random SEC `key:value` string
|
||||||
"""
|
"""
|
||||||
|
|
||||||
randkey = self.generate_aternos_rand()
|
randkey = self.generate_aternos_rand()
|
||||||
|
@ -107,10 +112,11 @@ class AternosConnect:
|
||||||
"""Generates a random string using
|
"""Generates a random string using
|
||||||
Aternos algorithm from main.js file
|
Aternos algorithm from main.js file
|
||||||
|
|
||||||
:param randlen: Random string length, defaults to 16
|
Args:
|
||||||
:type randlen: int, optional
|
randlen (int, optional): Random string length
|
||||||
:return: Random string for SEC token
|
|
||||||
:rtype: str
|
Returns:
|
||||||
|
Random string for SEC token
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# a list with randlen+1 empty strings:
|
# a list with randlen+1 empty strings:
|
||||||
|
@ -129,16 +135,15 @@ class AternosConnect:
|
||||||
|
|
||||||
"""Converts an integer to specified base
|
"""Converts an integer to specified base
|
||||||
|
|
||||||
:param num: Integer in any base to convert.
|
Args:
|
||||||
If it is a float started with `0,`,
|
num (Union[int,float,str]): Integer in any base to convert.
|
||||||
zero and comma will be removed to get int
|
If it is a float starting with `0.`,
|
||||||
:type num: Union[int,float,str]
|
zero and point will be removed to get int
|
||||||
:param base: New base
|
base (int): New base
|
||||||
:type base: int
|
frombase (int, optional): Given number base
|
||||||
:param frombase: Given number base, defaults to 10
|
|
||||||
:type frombase: int, optional
|
Returns:
|
||||||
:return: Number converted to a specified base
|
Number converted to a specified base
|
||||||
:rtype: str
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if isinstance(num, str):
|
if isinstance(num, str):
|
||||||
|
@ -159,40 +164,35 @@ class AternosConnect:
|
||||||
|
|
||||||
def request_cloudflare(
|
def request_cloudflare(
|
||||||
self, url: str, method: str,
|
self, url: str, method: str,
|
||||||
params: Optional[dict] = None,
|
params: Optional[Dict[Any, Any]] = None,
|
||||||
data: Optional[dict] = None,
|
data: Optional[Dict[Any, Any]] = None,
|
||||||
headers: Optional[dict] = None,
|
headers: Optional[Dict[Any, Any]] = None,
|
||||||
reqcookies: Optional[dict] = None,
|
reqcookies: Optional[Dict[Any, Any]] = None,
|
||||||
sendtoken: bool = False,
|
sendtoken: bool = False,
|
||||||
retry: int = 5) -> Response:
|
retry: int = 5) -> requests.Response:
|
||||||
|
|
||||||
"""Sends a request to Aternos API bypass Cloudflare
|
"""Sends a request to Aternos API bypass Cloudflare
|
||||||
|
|
||||||
:param url: Request URL
|
Args:
|
||||||
:type url: str
|
url (str): Request URL
|
||||||
:param method: Request method, must be GET or POST
|
method (str): Request method, must be GET or POST
|
||||||
:type method: str
|
params (Optional[Dict[Any, Any]], optional): URL parameters
|
||||||
:param params: URL parameters, defaults to None
|
data (Optional[Dict[Any, Any]], optional): POST request data,
|
||||||
:type params: Optional[dict], optional
|
if the method is GET, this dict will be combined with params
|
||||||
:param data: POST request data, if the method is GET,
|
headers (Optional[Dict[Any, Any]], optional): Custom headers
|
||||||
this dict will be combined with params, defaults to None
|
reqcookies (Optional[Dict[Any, Any]], optional):
|
||||||
:type data: Optional[dict], optional
|
Cookies only for this request
|
||||||
:param headers: Custom headers, defaults to None
|
sendtoken (bool, optional): If the ajax and SEC token
|
||||||
:type headers: Optional[dict], optional
|
should be sent
|
||||||
:param reqcookies: Cookies only for this request, defaults to None
|
retry (int, optional): How many times parser must retry
|
||||||
:type reqcookies: Optional[dict], optional
|
connection to API bypass Cloudflare
|
||||||
:param sendtoken: If the ajax and SEC token
|
|
||||||
should be sent, defaults to False
|
Raises:
|
||||||
:type sendtoken: bool, optional
|
CloudflareError: When the parser has exceeded retries count
|
||||||
:param retry: How many times parser must retry
|
NotImplementedError: When the specified method is not GET or POST
|
||||||
connection to API bypass Cloudflare, defaults to 5
|
|
||||||
:type retry: int, optional
|
Returns:
|
||||||
:raises CloudflareError:
|
API response
|
||||||
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:
|
if retry <= 0:
|
||||||
|
@ -202,12 +202,6 @@ class AternosConnect:
|
||||||
self.session = CloudScraper()
|
self.session = CloudScraper()
|
||||||
self.session.cookies.update(old_cookies)
|
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 {}
|
params = params or {}
|
||||||
data = data or {}
|
data = data or {}
|
||||||
headers = headers or {}
|
headers = headers or {}
|
||||||
|
@ -276,3 +270,17 @@ class AternosConnect:
|
||||||
|
|
||||||
req.raise_for_status()
|
req.raise_for_status()
|
||||||
return req
|
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):
|
class ServerError(AternosError):
|
||||||
|
|
||||||
"""Common class for server errors
|
"""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
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, reason: str, message: str = '') -> None:
|
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
|
self.reason = reason
|
||||||
super().__init__(message)
|
super().__init__(message)
|
||||||
|
|
||||||
|
|
||||||
class ServerStartError(AternosError):
|
class ServerStartError(AternosError):
|
||||||
|
|
||||||
"""Raised when Aternos can not start Minecraft server
|
"""Raised when Aternos can not start Minecraft server"""
|
||||||
|
|
||||||
:param reason: Code which contains error reason
|
|
||||||
:type reason: str
|
|
||||||
"""
|
|
||||||
|
|
||||||
MESSAGE: Final = 'Unable to start server, code: {}'
|
MESSAGE: Final = 'Unable to start server, code: {}'
|
||||||
reason_msg = {
|
reason_msg = {
|
||||||
|
@ -57,22 +54,31 @@ class ServerStartError(AternosError):
|
||||||
'EULA was not accepted. '
|
'EULA was not accepted. '
|
||||||
'Use start(accepteula=True)',
|
'Use start(accepteula=True)',
|
||||||
|
|
||||||
'already': 'Server is already running',
|
'already': 'Server has already started',
|
||||||
'wrongversion': 'Incorrect software version installed',
|
'wrongversion': 'Incorrect software version installed',
|
||||||
|
|
||||||
'file':
|
'file':
|
||||||
'File server is unavailbale, '
|
'File server is unavailbale, '
|
||||||
'view https://status.aternos.gmbh',
|
'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:
|
def __init__(self, reason: str) -> None:
|
||||||
|
|
||||||
|
"""Raised when Aternos
|
||||||
|
can not start Minecraft server
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reason (str):
|
||||||
|
Code which contains error reason
|
||||||
|
"""
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
reason,
|
reason,
|
||||||
self.reason_msg.get(
|
self.reason_msg.get(
|
||||||
reason, self.MESSAGE.format(reason)
|
reason,
|
||||||
|
self.MESSAGE.format(reason)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -19,109 +19,214 @@ class FileType(enum.IntEnum):
|
||||||
|
|
||||||
file = 0
|
file = 0
|
||||||
directory = 1
|
directory = 1
|
||||||
|
dir = 1
|
||||||
|
|
||||||
|
|
||||||
class AternosFile:
|
class AternosFile:
|
||||||
|
|
||||||
"""File class which contains info about its path, type and size
|
"""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,
|
self,
|
||||||
atserv: 'AternosServer',
|
atserv: 'AternosServer',
|
||||||
path: str, name: str,
|
path: str, rmable: bool,
|
||||||
|
dlable: bool, editable: bool,
|
||||||
ftype: FileType = FileType.file,
|
ftype: FileType = FileType.file,
|
||||||
size: Union[int, float] = 0) -> None:
|
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.atserv = atserv
|
||||||
self._path = path.lstrip('/')
|
|
||||||
self._name = name
|
self._path = path
|
||||||
self._full = path + name
|
self._name = path[path.rfind('/') + 1:]
|
||||||
|
self._dirname = path[:path.rfind('/')]
|
||||||
|
|
||||||
|
self._deleteable = rmable
|
||||||
|
self._downloadable = dlable
|
||||||
|
self._editable = editable
|
||||||
|
|
||||||
self._ftype = ftype
|
self._ftype = ftype
|
||||||
self._size = float(size)
|
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:
|
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',
|
'https://aternos.org/panel/ajax/delete.php',
|
||||||
'POST', data={'file': self._full},
|
'POST', data={'file': self._path},
|
||||||
sendtoken=True
|
sendtoken=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if req.content == b'{"success":false}':
|
||||||
|
raise FileError('Unable to delete the file')
|
||||||
|
|
||||||
def get_content(self) -> bytes:
|
def get_content(self) -> bytes:
|
||||||
|
|
||||||
"""Requests file content in bytes (downloads it)
|
"""Requests file content in bytes (downloads it)
|
||||||
|
|
||||||
:raises FileError: If downloading
|
Raises:
|
||||||
the file is not allowed by Aternos
|
RuntimeWarning: Message about probability of FileError
|
||||||
:return: File content
|
FileError: If downloading this file is disallowed by Aternos
|
||||||
:rtype: bytes
|
|
||||||
|
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(
|
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={
|
||||||
'file': self._full
|
'file': self._path
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if file.content == b'{"success":false}':
|
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
|
return file.content
|
||||||
|
|
||||||
def set_content(self, value: bytes) -> None:
|
def set_content(self, value: bytes) -> None:
|
||||||
|
|
||||||
"""Modifies the file content
|
"""Modifies file content
|
||||||
|
|
||||||
:param value: New content
|
Args:
|
||||||
:type value: bytes
|
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',
|
'https://aternos.org/panel/ajax/save.php',
|
||||||
'POST', data={
|
'POST', data={
|
||||||
'file': self._full,
|
'file': self._path,
|
||||||
'content': value
|
'content': value
|
||||||
}, sendtoken=True
|
}, sendtoken=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if req.content == b'{"success":false}':
|
||||||
|
raise FileError('Unable to save the file')
|
||||||
|
|
||||||
def get_text(self) -> str:
|
def get_text(self) -> str:
|
||||||
|
|
||||||
"""Requests editing the file as a text
|
"""Requests editing the file as a text
|
||||||
(try it if downloading is disallowed)
|
|
||||||
|
|
||||||
:return: File text content
|
Raises:
|
||||||
:rtype: str
|
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(
|
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)
|
edittree = lxml.html.fromstring(editor.content)
|
||||||
|
editblock = edittree.xpath('//div[@id="editor"]')
|
||||||
|
|
||||||
editblock = edittree.xpath('//div[@id="editor"]')[0]
|
if len(editblock) < 1:
|
||||||
return editblock.text_content()
|
raise FileError(
|
||||||
|
'Unable to open editor. '
|
||||||
|
'Try to get file content'
|
||||||
|
)
|
||||||
|
|
||||||
|
return editblock[0].text_content()
|
||||||
|
|
||||||
def set_text(self, value: str) -> None:
|
def set_text(self, value: str) -> None:
|
||||||
|
|
||||||
"""Modifies the file content,
|
"""Modifies the file content,
|
||||||
but unlike set_content takes
|
but unlike `set_content` takes
|
||||||
a string as a new value
|
a string as an argument
|
||||||
|
|
||||||
:param value: New content
|
Args:
|
||||||
:type value: str
|
value (str): New content
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.set_content(value.encode('utf-8'))
|
self.set_content(value.encode('utf-8'))
|
||||||
|
@ -129,11 +234,12 @@ class AternosFile:
|
||||||
@property
|
@property
|
||||||
def path(self) -> str:
|
def path(self) -> str:
|
||||||
|
|
||||||
"""Path to a directory which
|
"""Abslute path to the file
|
||||||
contains the file, without leading slash
|
without leading slash
|
||||||
|
including filename
|
||||||
|
|
||||||
:return: Full path to directory
|
Returns:
|
||||||
:rtype: str
|
Full path to the file
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self._path
|
return self._path
|
||||||
|
@ -141,60 +247,105 @@ class AternosFile:
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
|
|
||||||
"""Filename including extension
|
"""Filename with extension
|
||||||
|
|
||||||
:return: Filename
|
Returns:
|
||||||
:rtype: str
|
Filename
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self._name
|
return self._name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def full(self) -> str:
|
def dirname(self) -> str:
|
||||||
|
|
||||||
"""Absolute path to the file,
|
"""Full path to the directory
|
||||||
without leading slash
|
which contains the file
|
||||||
|
without leading slash.
|
||||||
|
Empty path means root (`/`)
|
||||||
|
|
||||||
:return: Full path
|
Returns:
|
||||||
:rtype: str
|
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
|
@property
|
||||||
def is_dir(self) -> bool:
|
def is_dir(self) -> bool:
|
||||||
|
|
||||||
"""Check if the file object is a directory
|
"""Check if the file object is a directory
|
||||||
|
|
||||||
:return: `True` if the file
|
Returns:
|
||||||
is a directory, otherwise `False`
|
True if it is a directory, otherwise False
|
||||||
:rtype: bool
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self._ftype == FileType.directory:
|
return self._ftype == FileType.dir
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_file(self) -> bool:
|
def is_file(self) -> bool:
|
||||||
|
|
||||||
"""Check if the file object is not a directory
|
"""Check if the file object is not a directory
|
||||||
|
|
||||||
:return: `True` if it is a file, otherwise `False`
|
Returns:
|
||||||
:rtype: bool
|
True if it is a file, otherwise False
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self._ftype == FileType.file:
|
return self._ftype == FileType.file
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def size(self) -> float:
|
def size(self) -> float:
|
||||||
|
|
||||||
"""File size in bytes
|
"""File size in bytes
|
||||||
|
|
||||||
:return: File size
|
Returns:
|
||||||
:rtype: float
|
File size
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self._size
|
return self._size
|
||||||
|
|
|
@ -12,26 +12,32 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
class FileManager:
|
class FileManager:
|
||||||
|
|
||||||
"""Aternos file manager class for viewing files structure
|
"""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:
|
||||||
|
|
||||||
|
"""Aternos file manager class
|
||||||
|
for viewing files structure
|
||||||
|
|
||||||
|
Args:
|
||||||
|
atserv (python_aternos.atserver.AternosServer):
|
||||||
|
atserver.AternosServer instance
|
||||||
|
"""
|
||||||
|
|
||||||
self.atserv = atserv
|
self.atserv = atserv
|
||||||
|
|
||||||
def listdir(self, path: str = '') -> List[AternosFile]:
|
def list_dir(self, path: str = '') -> List[AternosFile]:
|
||||||
|
|
||||||
"""Requests a list of files
|
"""Requests a list of files
|
||||||
in the specified directory
|
in the specified directory
|
||||||
|
|
||||||
:param path: Directory
|
Args:
|
||||||
(an empty string means root), defaults to ''
|
path (str, optional):
|
||||||
:type path: str, optional
|
Directory (an empty string means root)
|
||||||
:return: List of :class:`python_aternos.atfile.AternosFile`
|
|
||||||
:rtype: List[AternosFile]
|
Returns:
|
||||||
|
List of atfile.AternosFile objects
|
||||||
"""
|
"""
|
||||||
|
|
||||||
path = path.lstrip('/')
|
path = path.lstrip('/')
|
||||||
|
@ -42,29 +48,35 @@ class FileManager:
|
||||||
filestree = lxml.html.fromstring(filesreq.content)
|
filestree = lxml.html.fromstring(filesreq.content)
|
||||||
|
|
||||||
fileslist = filestree.xpath(
|
fileslist = filestree.xpath(
|
||||||
'//div[contains(concat(" ",normalize-space(@class)," ")," file ")]'
|
'//div[@class="file" or @class="file clickable"]'
|
||||||
)
|
)
|
||||||
|
|
||||||
files = []
|
files = []
|
||||||
for f in fileslist:
|
for f in fileslist:
|
||||||
|
|
||||||
ftype_raw = f.xpath('@data-type')[0]
|
ftype_raw = f.xpath('@data-type')[0]
|
||||||
ftype = FileType.file \
|
|
||||||
if ftype_raw == 'file' \
|
|
||||||
else FileType.directory
|
|
||||||
|
|
||||||
fsize = self.extract_size(
|
fsize = self.extract_size(
|
||||||
f.xpath('./div[@class="filesize"]')
|
f.xpath('./div[@class="filesize"]')
|
||||||
)
|
)
|
||||||
|
|
||||||
fullpath = f.xpath('@data-path')[0]
|
rm_btn = f.xpath('./div[contains(@class,"js-delete-file")]')
|
||||||
filepath = fullpath[:fullpath.rfind('/')]
|
dl_btn = f.xpath('./div[contains(@class,"js-download-file")]')
|
||||||
filename = fullpath[fullpath.rfind('/'):]
|
clickable = 'clickable' in f.classes
|
||||||
|
is_config = ('server.properties' in path) or ('level.dat' in path)
|
||||||
|
|
||||||
files.append(
|
files.append(
|
||||||
AternosFile(
|
AternosFile(
|
||||||
self.atserv,
|
atserv=self.atserv,
|
||||||
filepath, filename,
|
path=f.xpath('@data-path')[0],
|
||||||
ftype, fsize
|
|
||||||
|
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
|
"""Parses file size from the LXML tree
|
||||||
|
|
||||||
:param fsize_raw: XPath method result
|
Args:
|
||||||
:type fsize_raw: List[Any]
|
fsize_raw (List[Any]): XPath parsing result
|
||||||
:return: File size in bytes
|
|
||||||
:rtype: float
|
Returns:
|
||||||
|
File size in bytes
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if len(fsize_raw) > 0:
|
if len(fsize_raw) > 0:
|
||||||
|
@ -87,7 +100,10 @@ class FileManager:
|
||||||
fsize_msr = fsize_text[fsize_text.rfind(' ') + 1:]
|
fsize_msr = fsize_text[fsize_text.rfind(' ') + 1:]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return self.convert_size(float(fsize_num), fsize_msr)
|
return self.convert_size(
|
||||||
|
float(fsize_num),
|
||||||
|
fsize_msr
|
||||||
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return -1.0
|
return -1.0
|
||||||
|
|
||||||
|
@ -100,12 +116,12 @@ class FileManager:
|
||||||
|
|
||||||
"""Converts "human" file size to size in bytes
|
"""Converts "human" file size to size in bytes
|
||||||
|
|
||||||
:param num: Size
|
Args:
|
||||||
:type num: Union[int,float]
|
num (Union[int,float]): Size
|
||||||
:param measure: Units (B, kB, MB, GB)
|
measure (str): Units (B, kB, MB, GB)
|
||||||
:type measure: str
|
|
||||||
:return: Size in bytes
|
Returns:
|
||||||
:rtype: float
|
Size in bytes
|
||||||
"""
|
"""
|
||||||
|
|
||||||
measure_match = {
|
measure_match = {
|
||||||
|
@ -121,30 +137,35 @@ class FileManager:
|
||||||
"""Returns :class:`python_aternos.atfile.AternosFile`
|
"""Returns :class:`python_aternos.atfile.AternosFile`
|
||||||
instance by its path
|
instance by its path
|
||||||
|
|
||||||
:param path: Path to file including its filename
|
Args:
|
||||||
:type path: str
|
path (str): Path to the file including its filename
|
||||||
:return: _description_
|
|
||||||
:rtype: Optional[AternosFile]
|
Returns:
|
||||||
|
atfile.AternosFile object
|
||||||
|
if file has been found,
|
||||||
|
otherwise None
|
||||||
"""
|
"""
|
||||||
|
|
||||||
filepath = path[:path.rfind('/')]
|
filedir = path[:path.rfind('/')]
|
||||||
filename = path[path.rfind('/'):]
|
filename = path[path.rfind('/'):]
|
||||||
|
|
||||||
filedir = self.listdir(filepath)
|
files = self.list_dir(filedir)
|
||||||
for file in filedir:
|
|
||||||
if file.name == filename:
|
|
||||||
return file
|
|
||||||
|
|
||||||
return None
|
return {
|
||||||
|
'file': f
|
||||||
|
for f in files
|
||||||
|
if f.name == filename
|
||||||
|
}.get('file', None)
|
||||||
|
|
||||||
def dl_file(self, path: str) -> bytes:
|
def dl_file(self, path: str) -> bytes:
|
||||||
|
|
||||||
"""Returns the file content in bytes (downloads it)
|
"""Returns the file content in bytes (downloads it)
|
||||||
|
|
||||||
:param path: Path to file including its filename
|
Args:
|
||||||
:type path: str
|
path (str): Path to file including its filename
|
||||||
:return: File content
|
|
||||||
:rtype: bytes
|
Returns:
|
||||||
|
File content
|
||||||
"""
|
"""
|
||||||
|
|
||||||
file = self.atserv.atserver_request( # type: ignore
|
file = self.atserv.atserver_request( # type: ignore
|
||||||
|
@ -161,10 +182,11 @@ class FileManager:
|
||||||
"""Returns the world zip file content
|
"""Returns the world zip file content
|
||||||
by its name (downloads it)
|
by its name (downloads it)
|
||||||
|
|
||||||
:param world: Name of world, defaults to 'world'
|
Args:
|
||||||
:type world: str, optional
|
world (str, optional): Name of world
|
||||||
:return: Zip file content
|
|
||||||
:rtype: bytes
|
Returns:
|
||||||
|
ZIP file content
|
||||||
"""
|
"""
|
||||||
|
|
||||||
resp = self.atserv.atserver_request( # type: ignore
|
resp = self.atserv.atserver_request( # type: ignore
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
"""Parsing and executing JavaScript code"""
|
"""Parsing and executing JavaScript code"""
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import regex
|
import regex
|
||||||
import js2py
|
import js2py
|
||||||
|
|
||||||
|
@ -13,12 +10,14 @@ 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)
|
"""Converts a ECMA6 function
|
||||||
|
to ECMA5 format (without arrow expressions)
|
||||||
|
|
||||||
:param f: ECMA6 function
|
Args:
|
||||||
:type f: str
|
f (str): ECMA6 function
|
||||||
:return: ECMA5 function
|
|
||||||
:rtype: str
|
Returns:
|
||||||
|
ECMA5 function
|
||||||
"""
|
"""
|
||||||
|
|
||||||
f = regex.sub(r'/\*.+?\*/', '', f)
|
f = regex.sub(r'/\*.+?\*/', '', f)
|
||||||
|
@ -35,23 +34,25 @@ def atob(s: str) -> str:
|
||||||
|
|
||||||
"""Decodes base64 string
|
"""Decodes base64 string
|
||||||
|
|
||||||
:param s: Encoded data
|
Args:
|
||||||
:type s: str
|
s (str): Encoded data
|
||||||
:return: Decoded string
|
|
||||||
:rtype: str
|
Returns:
|
||||||
|
Decoded string
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return base64.standard_b64decode(str(s)).decode('utf-8')
|
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
|
"""Executes a JavaScript function
|
||||||
|
|
||||||
:param f: ECMA6 function
|
Args:
|
||||||
:type f: str
|
f (str): ECMA6 function
|
||||||
:return: JavaScript interpreter context
|
|
||||||
:rtype: Any
|
Returns:
|
||||||
|
JavaScript interpreter context
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ctx = js2py.EvalJs({'atob': atob})
|
ctx = js2py.EvalJs({'atob': atob})
|
||||||
|
|
|
@ -25,26 +25,33 @@ class Lists(enum.Enum):
|
||||||
|
|
||||||
class PlayersList:
|
class PlayersList:
|
||||||
|
|
||||||
"""Class for managing operators, whitelist and banned players lists
|
"""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__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
lst: Union[str, Lists],
|
lst: Union[str, Lists],
|
||||||
atserv: 'AternosServer') -> None:
|
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.atserv = atserv
|
||||||
self.lst = Lists(lst)
|
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)
|
common_whl = (self.lst == Lists.whl)
|
||||||
# 1 is atserver.Edition.bedrock
|
bedrock = (atserv.is_bedrock)
|
||||||
bedrock = (atserv.edition == 1)
|
|
||||||
|
|
||||||
if common_whl and bedrock:
|
if common_whl and bedrock:
|
||||||
self.lst = Lists.whl_be
|
self.lst = Lists.whl_be
|
||||||
|
@ -56,11 +63,12 @@ class PlayersList:
|
||||||
|
|
||||||
"""Parse a players list
|
"""Parse a players list
|
||||||
|
|
||||||
:param cache: If the function can return
|
Args:
|
||||||
cached list (highly recommended), defaults to True
|
cache (bool, optional): If the function should
|
||||||
:type cache: bool, optional
|
return cached list (highly recommended)
|
||||||
:return: List of players nicknames
|
|
||||||
:rtype: List[str]
|
Returns:
|
||||||
|
List of players' nicknames
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if cache and self.parsed:
|
if cache and self.parsed:
|
||||||
|
@ -88,8 +96,8 @@ class PlayersList:
|
||||||
|
|
||||||
"""Appends a player to the list by the nickname
|
"""Appends a player to the list by the nickname
|
||||||
|
|
||||||
:param name: Player's nickname
|
Args:
|
||||||
:type name: str
|
name (str): Player's nickname
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.atserv.atserver_request(
|
self.atserv.atserver_request(
|
||||||
|
@ -106,8 +114,8 @@ class PlayersList:
|
||||||
|
|
||||||
"""Removes a player from the list by the nickname
|
"""Removes a player from the list by the nickname
|
||||||
|
|
||||||
:param name: Player's nickname
|
Args:
|
||||||
:type name: str
|
name (str): Player's nickname
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.atserv.atserver_request(
|
self.atserv.atserver_request(
|
||||||
|
|
|
@ -3,8 +3,10 @@
|
||||||
import enum
|
import enum
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from typing import Optional, List
|
from typing import Optional
|
||||||
from requests import Response
|
from typing import List, Dict, Any
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
from .atconnect import AternosConnect
|
from .atconnect import AternosConnect
|
||||||
from .aterrors import ServerStartError
|
from .aterrors import ServerStartError
|
||||||
|
@ -17,7 +19,7 @@ from .atwss import AternosWss
|
||||||
|
|
||||||
class Edition(enum.IntEnum):
|
class Edition(enum.IntEnum):
|
||||||
|
|
||||||
"""Server edition type enum"""
|
"""Server edition type enum (java, bedrock)"""
|
||||||
|
|
||||||
java = 0
|
java = 0
|
||||||
bedrock = 1
|
bedrock = 1
|
||||||
|
@ -32,32 +34,36 @@ class Status(enum.IntEnum):
|
||||||
|
|
||||||
off = 0
|
off = 0
|
||||||
on = 1
|
on = 1
|
||||||
|
|
||||||
starting = 2
|
starting = 2
|
||||||
shutdown = 3
|
shutdown = 3
|
||||||
unknown = 6
|
|
||||||
|
loading = 6
|
||||||
error = 7
|
error = 7
|
||||||
|
|
||||||
|
preparing = 10
|
||||||
confirm = 10
|
confirm = 10
|
||||||
|
|
||||||
|
|
||||||
class AternosServer:
|
class AternosServer:
|
||||||
|
|
||||||
"""Class for controlling your Aternos Minecraft server
|
"""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
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, servid: str,
|
self, servid: str,
|
||||||
atconn: AternosConnect,
|
atconn: AternosConnect,
|
||||||
reqinfo: bool = True) -> None:
|
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.servid = servid
|
||||||
self.atconn = atconn
|
self.atconn = atconn
|
||||||
if reqinfo:
|
if reqinfo:
|
||||||
|
@ -75,15 +81,17 @@ class AternosServer:
|
||||||
|
|
||||||
def wss(self, autoconfirm: bool = False) -> AternosWss:
|
def wss(self, autoconfirm: bool = False) -> AternosWss:
|
||||||
|
|
||||||
"""Returns :class:`python_aternos.atwss.AternosWss`
|
"""Returns AternosWss instance for
|
||||||
instance for listening server streams in real-time
|
listening server streams in real-time
|
||||||
|
|
||||||
:param autoconfirm: Automatically start server status listener
|
Args:
|
||||||
|
autoconfirm (bool, optional):
|
||||||
|
Automatically start server status listener
|
||||||
when AternosWss connects to API to confirm
|
when AternosWss connects to API to confirm
|
||||||
server launching, defaults to `False`
|
server launching
|
||||||
:type autoconfirm: bool, optional
|
|
||||||
:return: :class:`python_aternos.atwss.AternosWss` object
|
Returns:
|
||||||
:rtype: python_aternos.atwss.AternosWss
|
AternosWss object
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return AternosWss(self, autoconfirm)
|
return AternosWss(self, autoconfirm)
|
||||||
|
@ -95,13 +103,15 @@ class AternosServer:
|
||||||
|
|
||||||
"""Starts a server
|
"""Starts a server
|
||||||
|
|
||||||
:param headstart: Start a server in the headstart mode
|
Args:
|
||||||
which allows you to skip all queue, defaults to `False`
|
headstart (bool, optional): Start a server in
|
||||||
:type headstart: bool, optional
|
the headstart mode which allows
|
||||||
:param accepteula: Automatically accept
|
you to skip all queue
|
||||||
the Mojang EULA, defaults to `True`
|
accepteula (bool, optional):
|
||||||
:type accepteula: bool, optional
|
Automatically accept the Mojang EULA
|
||||||
:raises ServerStartError: When Aternos
|
|
||||||
|
Raises:
|
||||||
|
ServerStartError: When Aternos
|
||||||
is unable to start the server
|
is unable to start the server
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -171,66 +181,64 @@ class AternosServer:
|
||||||
|
|
||||||
def files(self) -> FileManager:
|
def files(self) -> FileManager:
|
||||||
|
|
||||||
"""Returns :class:`python_aternos.atfm.FileManager`
|
"""Returns FileManager instance
|
||||||
instance for file operations
|
for file operations
|
||||||
|
|
||||||
:return: :class:`python_aternos.atfm.FileManager` object
|
Returns:
|
||||||
:rtype: python_aternos.atfm.FileManager
|
FileManager object
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return FileManager(self)
|
return FileManager(self)
|
||||||
|
|
||||||
def config(self) -> AternosConfig:
|
def config(self) -> AternosConfig:
|
||||||
|
|
||||||
"""Returns :class:`python_aternos.atconf.AternosConfig`
|
"""Returns AternosConfig instance
|
||||||
instance for editing server settings
|
for editing server settings
|
||||||
|
|
||||||
:return: :class:`python_aternos.atconf.AternosConfig` object
|
Returns:
|
||||||
:rtype: python_aternos.atconf.AternosConfig
|
AternosConfig object
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return AternosConfig(self)
|
return AternosConfig(self)
|
||||||
|
|
||||||
def players(self, lst: Lists) -> PlayersList:
|
def players(self, lst: Lists) -> PlayersList:
|
||||||
|
|
||||||
"""Returns :class:`python_aternos.atplayers.PlayersList`
|
"""Returns PlayersList instance
|
||||||
instance for managing operators, whitelist and banned players lists
|
for managing operators, whitelist
|
||||||
|
and banned players lists
|
||||||
|
|
||||||
:param lst: Players list type, must be
|
Args:
|
||||||
the :class:`python_aternos.atplayers.Lists` enum value
|
lst (Lists): Players list type,
|
||||||
:type lst: python_aternos.atplayers.Lists
|
must be the atplayers.Lists enum value
|
||||||
:return: :class:`python_aternos.atplayers.PlayersList`
|
|
||||||
:rtype: python_aternos.atplayers.PlayersList
|
Returns:
|
||||||
|
PlayersList object
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return PlayersList(lst, self)
|
return PlayersList(lst, self)
|
||||||
|
|
||||||
def atserver_request(
|
def atserver_request(
|
||||||
self, url: str, method: str,
|
self, url: str, method: str,
|
||||||
params: Optional[dict] = None,
|
params: Optional[Dict[Any, Any]] = None,
|
||||||
data: Optional[dict] = None,
|
data: Optional[Dict[Any, Any]] = None,
|
||||||
headers: Optional[dict] = None,
|
headers: Optional[Dict[Any, Any]] = None,
|
||||||
sendtoken: bool = False) -> Response:
|
sendtoken: bool = False) -> requests.Response:
|
||||||
|
|
||||||
"""Sends a request to Aternos API
|
"""Sends a request to Aternos API
|
||||||
with server IDenitfier parameter
|
with server IDenitfier parameter
|
||||||
|
|
||||||
:param url: Request URL
|
Args:
|
||||||
:type url: str
|
url (str): Request URL
|
||||||
:param method: Request method, must be GET or POST
|
method (str): Request method, must be GET or POST
|
||||||
:type method: str
|
params (Optional[Dict[Any, Any]], optional): URL parameters
|
||||||
:param params: URL parameters, defaults to None
|
data (Optional[Dict[Any, Any]], optional): POST request data,
|
||||||
:type params: Optional[dict], optional
|
if the method is GET, this dict
|
||||||
:param data: POST request data, if the method is GET,
|
will be combined with params
|
||||||
this dict will be combined with params, defaults to None
|
headers (Optional[Dict[Any, Any]], optional): Custom headers
|
||||||
:type data: Optional[dict], optional
|
sendtoken (bool, optional): If the ajax and SEC token should be sent
|
||||||
:param headers: Custom headers, defaults to None
|
|
||||||
:type headers: Optional[dict], optional
|
Returns:
|
||||||
:param sendtoken: If the ajax and SEC token
|
API response
|
||||||
should be sent, defaults to False
|
|
||||||
:type sendtoken: bool, optional
|
|
||||||
:return: API response
|
|
||||||
:rtype: requests.Response
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self.atconn.request_cloudflare(
|
return self.atconn.request_cloudflare(
|
||||||
|
@ -246,10 +254,11 @@ class AternosServer:
|
||||||
@property
|
@property
|
||||||
def subdomain(self) -> str:
|
def subdomain(self) -> str:
|
||||||
|
|
||||||
"""Server subdomain (part of domain before `.aternos.me`)
|
"""Server subdomain
|
||||||
|
(the part of domain before `.aternos.me`)
|
||||||
|
|
||||||
:return: Subdomain
|
Returns:
|
||||||
:rtype: str
|
Subdomain
|
||||||
"""
|
"""
|
||||||
|
|
||||||
atdomain = self.domain
|
atdomain = self.domain
|
||||||
|
@ -258,10 +267,10 @@ class AternosServer:
|
||||||
@subdomain.setter
|
@subdomain.setter
|
||||||
def subdomain(self, value: str) -> None:
|
def subdomain(self, value: str) -> None:
|
||||||
|
|
||||||
"""Set new subdomain for your server
|
"""Set a new subdomain for your server
|
||||||
|
|
||||||
:param value: Subdomain
|
Args:
|
||||||
:type value: str
|
value (str): Subdomain
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.atserver_request(
|
self.atserver_request(
|
||||||
|
@ -273,11 +282,12 @@ class AternosServer:
|
||||||
@property
|
@property
|
||||||
def motd(self) -> str:
|
def motd(self) -> str:
|
||||||
|
|
||||||
"""Server message of the day,
|
"""Server message of the day
|
||||||
which is shown below its name in the servers list
|
which is shown below its name
|
||||||
|
in the Minecraft servers list
|
||||||
|
|
||||||
:return: MOTD
|
Returns:
|
||||||
:rtype: str
|
MOTD
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self._info['motd']
|
return self._info['motd']
|
||||||
|
@ -285,10 +295,10 @@ class AternosServer:
|
||||||
@motd.setter
|
@motd.setter
|
||||||
def motd(self, value: str) -> None:
|
def motd(self, value: str) -> None:
|
||||||
|
|
||||||
"""Set new message of the day
|
"""Set a new message of the day
|
||||||
|
|
||||||
:param value: MOTD
|
Args:
|
||||||
:type value: str
|
value (str): New MOTD
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.atserver_request(
|
self.atserver_request(
|
||||||
|
@ -300,10 +310,11 @@ class AternosServer:
|
||||||
@property
|
@property
|
||||||
def address(self) -> str:
|
def address(self) -> str:
|
||||||
|
|
||||||
"""Full server address including domain and port
|
"""Full server address
|
||||||
|
including domain and port
|
||||||
|
|
||||||
:return: Server address
|
Returns:
|
||||||
:rtype: str
|
Server address
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self._info['displayAddress']
|
return self._info['displayAddress']
|
||||||
|
@ -311,11 +322,11 @@ class AternosServer:
|
||||||
@property
|
@property
|
||||||
def domain(self) -> str:
|
def domain(self) -> str:
|
||||||
|
|
||||||
"""Server domain (test.aternos.me),
|
"""Server domain (e.g. `test.aternos.me`).
|
||||||
address without port number
|
In other words, address without port number
|
||||||
|
|
||||||
:return: Domain
|
Returns:
|
||||||
:rtype: str
|
Domain
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self._info['ip']
|
return self._info['ip']
|
||||||
|
@ -325,8 +336,8 @@ class AternosServer:
|
||||||
|
|
||||||
"""Server port number
|
"""Server port number
|
||||||
|
|
||||||
:return: Port
|
Returns:
|
||||||
:rtype: int
|
Port
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self._info['port']
|
return self._info['port']
|
||||||
|
@ -336,20 +347,42 @@ class AternosServer:
|
||||||
|
|
||||||
"""Server software edition: Java or Bedrock
|
"""Server software edition: Java or Bedrock
|
||||||
|
|
||||||
:return: Software edition
|
Returns:
|
||||||
:rtype: Edition
|
Software edition
|
||||||
"""
|
"""
|
||||||
|
|
||||||
soft_type = self._info['bedrock']
|
soft_type = self._info['bedrock']
|
||||||
return Edition(soft_type)
|
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
|
@property
|
||||||
def software(self) -> str:
|
def software(self) -> str:
|
||||||
|
|
||||||
"""Server software name (e.g. `Vanilla`)
|
"""Server software name (e.g. `Vanilla`)
|
||||||
|
|
||||||
:return: Software name
|
Returns:
|
||||||
:rtype: str
|
Software name
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self._info['software']
|
return self._info['software']
|
||||||
|
@ -357,33 +390,50 @@ class AternosServer:
|
||||||
@property
|
@property
|
||||||
def version(self) -> str:
|
def version(self) -> str:
|
||||||
|
|
||||||
"""Server software version (e.g. `1.16.5`)
|
"""Server software version (1.16.5)
|
||||||
|
|
||||||
:return: Software version
|
Returns:
|
||||||
:rtype: str
|
Software version
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self._info['version']
|
return self._info['version']
|
||||||
|
|
||||||
@property
|
@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
|
Returns:
|
||||||
:rtype: str
|
CSS class
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self._info['class']
|
return self._info['class']
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def status_num(self) -> int:
|
def status(self) -> str:
|
||||||
|
|
||||||
"""Server numeric status. It is highly recommended
|
"""Server status string
|
||||||
to use status string instead of a number.
|
(offline, loading, preparing)
|
||||||
|
|
||||||
:return: Status code
|
Returns:
|
||||||
:rtype: Status
|
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'])
|
return Status(self._info['status'])
|
||||||
|
@ -391,10 +441,10 @@ class AternosServer:
|
||||||
@property
|
@property
|
||||||
def players_list(self) -> List[str]:
|
def players_list(self) -> List[str]:
|
||||||
|
|
||||||
"""List of connected players nicknames
|
"""List of connected players' nicknames
|
||||||
|
|
||||||
:return: Connected players
|
Returns:
|
||||||
:rtype: List[str]
|
Connected players
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self._info['playerlist']
|
return self._info['playerlist']
|
||||||
|
@ -402,10 +452,10 @@ class AternosServer:
|
||||||
@property
|
@property
|
||||||
def players_count(self) -> int:
|
def players_count(self) -> int:
|
||||||
|
|
||||||
"""How many connected players
|
"""How many players are connected
|
||||||
|
|
||||||
:return: Connected players count
|
Returns:
|
||||||
:rtype: int
|
Connected players count
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return int(self._info['players'])
|
return int(self._info['players'])
|
||||||
|
@ -413,10 +463,11 @@ class AternosServer:
|
||||||
@property
|
@property
|
||||||
def slots(self) -> int:
|
def slots(self) -> int:
|
||||||
|
|
||||||
"""Server slots, how many players can connect
|
"""Server slots, how many
|
||||||
|
players **can** connect
|
||||||
|
|
||||||
:return: Slots count
|
Returns:
|
||||||
:rtype: int
|
Slots count
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return int(self._info['slots'])
|
return int(self._info['slots'])
|
||||||
|
@ -426,8 +477,8 @@ class AternosServer:
|
||||||
|
|
||||||
"""Server used RAM in MB
|
"""Server used RAM in MB
|
||||||
|
|
||||||
:return: Used RAM
|
Returns:
|
||||||
:rtype: int
|
Used RAM
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return int(self._info['ram'])
|
return int(self._info['ram'])
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
"""Connects to Aternos API websocket
|
"""Connects to Aternos WebSocket API
|
||||||
for real-time information"""
|
for real-time information"""
|
||||||
|
|
||||||
import enum
|
import enum
|
||||||
|
@ -35,27 +35,31 @@ class Streams(enum.Enum):
|
||||||
none = (-1, None)
|
none = (-1, None)
|
||||||
|
|
||||||
def __init__(self, num: int, stream: str) -> None:
|
def __init__(self, num: int, stream: str) -> None:
|
||||||
|
|
||||||
self.num = num
|
self.num = num
|
||||||
self.stream = stream
|
self.stream = stream
|
||||||
|
|
||||||
|
|
||||||
class AternosWss:
|
class AternosWss:
|
||||||
|
|
||||||
"""Class for managing websocket connection
|
"""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__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
atserv: 'AternosServer',
|
atserv: 'AternosServer',
|
||||||
autoconfirm: bool = False) -> None:
|
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.atserv = atserv
|
||||||
self.servid = atserv.servid
|
self.servid = atserv.servid
|
||||||
|
|
||||||
|
@ -73,7 +77,9 @@ class AternosWss:
|
||||||
|
|
||||||
async def confirm(self) -> None:
|
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()
|
self.atserv.confirm()
|
||||||
|
|
||||||
|
@ -86,12 +92,12 @@ class AternosWss:
|
||||||
When websocket receives message from the specified stream,
|
When websocket receives message from the specified stream,
|
||||||
it calls all listeners created with this decorator.
|
it calls all listeners created with this decorator.
|
||||||
|
|
||||||
:param stream: Stream that your function should listen
|
Args:
|
||||||
:type stream: python_aternos.atwss.Streams
|
stream (Streams): Stream that your function should listen
|
||||||
:param args: Arguments which will be passed to your function
|
*args (tuple, optional): Arguments which will be passed to your function
|
||||||
:type args: tuple, optional
|
|
||||||
:return: ...
|
Returns:
|
||||||
:rtype: Callable[[Callable[[Any], Coroutine[Any, Any, None]]], Any]
|
...
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(func: FunctionT) -> None:
|
def decorator(func: FunctionT) -> None:
|
||||||
|
@ -100,7 +106,8 @@ class AternosWss:
|
||||||
|
|
||||||
async def connect(self) -> None:
|
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 = [
|
headers = [
|
||||||
('Host', 'aternos.org'),
|
('Host', 'aternos.org'),
|
||||||
|
@ -118,9 +125,14 @@ class AternosWss:
|
||||||
)
|
)
|
||||||
|
|
||||||
@self.wssreceiver(Streams.status)
|
@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:
|
if not self.autoconfirm:
|
||||||
return
|
return
|
||||||
|
@ -130,13 +142,14 @@ class AternosWss:
|
||||||
confirmation = in_queue and pending
|
confirmation = in_queue and pending
|
||||||
|
|
||||||
if confirmation and not self.confirmed:
|
if confirmation and not self.confirmed:
|
||||||
self.confirm()
|
await self.confirm()
|
||||||
|
|
||||||
@self.wssreceiver(Streams.status)
|
@self.wssreceiver(Streams.status)
|
||||||
async def streamsfunc(msg):
|
async def streamsfunc(msg: Dict[str, Any]) -> None:
|
||||||
|
|
||||||
"""Automatically starts streams. Detailed description:
|
"""Automatically starts streams. Detailed description:
|
||||||
|
|
||||||
|
https://github.com/DarkCat09/python-aternos/issues/22#issuecomment-1146788496
|
||||||
According to the websocket messages from the web site,
|
According to the websocket messages from the web site,
|
||||||
Aternos can't receive any data from a stream (e.g. console) until
|
Aternos can't receive any data from a stream (e.g. console) until
|
||||||
it requests this stream via the special message
|
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
|
these data is sent from API by default, so there's None value in
|
||||||
the second item of its stream type tuple
|
the second item of its stream type tuple
|
||||||
(`<Streams.status: (0, None)>`).
|
(`<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:
|
if msg['status'] == 2:
|
||||||
|
@ -159,7 +174,7 @@ class AternosWss:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if strm.stream:
|
if strm.stream:
|
||||||
logging.debug(f'Enabling {strm.stream} stream')
|
logging.debug(f'Requesting {strm.stream} stream')
|
||||||
await self.send({
|
await self.send({
|
||||||
'stream': strm.stream,
|
'stream': strm.stream,
|
||||||
'type': 'start'
|
'type': 'start'
|
||||||
|
@ -180,8 +195,9 @@ class AternosWss:
|
||||||
|
|
||||||
"""Sends a message to websocket server
|
"""Sends a message to websocket server
|
||||||
|
|
||||||
:param obj: Message, may be a string or a dict
|
Args:
|
||||||
:type obj: Union[Dict[str, Any],str]
|
obj (Union[Dict[str, Any],str]):
|
||||||
|
Message, may be a string or a dict
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if isinstance(obj, dict):
|
if isinstance(obj, dict):
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -5,7 +5,7 @@ with open('README.md', 'rt') as readme:
|
||||||
|
|
||||||
setuptools.setup(
|
setuptools.setup(
|
||||||
name='python-aternos',
|
name='python-aternos',
|
||||||
version='1.1.2',
|
version='2.0.1',
|
||||||
author='Chechkenev Andrey (@DarkCat09)',
|
author='Chechkenev Andrey (@DarkCat09)',
|
||||||
author_email='aacd0709@mail.ru',
|
author_email='aacd0709@mail.ru',
|
||||||
description='An unofficial Aternos API',
|
description='An unofficial Aternos API',
|
||||||
|
|
|
@ -68,3 +68,7 @@ class TestJs2Py(unittest.TestCase):
|
||||||
ctx = atjsparse.exec_js(f)
|
ctx = atjsparse.exec_js(f)
|
||||||
res = ctx.window['AJAX_TOKEN']
|
res = ctx.window['AJAX_TOKEN']
|
||||||
self.assertEqual(res, self.results[i])
|
self.assertEqual(res, self.results[i])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
|
|
|
@ -26,5 +26,14 @@ class TestLogin(unittest.TestCase):
|
||||||
at = Client.from_hashed(
|
at = Client.from_hashed(
|
||||||
AUTH_USER, AUTH_MD5
|
AUTH_USER, AUTH_MD5
|
||||||
)
|
)
|
||||||
srvs = len(at.list_servers())
|
|
||||||
|
srvs = len(
|
||||||
|
at.list_servers(
|
||||||
|
cache=False
|
||||||
|
)
|
||||||
|
)
|
||||||
self.assertTrue(srvs > 0)
|
self.assertTrue(srvs > 0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
|
|
Reference in a new issue