mirror of
https://github.com/deesiigneer/pyspapi.git
synced 2026-04-20 12:35:26 +00:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c08394addf | ||
|
|
63ee509067 | ||
|
|
4ecafef192 | ||
|
|
d44199c4cc | ||
|
|
056ebce615 | ||
|
|
b00b2b76cd | ||
|
|
da86771e3c | ||
|
|
479a02b95f | ||
|
|
5c905ea097 | ||
|
|
83d9663da3 | ||
|
|
93760d2d87 | ||
|
|
108d30ebac | ||
|
|
b4ddd1fd93 | ||
|
|
5449659566 | ||
|
|
85ef993011 | ||
|
|
8e6664bc51 | ||
|
|
0d46f0a126 | ||
|
|
f5484d3900 | ||
|
|
1c35daf142 | ||
|
|
91ed017ac6 | ||
|
|
1806ba7929 | ||
|
|
f4623e9204 | ||
|
|
906060a4b7 | ||
|
|
6bf342a9f1 | ||
|
|
a6cbf2adcc | ||
|
|
59d43736c0 | ||
|
|
9429c443c4 | ||
|
|
152b5272c1 | ||
|
|
81d8b1de35 | ||
|
|
0667922c8c | ||
|
|
9a61797253 | ||
|
|
c9c1bc8e81 | ||
|
|
f551a860bc | ||
|
|
1065b5748b | ||
|
|
1d6af2b8ec | ||
|
|
f244e3f4db | ||
|
|
76dc15a332 | ||
|
|
a2a4ac9fab | ||
|
|
ef40d83a9c | ||
|
|
4a611396a1 | ||
|
|
7cc708ee03 | ||
|
|
8cba25a754 |
38
.github/workflows/python-publish.yml
vendored
Normal file
38
.github/workflows/python-publish.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
# This workflow will upload a Python Package using Twine when a release is created
|
||||
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
|
||||
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
name: Upload to PYPI
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
regular:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [ 3.9 ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install twine
|
||||
- name: Compile package
|
||||
run: |
|
||||
python3 setup.py sdist
|
||||
- name: Publish package
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
19
.readthedocs.yml
Normal file
19
.readthedocs.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
version: 2
|
||||
formats: []
|
||||
|
||||
build:
|
||||
image: latest
|
||||
|
||||
sphinx:
|
||||
configuration: docs/conf.py
|
||||
fail_on_warning: false
|
||||
builder: html
|
||||
|
||||
python:
|
||||
version: "3.8"
|
||||
install:
|
||||
- method: pip
|
||||
path: .
|
||||
extra_requirements:
|
||||
- docs
|
||||
- requirements: docs/requirements.txt
|
||||
3
MANIFEST.in
Normal file
3
MANIFEST.in
Normal file
@@ -0,0 +1,3 @@
|
||||
include README.rst
|
||||
include LICENSE
|
||||
include requirements.txt
|
||||
87
README.md
87
README.md
@@ -1,87 +0,0 @@
|
||||
# pyspapi
|
||||
Фреймворк [API](https://github.com/sp-worlds/api-docs) для серверов СП
|
||||
|
||||
## Установка
|
||||
**Требуется *Python 3.7* или выше**
|
||||
|
||||
*Windows*
|
||||
```commandline
|
||||
> pip install pyspapi
|
||||
```
|
||||
*Linux*
|
||||
```commandline
|
||||
$ sudo apt pip3 install pyspapi
|
||||
```
|
||||
|
||||
## Примеры
|
||||
### [Оплата](https://github.com/sp-worlds/api-docs/blob/main/PAYMENTS.md)
|
||||
|
||||
```Python
|
||||
import spapi
|
||||
|
||||
api = spapi.Api(card_id='CARD_ID',
|
||||
token='TOKEN')
|
||||
|
||||
print(api.payment(amount=1,
|
||||
redirecturl='https://www.google.com/',
|
||||
webhookurl='https://www.yourwebhook.com/',
|
||||
data='Какие-то данные'
|
||||
)
|
||||
)
|
||||
|
||||
```
|
||||
- `amount` - Стоимость покупки в АРах
|
||||
- `redirectUrl` - URL страницы, на которую попадет пользователь после оплаты
|
||||
- `webhookUrl` - URL, куда наш сервер направит запрос, чтобы оповестить ваш сервер об успешной оплате
|
||||
- `data` - Строка до 100 символов, сюда можно пометить любые полезные данных.
|
||||
#### Получение данных об успешной оплате
|
||||
После успешной оплаты на URL указанный в `webhookUrl` придет POST запрос.
|
||||
|
||||
*Тело запроса будет в формате JSON:*
|
||||
|
||||
- `payer` - Ник игрока, который совершил оплату
|
||||
- `amount` - Стоимость покупки
|
||||
- `data` - Данные, которые вы отдали при создании запроса на оплату
|
||||
|
||||
### [Переводы](https://github.com/sp-worlds/api-docs/blob/main/TRANSACTIONS.md)
|
||||
|
||||
```Python
|
||||
import spapi
|
||||
|
||||
api = spapi.Api(card_id='CARD_ID',
|
||||
token='TOKEN')
|
||||
|
||||
print(api.transaction(receiver='12345',
|
||||
amount=1,
|
||||
comment="test"
|
||||
)
|
||||
)
|
||||
|
||||
```
|
||||
- `receiver` - Номер карты получателя
|
||||
- `amount` - Количество АР для перевода
|
||||
- `comment` - Комментарий к переводу
|
||||
-
|
||||
### [Проверка наличия проходки](https://github.com/sp-worlds/api-docs/blob/main/USERS.md)
|
||||
|
||||
```Python
|
||||
import spapi
|
||||
|
||||
api = spapi.Api(card_id='CARD_ID',
|
||||
token='TOKEN')
|
||||
|
||||
print(api.check_user(discord_user_id=123456789012345678)
|
||||
)
|
||||
|
||||
```
|
||||
- `discord_user_id` - ID пользователя в Discord.
|
||||
|
||||
*В ответ вы получите JSON:*
|
||||
|
||||
- `username` - Ник пользователя или null, если у пользователя нет входа на сервер.
|
||||
|
||||
|
||||
## Ссылки
|
||||
|
||||
- [Discord сервер разработчика](https://discord.gg/sJYtYnhN)
|
||||
- [Документация API сайтов СП](https://github.com/sp-worlds/api-docs)
|
||||
62
README.rst
Normal file
62
README.rst
Normal file
@@ -0,0 +1,62 @@
|
||||
.. image:: https://raw.githubusercontent.com/deesiigneer/pyspapi/main/assets/repo-banner.png
|
||||
:alt: pyspapi
|
||||
|
||||
.. image:: https://img.shields.io/discord/850091193190973472?color=5865F2&label=discord
|
||||
:target: https://discord.gg/VbyHaKRAaN
|
||||
:alt: Discord server invite
|
||||
.. image:: https://img.shields.io/github/v/release/deesiigneer/pyspapi?include_prereleases&label=github%20release
|
||||
:target: https://github.com/deesiigneer/pyspapi/
|
||||
:alt: GitHub release (latest by date including pre-releases)
|
||||
.. image:: https://img.shields.io/pypi/v/pyspapi.svg
|
||||
:target: https://pypi.org/project/pyspapi/
|
||||
:alt: PyPI downloads info
|
||||
.. image:: https://img.shields.io/pypi/dm/pyspapi?color=informational&label=pypi%20downloads
|
||||
:target: https://pypi.org/project/pyspapi/
|
||||
:alt: PyPI version info
|
||||
.. image:: https://img.shields.io/readthedocs/pyspapi
|
||||
:target: https://pyspapi.readthedocs.io/
|
||||
:alt: pyspapi documentation
|
||||
|
||||
pyspapi
|
||||
========
|
||||
|
||||
`API <https://github.com/sp-worlds/api-docs>`_ wrapper for SP servers written in Python.
|
||||
|
||||
Installation
|
||||
-------------
|
||||
**Requires Python 3.8 or higher**
|
||||
|
||||
*Windows*
|
||||
|
||||
|
||||
.. code:: sh
|
||||
|
||||
pip install pyspapi
|
||||
|
||||
*Linux/macOS*
|
||||
|
||||
.. code:: sh
|
||||
|
||||
sudo apt pip3 install pyspapi
|
||||
|
||||
Quick example
|
||||
--------------
|
||||
|
||||
Checking the balance
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
.. code:: py
|
||||
|
||||
import pyspapi
|
||||
|
||||
print(await pyspapi.API(card_id='card_id', token='token').balance)
|
||||
|
||||
More examples can be found in the `examples <https://github.com/deesiigneer/pyspapi/tree/main/examples>`_
|
||||
|
||||
Links
|
||||
------
|
||||
|
||||
- `Discord server <https://discord.gg/VbyHaKRAaN>`_
|
||||
- `pyspapi documentation <https://pyspapi.readthedocs.io/>`_
|
||||
- `API documentation <https://spworlds.readthedocs.io>`_
|
||||
- `PyPi <https://pypi.org/project/pyspapi/>`_
|
||||
- `API documentation for SP sites <https://github.com/sp-worlds/api-docs>`_
|
||||
BIN
assets/logo.png
Normal file
BIN
assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
BIN
assets/pyspapi_repo_banner.png
Normal file
BIN
assets/pyspapi_repo_banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
BIN
assets/repo-banner.png
Normal file
BIN
assets/repo-banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
20
docs/Makefile
Normal file
20
docs/Makefile
Normal file
@@ -0,0 +1,20 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line, and also
|
||||
# from the environment for the first two.
|
||||
SPHINXOPTS ?=
|
||||
SPHINXBUILD ?= sphinx-build
|
||||
SOURCEDIR = source
|
||||
BUILDDIR = build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
BIN
docs/_static/404.png
vendored
Normal file
BIN
docs/_static/404.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
19
docs/_static/custom.css
vendored
Normal file
19
docs/_static/custom.css
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
/* Background of stable should be green */
|
||||
#version_switcher a[data-version-name*="stable"] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#version_switcher a[data-version-name*="stable"] span {
|
||||
color: var(--pst-color-success);
|
||||
}
|
||||
|
||||
#version_switcher a[data-version-name*="stable"] span:before {
|
||||
content: "";
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background-color: var(--pst-color-success);
|
||||
opacity: 0.1;
|
||||
}
|
||||
12
docs/_static/switcher.json
vendored
Normal file
12
docs/_static/switcher.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
[
|
||||
{
|
||||
"name": "latest",
|
||||
"version": "latest",
|
||||
"url": "https://pyspapi.readthedocs.io/en/latest/"
|
||||
},
|
||||
{
|
||||
"name": "stable",
|
||||
"version": "stable",
|
||||
"url": "https://pyspapi.readthedocs.io/en/stable/"
|
||||
}
|
||||
]
|
||||
74
docs/api.rst
Normal file
74
docs/api.rst
Normal file
@@ -0,0 +1,74 @@
|
||||
.. py:currentmodule:: pyspapi
|
||||
|
||||
API Reference
|
||||
===============
|
||||
|
||||
The following section outlines the API of pyspapi.
|
||||
|
||||
Version Info
|
||||
---------------------
|
||||
|
||||
There are two main ways to query version information.
|
||||
|
||||
.. data:: version_info
|
||||
|
||||
A named tuple that is similar to :obj:`py:sys.version_info`.
|
||||
|
||||
Just like :obj:`py:sys.version_info` the valid values for ``releaselevel`` are
|
||||
'alpha', 'beta', 'candidate' and 'final'.
|
||||
|
||||
.. data:: __version__
|
||||
|
||||
A string representation of the version.
|
||||
|
||||
``pyspapi``
|
||||
-----------
|
||||
|
||||
``SPAPI``
|
||||
~~~~~
|
||||
.. autoclass:: SPAPI
|
||||
:members:
|
||||
|
||||
.. automethod:: SPAPI.event()
|
||||
:decorator:
|
||||
|
||||
.. automethod:: SPAPI.check_user_access
|
||||
:decorator:
|
||||
|
||||
.. automethod:: SPAPI.get_user
|
||||
:decorator:
|
||||
|
||||
.. automethod:: SPAPI.get_users
|
||||
:decorator:
|
||||
|
||||
.. automethod:: SPAPI.payment
|
||||
:decorator:
|
||||
|
||||
.. automethod:: SPAPI.transaction
|
||||
:decorator:
|
||||
|
||||
.. automethod:: SPAPI.webhook_verify
|
||||
:decorator:
|
||||
|
||||
MojangAPI
|
||||
~~~~~
|
||||
.. autoclass:: MojangAPI
|
||||
:members:
|
||||
|
||||
.. automethod:: SPAPI.event()
|
||||
:decorator:
|
||||
|
||||
.. automethod:: SPAPI.get_name_history
|
||||
:decorator:
|
||||
|
||||
.. automethod:: SPAPI.get_profile
|
||||
:decorator:
|
||||
|
||||
.. automethod:: SPAPI.get_username
|
||||
:decorator:
|
||||
|
||||
.. automethod:: SPAPI.get_uuid
|
||||
:decorator:
|
||||
|
||||
.. automethod:: SPAPI.get_uuids
|
||||
:decorator:
|
||||
76
docs/conf.py
Normal file
76
docs/conf.py
Normal file
@@ -0,0 +1,76 @@
|
||||
from re import search, MULTILINE
|
||||
|
||||
project = 'pyspapi'
|
||||
copyright = '2022, deesiigneer'
|
||||
author = 'deesiigneer'
|
||||
with open("../pyspapi/__init__.py") as f:
|
||||
match = search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', f.read(), MULTILINE)
|
||||
|
||||
if not match or match.group(1) is None:
|
||||
raise RuntimeError("The version could not be resolved")
|
||||
|
||||
version = match.group(1)
|
||||
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = version
|
||||
|
||||
# -- General configuration
|
||||
|
||||
extensions = [
|
||||
'sphinx.ext.duration',
|
||||
'sphinx.ext.doctest',
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.autosummary',
|
||||
'sphinx.ext.intersphinx',
|
||||
]
|
||||
|
||||
intersphinx_mapping = {
|
||||
'python': ('https://docs.python.org/3/', None),
|
||||
'sphinx': ('https://www.sphinx-doc.org/en/master/', None),
|
||||
}
|
||||
intersphinx_disabled_domains = ['std']
|
||||
language = None
|
||||
locale_dirs = ["locale/"]
|
||||
exclude_patterns = []
|
||||
html_static_path = ["_static"]
|
||||
html_theme = "pydata_sphinx_theme"
|
||||
html_logo = "./images/logo.png"
|
||||
html_favicon = "./images/logo.ico"
|
||||
html_theme_options = {
|
||||
"external_links": [
|
||||
{
|
||||
"url": "https://github.com/deesiigneer/pyspapi/releases",
|
||||
"name": "Changelog",
|
||||
}
|
||||
],
|
||||
"icon_links": [
|
||||
{
|
||||
"name": "GitHub",
|
||||
"url": "https://github.com/deesiigneer/pyspapi",
|
||||
"icon": "fab fa-brands fa-github",
|
||||
"type": "fontawesome"
|
||||
},
|
||||
{
|
||||
"name": "Discord",
|
||||
"url": "https://discord.gg/VbyHaKRAaN",
|
||||
"icon": "fab fa-brands fa-discord",
|
||||
"type": "fontawesome"
|
||||
},
|
||||
{
|
||||
"name": "PyPi",
|
||||
"url": "https://pypi.org/project/pyspapi/",
|
||||
"icon": "fab fa-brands fa-python",
|
||||
"type": "fontawesome"
|
||||
}
|
||||
],
|
||||
"header_links_before_dropdown": 4,
|
||||
"show_toc_level": 1,
|
||||
"navbar_start": ["navbar-logo", "version-switcher"],
|
||||
"switcher": {
|
||||
"json_url": "https://pyspapi.readthedocs.io/en/latest/_static/switcher.json",
|
||||
"version_match": "latest"
|
||||
},
|
||||
"navigation_with_keys": True,
|
||||
}
|
||||
html_css_files = ["custom.css"]
|
||||
|
||||
BIN
docs/images/logo.ico
Normal file
BIN
docs/images/logo.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.3 KiB |
BIN
docs/images/logo.png
Normal file
BIN
docs/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
35
docs/index.rst
Normal file
35
docs/index.rst
Normal file
@@ -0,0 +1,35 @@
|
||||
:theme_html_remove_secondary_sidebar:
|
||||
|
||||
Welcome to pyspapi
|
||||
===================
|
||||
|
||||
API wrapper for SP servers written in Python.
|
||||
|
||||
Getting started
|
||||
---------------
|
||||
|
||||
Is this your first time using the library? This is the place to get started!
|
||||
|
||||
- **First steps:** :ref:`Quickstart <quickstart>`
|
||||
- **Examples:** Many examples are available in the `examples directory <https://github.com/deesiigneer/pyspapi/tree/main/examples/>`_.
|
||||
|
||||
Getting help
|
||||
------------
|
||||
|
||||
If you're having trouble with something, these resources might help.
|
||||
|
||||
- Ask questions in `Discord <https://discord.gg/VbyHaKRAaN>`_ server.
|
||||
- If you're looking for something specific, try the :ref:`searching <search>`.
|
||||
- Report bugs in the `issue tracker <https://github.com/deesiigneer/pyspapi/issues>`_.
|
||||
- Ask in `GitHub discussions page <https://github.com/deesiigneer/pyspapi/discussions>`_.
|
||||
|
||||
Manuals
|
||||
-------
|
||||
|
||||
These pages go into great detail about everything the API can do.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
api
|
||||
quickstart
|
||||
35
docs/make.bat
Normal file
35
docs/make.bat
Normal file
@@ -0,0 +1,35 @@
|
||||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=source
|
||||
set BUILDDIR=build
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
%SPHINXBUILD% >NUL 2>NUL
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.http://sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||
goto end
|
||||
|
||||
:help
|
||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||
|
||||
:end
|
||||
popd
|
||||
28
docs/quickstart.rst
Normal file
28
docs/quickstart.rst
Normal file
@@ -0,0 +1,28 @@
|
||||
:orphan:
|
||||
|
||||
.. _quickstart:
|
||||
|
||||
.. currentmodule:: pyspapi
|
||||
|
||||
Quickstart
|
||||
==========
|
||||
|
||||
This page gives a brief introduction to the library.
|
||||
|
||||
Checking balance
|
||||
-------------
|
||||
|
||||
Let's output the amount of money remaining in the card account to the console.
|
||||
|
||||
It looks something like this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import pyspapi
|
||||
|
||||
print(pyspapi.SPAPI(card_id='card_id', token='token').balance)
|
||||
|
||||
Make sure not to name it ``pyspapi`` as that'll conflict with the library.
|
||||
|
||||
|
||||
You can find more examples in the `examples directory <https://github.com/deesiigneer/pyspapi/tree/main/examples/>`_ on GitHub.
|
||||
1
docs/requirements.txt
Normal file
1
docs/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
pydata_sphinx_theme
|
||||
12
examples/get_name_history.py
Normal file
12
examples/get_name_history.py
Normal file
@@ -0,0 +1,12 @@
|
||||
import pyspapi
|
||||
import asyncio
|
||||
|
||||
api = pyspapi.API(card_id='card_id', token='token')
|
||||
|
||||
|
||||
async def main():
|
||||
print(await api.get_name_history(uuid='63ed47877aa3470fbfc46c5356c3d797'))
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(main())
|
||||
|
||||
15
examples/get_profile.py
Normal file
15
examples/get_profile.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import pyspapi
|
||||
import asyncio
|
||||
|
||||
api = pyspapi.API(card_id='card_id', token='token')
|
||||
|
||||
|
||||
async def main():
|
||||
mojang_profile = await api.get_profile(uuid='63ed47877aa3470fbfc46c5356c3d797')
|
||||
print(mojang_profile)
|
||||
print(mojang_profile.id, mojang_profile.timestamp)
|
||||
print(mojang_profile.skin, mojang_profile.skin.model, mojang_profile.skin.cape_url, mojang_profile.skin.url)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(main())
|
||||
|
||||
21
examples/get_user and get_users.py
Normal file
21
examples/get_user and get_users.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import pyspapi
|
||||
import asyncio
|
||||
|
||||
|
||||
api = pyspapi.API(card_id='card_id', token='token')
|
||||
|
||||
|
||||
async def main():
|
||||
user = await api.get_user(264329096920563714)
|
||||
print(user)
|
||||
print(user.access)
|
||||
# У API есть лимиты, каждый user = 1 запрос, учитывайте это при использовании get_users
|
||||
# https://spworlds.readthedocs.io/ru/latest/index.html#id3
|
||||
users = await api.get_users([262632724928397312, 264329096920563714])
|
||||
for user in users:
|
||||
print(user)
|
||||
if user is not None:
|
||||
print(user.access)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(main())
|
||||
17
examples/get_uuid and get_uuids.py
Normal file
17
examples/get_uuid and get_uuids.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import pyspapi
|
||||
|
||||
import pyspapi
|
||||
import asyncio
|
||||
|
||||
api = pyspapi.API(card_id='card_id', token='token')
|
||||
|
||||
|
||||
async def main():
|
||||
uuid = await pyspapi.API(card_id='card_id', token='token').get_uuid(username='deesiigneer')
|
||||
print(uuid)
|
||||
print(uuid.id, uuid.name)
|
||||
print(await pyspapi.API(card_id='card_id', token='token').get_uuids(['deesiigneer', '5opka', 'OsterMiner']))
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(main())
|
||||
|
||||
16
examples/payments.py
Normal file
16
examples/payments.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import pyspapi
|
||||
import asyncio
|
||||
|
||||
api = pyspapi.API(card_id='card_id', token='token')
|
||||
|
||||
|
||||
async def main():
|
||||
print(await api.payment(amount=1,
|
||||
redirect_url='https://www.google.com/',
|
||||
webhook_url='https://www.google.com/',
|
||||
data='some-data'
|
||||
)
|
||||
)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(main())
|
||||
15
examples/transaction.py
Normal file
15
examples/transaction.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import pyspapi
|
||||
import asyncio
|
||||
|
||||
api = pyspapi.API(card_id='CARD_ID', token='TOKEN')
|
||||
|
||||
|
||||
async def main():
|
||||
print(await api.transaction(receiver=12345,
|
||||
amount=1,
|
||||
comment="test"
|
||||
)
|
||||
)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(main())
|
||||
5
examples/webhook_listener.py
Normal file
5
examples/webhook_listener.py
Normal file
@@ -0,0 +1,5 @@
|
||||
import pyspapi
|
||||
|
||||
api = pyspapi.API(card_id='your_card_id', token='your_token')
|
||||
|
||||
api.listener(host='myhost.com', port=80, webhook_path='/webhook/')
|
||||
10
examples/webhook_verify.py
Normal file
10
examples/webhook_verify.py
Normal file
@@ -0,0 +1,10 @@
|
||||
import pyspapi
|
||||
import asyncio
|
||||
|
||||
api = pyspapi.API(card_id='your_card_id', token='your_token')
|
||||
|
||||
async def main():
|
||||
print(await api.webhook_verify(data='webhook_data', header='webhook_header'))
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(main())
|
||||
4
pyspapi/__init__.py
Normal file
4
pyspapi/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .api import API
|
||||
from .types import SPUser, MojangProfile, Skin, UsernameToUUID
|
||||
|
||||
__version__ = "3.0.0a0"
|
||||
182
pyspapi/api.py
Normal file
182
pyspapi/api.py
Normal file
@@ -0,0 +1,182 @@
|
||||
import ast
|
||||
import warnings
|
||||
import asyncio
|
||||
from aiohttp import web
|
||||
from sys import version_info
|
||||
from base64 import b64encode, b64decode
|
||||
from hmac import new, compare_digest
|
||||
from hashlib import sha256
|
||||
from logging import getLogger
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
from .types import SPUser, MojangProfile, UsernameToUUID
|
||||
from .request import Request
|
||||
from .errors import Error
|
||||
|
||||
log = getLogger('pyspapi')
|
||||
|
||||
|
||||
class _BaseAPI:
|
||||
|
||||
_SPWORLDS = "https://spworlds.ru/api/public"
|
||||
_API_MOJANG = "https://api.mojang.com"
|
||||
_SESSIONSERVER_MOJANG = "https://sessionserver.mojang.com"
|
||||
|
||||
def __init__(self, card_id: str, token: str):
|
||||
"""
|
||||
:param card_id:
|
||||
:param token:
|
||||
"""
|
||||
self._id = card_id
|
||||
self._token = token
|
||||
|
||||
self._HEADER = {
|
||||
'Authorization': f"Bearer {str(b64encode(str(f'{self._id}:{self._token}').encode('utf-8')), 'utf-8')}",
|
||||
'User-Agent': f'pyspapi (https://github.com/deesiigneer/pyspapi) '
|
||||
f'Python {version_info.major}.{version_info.minor}.{version_info.micro}'
|
||||
}
|
||||
|
||||
async def webhook_verify(self, data: str, header) -> bool:
|
||||
"""
|
||||
Проверяет достоверность webhook'а. \n
|
||||
:param data: data из webhook.
|
||||
:param header: header X-Body-Hash из webhook.
|
||||
:return: True если header из webhook'а достоверен, иначе False
|
||||
"""
|
||||
print()
|
||||
hmac_data = b64encode(new(self._token.encode('utf-8'), data, sha256).digest())
|
||||
return compare_digest(hmac_data, header.encode('utf-8'))
|
||||
|
||||
def listener(self, host: str = '127.0.0.1', port: int = 80, webhook_path: str = '/webhook/'):
|
||||
app = web.Application()
|
||||
async def handle(request):
|
||||
request_data = await request.read()
|
||||
header = request.headers.get('X-Body-Hash')
|
||||
if header is not None:
|
||||
if await self.webhook_verify(data=request_data, header=header) is True:
|
||||
web.json_response(status=202)
|
||||
return True
|
||||
else:
|
||||
web.json_response(status=400)
|
||||
else:
|
||||
return web.json_response(status=404)
|
||||
app.add_routes([web.get(webhook_path, handle)])
|
||||
web.run_app(app, port=port, host=host)
|
||||
|
||||
|
||||
class API(_BaseAPI):
|
||||
"""
|
||||
class API
|
||||
"""
|
||||
|
||||
async def get_user(self, user_id: int) -> Optional[SPUser]:
|
||||
"""
|
||||
Получение информации об игроке SP \n
|
||||
:param user_id: ID пользователя в Discord.
|
||||
:return: Class User.
|
||||
"""
|
||||
sp_user = await Request.get(f'{self._SPWORLDS}/users/{str(user_id)}', self._HEADER)
|
||||
if sp_user is not None:
|
||||
return SPUser(await Request.get(f'{self._SPWORLDS}/users/{str(user_id)}', self._HEADER))
|
||||
else:
|
||||
return None
|
||||
|
||||
async def get_users(self, user_ids: List[int]) -> Union[SPUser, Any]:
|
||||
"""
|
||||
Получение никнеймов игроков в майнкрафте. **Максимально можно указать 60 user_ids, не используйте эту функцию
|
||||
чаще 1 раза в минуту если указали больше 60 user_ids**\n
|
||||
https://spworlds.readthedocs.io/ru/latest/index.html#id3\n
|
||||
:param user_ids: List[int] ID пользователей в Discord.
|
||||
:return: List[str] который содержит майнкрафт никнеймы игроков в том же порядке, который был задан, None если
|
||||
пользователь не найден или нет проходки.
|
||||
"""
|
||||
if len(user_ids) > 60:
|
||||
user_ids = user_ids[:60]
|
||||
warnings.warn('user_ids больше чем 60. Уменьшено до 60.')
|
||||
tasks = []
|
||||
for user_id in user_ids:
|
||||
tasks.append(self.get_user(user_id))
|
||||
return await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
async def get_uuid(self, username: str) -> Optional[UsernameToUUID]:
|
||||
"""
|
||||
Получить UUID игрока Minecraft.\n
|
||||
:param username: str никнейм игрока Minecraft.
|
||||
:return: Optional[str] UUID игрока Minecraft.
|
||||
"""
|
||||
response = await Request.get(f'{self._API_MOJANG}/users/profiles/minecraft/{username}')
|
||||
return UsernameToUUID(await Request.get(f'{self._API_MOJANG}/users/profiles/minecraft/{username}'))
|
||||
|
||||
async def get_uuids(self, usernames: list[str]) -> Dict[str, str]:
|
||||
"""
|
||||
Получить UUID's игроков Minecraft. **Не больше 10**\n
|
||||
:param usernames: List[str] Список с никнеймами игроков Minecraft.
|
||||
:return: Dict[str, str] UUID игроков Minecraft.
|
||||
"""
|
||||
if len(usernames) > 10:
|
||||
usernames = usernames[:10]
|
||||
warnings.warn('usernames больше чем 10. Уменьшено до 10.')
|
||||
return await Request.post(f'{self._API_MOJANG}/profiles/minecraft', payload=usernames)
|
||||
|
||||
async def get_name_history(self, uuid: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
История никнеймов в Minecraft.\n
|
||||
:param uuid: UUID игрока Minecraft.
|
||||
:return: List[Dict[str, Any]] который содержит name и changed_to_at
|
||||
"""
|
||||
requests = await Request.get(f"{self._API_MOJANG}/user/profiles/{uuid}/names")
|
||||
|
||||
name_data = []
|
||||
for data in requests:
|
||||
name_data_dict = {"name": data["name"]}
|
||||
if data.get("changedToAt"):
|
||||
name_data_dict["changed_to_at"] = data["changedToAt"]
|
||||
else:
|
||||
name_data_dict["changed_to_at"] = 0
|
||||
name_data.append(name_data_dict)
|
||||
return name_data
|
||||
|
||||
async def get_profile(self, uuid: str) -> MojangProfile:
|
||||
response = await Request.get(f'{self._SESSIONSERVER_MOJANG}/session/minecraft/profile/{uuid}')
|
||||
return MojangProfile(ast.literal_eval(b64decode(response["properties"][0]["value"]).decode()))
|
||||
|
||||
async def payment(self, amount: int, redirect_url: str, webhook_url: str, data: str) -> Optional[str]:
|
||||
"""
|
||||
Создание ссылки для оплаты.\n
|
||||
:param amount: Стоимость покупки в АРах.
|
||||
:param redirect_url: URL страницы, на которую попадет пользователь после оплаты.
|
||||
:param webhook_url: URL, куда наш сервер направит запрос, чтобы оповестить ваш сервер об успешной оплате.
|
||||
:param data: Строка до 100 символов, сюда можно поместить любые полезные данных.
|
||||
:return: Ссылку на страницу оплаты, на которую стоит перенаправить пользователя.
|
||||
"""
|
||||
if len(data) > 100:
|
||||
raise Error('В data больше 100 символов')
|
||||
return await Request.post(f'{self._SPWORLDS}/payment',
|
||||
payload={
|
||||
'amount': amount,
|
||||
'redirectUrl': redirect_url,
|
||||
'webhookUrl': webhook_url,
|
||||
'data': data},
|
||||
headers=self._HEADER)
|
||||
|
||||
async def transaction(self, receiver: int, amount: int, comment: str) -> Optional[str]:
|
||||
"""
|
||||
Перевод АР на карту. \n
|
||||
:param receiver: Номер карты получателя.
|
||||
:param amount: Количество АР для перевода.
|
||||
:param comment: Комментарий для перевода.
|
||||
:return: True если перевод успешен, иначе False.
|
||||
"""
|
||||
return 'Удачно' if await Request.post(f'{self._SPWORLDS}/transactions',
|
||||
payload={'receiver': receiver,
|
||||
'amount': amount,
|
||||
'comment': comment},
|
||||
headers=self._HEADER) else 'Что-то пошло не так...'
|
||||
|
||||
@property
|
||||
async def balance(self) -> Optional[int]:
|
||||
"""
|
||||
Проверка баланса карты \n
|
||||
:return: Количество АР на карте.
|
||||
"""
|
||||
balance = await Request.get(f'{self._SPWORLDS}/card', headers=self._HEADER)
|
||||
return balance['balance']
|
||||
29
pyspapi/errors.py
Normal file
29
pyspapi/errors.py
Normal file
@@ -0,0 +1,29 @@
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Unauthorized(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NotFound(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class TooManyRequests(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UserNotFound(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def handle(response):
|
||||
if response['error'] == 'Unauthorized':
|
||||
raise Unauthorized(response['message'])
|
||||
elif response['error'] == 'Not Found':
|
||||
raise NotFound(response['message'])
|
||||
elif response['error'] == 'Too Many Requests':
|
||||
raise TooManyRequests(response['message'])
|
||||
else:
|
||||
raise Exception(response)
|
||||
31
pyspapi/request.py
Normal file
31
pyspapi/request.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import aiohttp
|
||||
from .errors import handle
|
||||
from typing import Coroutine, Any, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
Response = Coroutine[Any, Any, T]
|
||||
|
||||
class Request:
|
||||
async def get(url: str, headers=None):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, headers=headers) as session_response:
|
||||
try:
|
||||
response = await session_response.json()
|
||||
except:
|
||||
response = await session_response.text()
|
||||
if session_response.status == 404:
|
||||
return None
|
||||
if not session_response.ok:
|
||||
handle(response)
|
||||
return response
|
||||
|
||||
async def post(url: str, payload, headers=None):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
session_response = await session.post(url, json=payload, headers=headers)
|
||||
try:
|
||||
response = await session_response.json()
|
||||
except:
|
||||
response = await session_response.text()
|
||||
if not session_response.ok:
|
||||
handle(response)
|
||||
return response
|
||||
63
pyspapi/types.py
Normal file
63
pyspapi/types.py
Normal file
@@ -0,0 +1,63 @@
|
||||
class SPUser:
|
||||
def __init__(self, data: dict):
|
||||
self.__data = data
|
||||
|
||||
@property
|
||||
def access(self) -> bool:
|
||||
return True if self.__data['username'] is not None else False
|
||||
|
||||
def __repr__(self):
|
||||
return self.__data['username']
|
||||
|
||||
|
||||
class MojangProfile:
|
||||
def __init__(self, data: dict):
|
||||
self.__data = data
|
||||
self.skin = Skin(data)
|
||||
|
||||
@property
|
||||
def id(self) -> str:
|
||||
return self.__data['profileId']
|
||||
|
||||
@property
|
||||
def timestamp(self):
|
||||
return self.__data['timestamp']
|
||||
|
||||
def __repr__(self):
|
||||
return self.__data['profileName']
|
||||
|
||||
|
||||
class Skin:
|
||||
def __init__(self, data: dict):
|
||||
self.__data = data
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
return self.__data['textures']['SKIN']['url']
|
||||
|
||||
@property
|
||||
def cape_url(self) -> str:
|
||||
return self.__data['textures']['CAPE']['url']
|
||||
|
||||
@property
|
||||
def model(self) -> str:
|
||||
return 'classic' if self.__data['textures']['SKIN'].get('metadata') is None else 'slim'
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.__data['textures']['SKIN'])
|
||||
|
||||
|
||||
class UsernameToUUID:
|
||||
def __init__(self, data: dict):
|
||||
self.__data = data
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self.__data['id']
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.__data['name']
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.__data['id'])
|
||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
requests==2.28.1
|
||||
aiohttp>=3.8.0,<4.0.0
|
||||
51
setup.py
51
setup.py
@@ -1,20 +1,47 @@
|
||||
import re
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
from os import path
|
||||
this_directory = path.abspath(path.dirname(__file__))
|
||||
with open(path.join(this_directory, 'README.md'), encoding='utf-8') as f:
|
||||
description = f.read()
|
||||
requirements = []
|
||||
with open("requirements.txt") as f:
|
||||
requirements = f.read().splitlines()
|
||||
|
||||
requires = ['requests==2.25.1']
|
||||
version = ""
|
||||
with open("pyspapi/__init__.py") as f:
|
||||
match = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', f.read(), re.MULTILINE)
|
||||
if match is None or match.group(1) is None:
|
||||
raise RuntimeError('Version is not set')
|
||||
|
||||
version = match.group(1)
|
||||
|
||||
if not version:
|
||||
raise RuntimeError("Version is not set")
|
||||
|
||||
readme = ""
|
||||
with open("README.rst") as f:
|
||||
readme = f.read()
|
||||
|
||||
packages = [
|
||||
"pyspapi"
|
||||
]
|
||||
|
||||
setup(
|
||||
name='pyspapi',
|
||||
version='1.0.0a',
|
||||
description='Framework for SP API',
|
||||
long_description=description,
|
||||
long_description_content_type='text/markdown',
|
||||
license='MIT',
|
||||
author='deesiigneer',
|
||||
author_email='xdeesiigneerx@gmail.com',
|
||||
packages=['spapi'],
|
||||
install_requires=requires,
|
||||
version=version,
|
||||
url='https://github.com/deesiigneer/pyspapi',
|
||||
project_urls={
|
||||
"pyspapi documentation": "https://pyspapi.readthedocs.io/",
|
||||
"api documentation": "https://spworlds.readthedocs.io/",
|
||||
"GitHub": "https://github.com/deesiigneer/pyspapi",
|
||||
"Discord": "https://discord.com/invite/VbyHaKRAaN"
|
||||
},
|
||||
description='API wrapper for SP servers written in Python',
|
||||
long_description=readme,
|
||||
long_description_content_type='text/x-rst',
|
||||
packages=packages,
|
||||
include_package_data=True,
|
||||
install_requires=requirements,
|
||||
python_requires='>=3.8.0',
|
||||
)
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
# pyspapi by deesiigneer
|
||||
#
|
||||
|
||||
import base64
|
||||
from requests import post, get
|
||||
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ApiError(Error):
|
||||
pass
|
||||
|
||||
|
||||
class Api:
|
||||
|
||||
def __init__(self, card_id: str, token: str):
|
||||
self.id = card_id
|
||||
self.token = token
|
||||
self.header = {
|
||||
'Authorization': f"Bearer {str(base64.b64encode(str(f'{self.id}:{self.token}').encode('utf-8')), 'utf-8')}",
|
||||
}
|
||||
|
||||
def _fetch(self, path, data):
|
||||
if path is None:
|
||||
result = get(url=f'https://spworlds.ru/api/public/users/{data}',
|
||||
headers=self.header)
|
||||
else:
|
||||
result = post(
|
||||
url=f'https://spworlds.ru/api/public/{path}',
|
||||
headers=self.header,
|
||||
json=data
|
||||
)
|
||||
if result.status_code == [200, 400]:
|
||||
ApiError(f'Ошибка при запросе к API {result.status_code}')
|
||||
|
||||
return result.json()
|
||||
|
||||
def payment(self, amount, redirecturl, webhookurl, data):
|
||||
"""
|
||||
Создание запроса на оплату.
|
||||
|
||||
:param amount: Стоимость покупки в АРах
|
||||
:param redirecturl: URL страницы, на которую попадет пользователь после оплаты
|
||||
:param webhookurl: URL, куда наш сервер направит запрос, чтобы оповестить ваш сервер об успешной оплате
|
||||
:param data: Строка до 100 символов, сюда можно поместить любые полезные данных.
|
||||
|
||||
:return: url - Ссылка на страницу оплаты, на которую стоит перенаправить пользователя.
|
||||
"""
|
||||
return self._fetch('payment', data={'amount': amount,
|
||||
'redirectUrl': redirecturl,
|
||||
'webhookUrl': webhookurl,
|
||||
'data': data})
|
||||
|
||||
def transaction(self, receiver, amount, comment):
|
||||
"""
|
||||
Перевод АР на карту.
|
||||
|
||||
:param receiver : Номер карты получателя
|
||||
|
||||
:param amount: Количество АР для перевода
|
||||
|
||||
:param comment: Комментарий для перевода
|
||||
"""
|
||||
return self._fetch('transactions', data={'receiver': receiver,
|
||||
'amount': amount,
|
||||
'comment': comment})
|
||||
|
||||
def check_user(self, discord_user_id):
|
||||
"""
|
||||
Проверка на наличие проходки
|
||||
:param discord_user_id: Стоимость покупки в АРах
|
||||
|
||||
:return: username - Ник пользователя или null, если у пользователя нет проходки на сервер.
|
||||
"""
|
||||
return self._fetch(None, data=discord_user_id)
|
||||
Reference in New Issue
Block a user