42 Commits

Author SHA1 Message Date
deesiigneer
c08394addf v3 asyncio/aiohttp alpha 2022-08-15 23:26:36 +03:00
Aleksey
63ee509067 update logo 2022-07-23 20:05:52 +03:00
deesiigneer
4ecafef192 upload repo banner 2022-07-23 20:04:52 +03:00
deesiigneer
d44199c4cc upload repo banner 2022-07-23 20:00:37 +03:00
deesiigneer
056ebce615 logo fix (incorrect gradient placement, lol) 2022-07-23 19:35:22 +03:00
deesiigneer
b00b2b76cd docs upload 2022-07-23 19:33:28 +03:00
deesiigneer
da86771e3c version bump 2022-07-20 22:21:33 +03:00
deesiigneer
479a02b95f transactions fix #9 2022-07-20 22:20:22 +03:00
deesiigneer
5c905ea097 python version downgrade for /docs 2022-07-20 21:12:47 +03:00
deesiigneer
83d9663da3 version bump 2022-07-20 21:12:17 +03:00
deesiigneer
93760d2d87 add MANIFEST file for pip 2022-07-20 21:02:57 +03:00
deesiigneer
108d30ebac typo fix 2022-07-19 21:52:59 +03:00
deesiigneer
b4ddd1fd93 update example 2022-07-18 22:27:58 +03:00
deesiigneer
5449659566 update example 2022-07-18 22:15:43 +03:00
deesiigneer
85ef993011 update example and add get_uuids() example 2022-07-18 22:15:05 +03:00
deesiigneer
8e6664bc51 new feature get_uuids() 2022-07-18 22:14:06 +03:00
deesiigneer
0d46f0a126 version bump 2022-07-18 22:12:48 +03:00
Aleksey
f5484d3900 version fix 2022-07-18 05:59:29 +03:00
Aleksey
1c35daf142 Delete 68747470733a2f2f692e696d6775722e636f6d2f6d656c685768552e706e67.png 2022-07-18 05:47:29 +03:00
deesiigneer
91ed017ac6 Merge remote-tracking branch 'origin/main' 2022-07-18 05:47:10 +03:00
deesiigneer
1806ba7929 logo asset 2022-07-18 05:47:00 +03:00
deesiigneer
f4623e9204 typo pypi fix 2022-07-18 05:45:05 +03:00
Aleksey
906060a4b7 Add files via upload 2022-07-18 04:58:01 +03:00
Aleksey
6bf342a9f1 update
translate to English
2022-07-18 04:49:47 +03:00
Aleksey
a6cbf2adcc fix 2022-07-18 04:25:31 +03:00
Aleksey
59d43736c0 Update python-publish.yml 2022-07-18 03:44:58 +03:00
Aleksey
9429c443c4 Merge pull request #5 from deesiigneer/v2.0.x
v2.0.0!
2022-07-18 03:17:19 +03:00
Aleksey
152b5272c1 Update README.rst 2022-07-18 03:15:38 +03:00
deesiigneer
81d8b1de35 v2.0.0 2022-07-18 03:12:27 +03:00
Aleksey
0667922c8c Update and rename README.md to README.rst 2022-07-08 11:26:54 +03:00
Aleksey
9a61797253 Update README.md 2022-06-22 11:59:41 +03:00
Aleksey
c9c1bc8e81 Update README.md 2022-06-09 14:46:41 +03:00
Aleksey
f551a860bc Update setup.py 2022-06-08 10:53:05 +03:00
Aleksey
1065b5748b Merge pull request #3 from deesiigneer/feature/webhook_verify
Добавление webhook_verify
2022-06-08 10:50:20 +03:00
Aleksey
1d6af2b8ec Update README.md
Немного исправлено описание библиотеки и добавлено описание функции webhook_veify()
2022-06-08 10:24:55 +03:00
deesiigneer
f244e3f4db new feature webhook_verify 2022-06-08 10:00:38 +03:00
deesiigneer
76dc15a332 Merge remote-tracking branch 'origin/main' 2022-06-06 14:52:00 +03:00
deesiigneer
a2a4ac9fab fix #1 2022-06-06 14:51:38 +03:00
Aleksey
ef40d83a9c Update python-publish.yml 2022-06-05 17:25:41 +03:00
Aleksey
4a611396a1 Create python-publish.yml 2022-06-05 17:23:55 +03:00
Aleksey
7cc708ee03 fix: typo 2022-06-04 11:43:56 +03:00
Aleksey
8cba25a754 fix: Разметка 2022-06-03 21:25:42 +03:00
37 changed files with 883 additions and 179 deletions

38
.github/workflows/python-publish.yml vendored Normal file
View 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
View 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
View File

@@ -0,0 +1,3 @@
include README.rst
include LICENSE
include requirements.txt

View File

@@ -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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
assets/repo-banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

20
docs/Makefile Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

19
docs/_static/custom.css vendored Normal file
View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

BIN
docs/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

35
docs/index.rst Normal file
View 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
View 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
View 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
View File

@@ -0,0 +1 @@
pydata_sphinx_theme

View 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
View 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())

View 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())

View 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
View 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
View 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())

View 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/')

View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,2 @@
requests==2.28.1
aiohttp>=3.8.0,<4.0.0

View File

@@ -1,4 +0,0 @@
[egg_info]
tag_build =
tag_date = 0

View File

@@ -1,20 +1,47 @@
import re
from setuptools import setup from setuptools import setup
from os import path requirements = []
this_directory = path.abspath(path.dirname(__file__)) with open("requirements.txt") as f:
with open(path.join(this_directory, 'README.md'), encoding='utf-8') as f: requirements = f.read().splitlines()
description = f.read()
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( setup(
name='pyspapi', name='pyspapi',
version='1.0.0a', license='MIT',
description='Framework for SP API',
long_description=description,
long_description_content_type='text/markdown',
author='deesiigneer', author='deesiigneer',
author_email='xdeesiigneerx@gmail.com', version=version,
packages=['spapi'], url='https://github.com/deesiigneer/pyspapi',
install_requires=requires, 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',
) )

View File

@@ -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)