From 6b771f8925c6db609063d90b4b923d6590320624 Mon Sep 17 00:00:00 2001 From: Teleport <77507478+teleport2@users.noreply.github.com> Date: Mon, 8 May 2023 21:43:58 +0300 Subject: [PATCH] Upload 1.5.0 version Upload docs --- README.md | 27 +++- docs/Makefile | 20 +++ docs/make.bat | 35 +++++ docs/source/conf.py | 73 +++++++++ docs/source/index.rst | 25 ++++ docs/source/pyspw.rst | 34 +++++ docs/source/reference.rst | 8 + examples/check_access.py | 14 ++ examples/check_api_work.py | 9 ++ examples/create_payment_link.py | 48 ++++++ examples/get_balance.py | 8 + examples/send_transaction.py | 37 +++++ examples/users_actions.py | 31 ++++ examples/validate_webhook.py | 19 +++ pyspw/Parameters.py | 82 +++++----- pyspw/Skin.py | 100 ------------- pyspw/User.py | 123 +++++++++++---- pyspw/__init__.py | 12 +- pyspw/api.py | 257 +++++++++++++------------------- pyspw/errors.py | 61 +++++--- requirements.txt | 5 + setup.py | 15 +- 22 files changed, 676 insertions(+), 367 deletions(-) create mode 100644 docs/Makefile create mode 100644 docs/make.bat create mode 100644 docs/source/conf.py create mode 100644 docs/source/index.rst create mode 100644 docs/source/pyspw.rst create mode 100644 docs/source/reference.rst create mode 100644 examples/check_access.py create mode 100644 examples/check_api_work.py create mode 100644 examples/create_payment_link.py create mode 100644 examples/get_balance.py create mode 100644 examples/send_transaction.py create mode 100644 examples/users_actions.py create mode 100644 examples/validate_webhook.py delete mode 100644 pyspw/Skin.py create mode 100644 requirements.txt diff --git a/README.md b/README.md index 9dbdb19..a53145a 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,28 @@ # Py SPW -Library fo work with SPworlds API. +Library for work with [SPWorlds](https://spworlds.ru) API in Python. ## Installation -Library written on python 3.10.5 +Need python version >=3.7 -To install, run this command: -`pip install Py-Spw` +```shell +pip install Py-Spw +``` -After import Py-SPW to your project: `import pyspw` +## Quick start +*Checking user access* +```python +import pyspw + +api = pyspw.SpApi(card_id='card_id', + card_token='card_token') + +print(api.check_access('437610383310716930')) +``` + +### How to +You can see [examples](https://github.com/teleportx/Py-SPW/tree/main/examples) to help solve your problem ## Links - [PyPi](https://pypi.org/project/Py-SPW) -- [Documentation](https://github.com/teleport2/Py-SPW/wiki) -- [Author](https://github.com/teleport2) \ No newline at end of file +- [Documentation](https://github.com/teleportx/Py-SPW/wiki) +- [API](https://github.com/sp-worlds/api-docs) \ No newline at end of file diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d0c3cbf --- /dev/null +++ b/docs/Makefile @@ -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) diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..dc1312a --- /dev/null +++ b/docs/make.bat @@ -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 + +%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.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..1e78d67 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,73 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +import os +import sys + +sys.path.insert(0, os.path.abspath('../..')) + +import pyspw + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = 'Py-SPW' +copyright = '2023, Teleport' +author = 'Teleport' +release = pyspw.__version__ + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [ + "sphinx.ext.autodoc", +] + +templates_path = ['_templates'] +exclude_patterns = [] + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = 'furo' +html_static_path = ['_static'] +html_sidebars = { + "**": [ + "sidebar/scroll-start.html", + "sidebar/brand.html", + "sidebar/search.html", + "sidebar/navigation.html", + "sidebar/navigation.html", + "sidebar/ethical-ads.html", + "sidebar/scroll-end.html", + ], +} +html_theme_options = { + "footer_icons": [ + { + "name": "GitHub", + "url": "https://github.com/teleportx/Py-SPW", + "html": """ + + + + """, + "class": "", + }, + { + "name": "PyPi", + "url": "https://pypi.org/project/Py-SPW", + "html": """ + + + + """, + "class": "", + }, + ], + "source_repository": "https://github.com/teleportx/Py-SPW/", + "source_branch": "main", + "source_directory": "docs/", +} diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..63faa98 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,25 @@ + + +Py-SPW Documentation +==================== + +Library for work with `SPWorlds `_ API in Python. + +Getting example +=============== +.. literalinclude:: ../../examples/check_access.py + +Ask help +============= + +* See the code `examples `_ +* If you found a bug in a library report it to `issue tracker `_ +* Get help with your code using Py-SPW `discussions `_ + +Documentation +============= +.. toctree:: + :maxdepth: 3 + + reference + pyspw diff --git a/docs/source/pyspw.rst b/docs/source/pyspw.rst new file mode 100644 index 0000000..fe02866 --- /dev/null +++ b/docs/source/pyspw.rst @@ -0,0 +1,34 @@ +pyspw +===== + +Api +--- + +.. automodule:: pyspw.api + :members: + :undoc-members: + :show-inheritance: + +User +---- + +.. automodule:: pyspw.User + :members: + :undoc-members: + :show-inheritance: + +Parameters +---------- + +.. automodule:: pyspw.Parameters + :members: + :undoc-members: + :show-inheritance: + +Errors +------ + +.. automodule:: pyspw.errors + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/reference.rst b/docs/source/reference.rst new file mode 100644 index 0000000..3bb2a80 --- /dev/null +++ b/docs/source/reference.rst @@ -0,0 +1,8 @@ +API Reference +============= + +.. automodule:: pyspw.api + :members: SpApi + :undoc-members: + :noindex: + :show-inheritance: \ No newline at end of file diff --git a/examples/check_access.py b/examples/check_access.py new file mode 100644 index 0000000..8709e69 --- /dev/null +++ b/examples/check_access.py @@ -0,0 +1,14 @@ +import pyspw + + +# Init library +api = pyspw.SpApi(card_id='card_id', + card_token='card_token') + +# Check access to server +print(api.check_access('287598524017803264')) # True +print(api.check_access('289341856083607552')) # False + + +# Check more than one access +print(api.check_accesses(['403987036219899908', '558667431187447809'], delay=1)) # False, True diff --git a/examples/check_api_work.py b/examples/check_api_work.py new file mode 100644 index 0000000..9c8da15 --- /dev/null +++ b/examples/check_api_work.py @@ -0,0 +1,9 @@ +import pyspw + +# Init library +api = pyspw.SpApi(card_id='card_id', + card_token='card_token') + + +# Checking +print(api.ping()) diff --git a/examples/create_payment_link.py b/examples/create_payment_link.py new file mode 100644 index 0000000..31e0b77 --- /dev/null +++ b/examples/create_payment_link.py @@ -0,0 +1,48 @@ +import json + +import pyspw +from pyspw.Parameters import Payment + + +# Init library +api = pyspw.SpApi(card_id='card_id', + card_token='card_token') + + +# Constructing payment +payment = Payment( + amount=150, # Payment amount (You can't set more than one shulker diamond ore) + redirectUrl='https://spwdev.xyz/', # URL which redirects user after successful payment + webhookUrl='https://spwdev.xyz/api/webhook_spw', # URL which receive webhook of successful payment with data + data=json.dumps({ # Useful data which received with webhook on webhookUrl + "type": "prepayment", + "order": 987951455 + }) +) + +# Create payment link +print(api.create_payment(payment)) + + +# Create more than one payment link +prepayment = Payment( + amount=150, + redirectUrl='https://spwdev.xyz/', + webhookUrl='https://spwdev.xyz/api/webhook_spw', + data=json.dumps({ + "type": "prepayment", + "order": 987951455 + }) +) + +# clone similar payment +post_payment = prepayment +post_payment.data = json.dumps({ # You can access to payment variables + "type": "post-payment", + "order": 987951455 + }) + + +# Create payment links +api.create_payments([prepayment, post_payment], delay=0.6) +# !Payments links valid for 5 minutes! diff --git a/examples/get_balance.py b/examples/get_balance.py new file mode 100644 index 0000000..f0a63c7 --- /dev/null +++ b/examples/get_balance.py @@ -0,0 +1,8 @@ +import pyspw + +# Init library +api = pyspw.SpApi(card_id='card_id', + card_token='card_token') + +# Get card balance +print(api.get_balance()) diff --git a/examples/send_transaction.py b/examples/send_transaction.py new file mode 100644 index 0000000..7672ebd --- /dev/null +++ b/examples/send_transaction.py @@ -0,0 +1,37 @@ +import pyspw +from pyspw.Parameters import Transaction + + +# Init library +api = pyspw.SpApi(card_id='card_id', + card_token='card_token') + + +# Constructing transaction +transaction = Transaction( + receiver='00001', # Card number of receiver + amount=24, # Amount of diamond ore which you want to send + comment='Buy diamond pickaxe' # Comment on transaction +) + +# Send transaction +api.send_transaction(transaction) + + +# Send more than one transaction +salary = Transaction( + receiver='00002', + amount=100, + comment='Salary for the January' +) + + +# You can get information from Transaction class +tax = Transaction( + receiver='00001', + amount=round(salary.amount * 0.2), # take 20% from salary amount + comment=f'Tax from `{salary.comment}`' +) + +# Send transactions +api.send_transactions([tax, salary], delay=0.8) diff --git a/examples/users_actions.py b/examples/users_actions.py new file mode 100644 index 0000000..5e410ed --- /dev/null +++ b/examples/users_actions.py @@ -0,0 +1,31 @@ +from typing import List + +import pyspw +from pyspw.User import User, Skin + +# Init library +api = pyspw.SpApi(card_id='card_id', + card_token='card_token') + + +# get user by discord id +user: User = api.get_user('262632724928397312') + +print(user.uuid) # user uuid +print(user.nickname) # user nickname + +# working with user skin +skin: Skin = user.get_skin() + +print(skin.variant) # skin variant (slim or classic) +print(skin.get_head().get_url()) # get url of head +bust_image: bytes = skin.get_bust().get_image() # get image (bytes) of skin bust + + +# Get more than one user +users: List[User] = api.get_users(['471286011851177994', + '533953916082323456'], delay=0.4) + +# print their uuids +for player in users: + print(player.uuid) diff --git a/examples/validate_webhook.py b/examples/validate_webhook.py new file mode 100644 index 0000000..aa1571c --- /dev/null +++ b/examples/validate_webhook.py @@ -0,0 +1,19 @@ +import json + +import pyspw + +# Init library +api = pyspw.SpApi(card_id='card_id', + card_token='card_token') + + +# Data received from webhook +webhook_body = { + "payer": "Nakke_", + "amount": 10, + "data": "brax10" +} +X_Body_Hash = "fba3046f2800197d8829556bdf2d04bf61a307d4ede31eb37fb4078d21e24d3e" + +# Verify +print(api.check_webhook(json.dumps(webhook_body), X_Body_Hash)) # True or False diff --git a/pyspw/Parameters.py b/pyspw/Parameters.py index 1404f7f..b1acd54 100644 --- a/pyspw/Parameters.py +++ b/pyspw/Parameters.py @@ -1,45 +1,45 @@ -class PaymentParameters: - def __init__(self, amount: int, redirectUrl: str, webhookUrl: str, data: str): - """ - Создание параметров ссылки на оплату - :param amount: Стоимость покупки в АРах. - :param redirectUrl: URL страницы, на которую попадет пользователь после оплаты. - :param webhookUrl: URL, куда наш сервер направит запрос, чтобы оповестить ваш сервер об успешной оплате. - :param data: Строка до 100 символов, сюда можно помеcтить любые полезные данных. - :return: Str ссылка на страницу оплаты, на которую стоит перенаправить пользователя. - """ - - self.amount = amount - self.redirectUrl = redirectUrl - self.webhookUrl = webhookUrl - self.data = data - - def __str__(self): - return f''' - amount: {self.amount} - redirectUrl: {self.redirectUrl} - webhookUrl: {self.webhookUrl} - data: {self.data} - ''' +from pydantic import BaseModel, validator +import validators -class TransactionParameters: - def __init__(self, receiver: str, amount: int, comment: str = 'No comment'): - """ - Отправка транзакции - :param receiver: Номер карты на которую будет совершена транзакция. - :param amount: Сумма транзакции. - :param comment: Комментарий к транзакции. - :return: None. - """ +class Payment(BaseModel): + amount: int + redirectUrl: str + webhookUrl: str + data: str - self.receiver = receiver - self.amount = amount - self.comment = comment + @validator('amount') + def max_amount(cls, value: int): + if value > 1728: + raise ValueError('amount must be <= 1728') + return value - def __str__(self): - return f''' - receiver: {self.receiver} - amount: {str(self.amount)} - comment: {self.comment} - ''' \ No newline at end of file + @validator('data') + def data_size(cls, value): + if len(value) > 100: + raise ValueError('data length must be <=100.') + return value + + @validator('redirectUrl', 'webhookUrl') + def verify_url(cls, value: str): + if validators.url(value): + return value + raise ValueError('is not url') + + +class Transaction(BaseModel): + receiver: str + amount: int + comment: str + + @validator('comment') + def comment_size(cls, value: str): + if len(value) > 32: + raise ValueError('comment length must be <=32.') + return value + + @validator('receiver') + def receiver_type(cls, value: str): + if len(value) != 5 or not value.isnumeric(): + raise ValueError(f'Receiver card (`{value}`) number not valid') + return value diff --git a/pyspw/Skin.py b/pyspw/Skin.py deleted file mode 100644 index cadfd22..0000000 --- a/pyspw/Skin.py +++ /dev/null @@ -1,100 +0,0 @@ -import requests as rq -from mojang import MojangAPI -from typing import Optional - -from . import errors as err - - -class SkinPart: - def __init__(self, url: str): - self.__skin_part_url = url - - def __str__(self): - return self.get_url() - - def __bytes__(self): - return self.get_image() - - def get_url(self) -> str: - return self.__skin_part_url - - def get_image(self) -> bytes: - try: - visage_surgeplay_response = rq.get(self.__skin_part_url) - if visage_surgeplay_response.status_code != 200: - raise err.SurgeplayApiError(f'HTTP status: {visage_surgeplay_response.status_code}') - return visage_surgeplay_response.content - - except rq.exceptions.ConnectionError as error: - raise err.SurgeplayApiError(error) - - -class Cape: - def __init__(self, url: Optional[str]): - self.__skin_part_url = url - - def __str__(self): - if self.__skin_part_url is None: - return 'None' - - return self.get_url() - - def __bytes__(self): - image = self.get_image() - if image is None: - return bytes(0) - - return image - - def get_url(self) -> Optional[str]: - if self.__skin_part_url is None: - return None - - return self.__skin_part_url - - def get_image(self) -> Optional[bytes]: - if self.__skin_part_url is None: - return None - - try: - visage_surgeplay_response = rq.get(self.__skin_part_url) - if visage_surgeplay_response.status_code != 200: - raise err.SurgeplayApiError(f'HTTP status: {visage_surgeplay_response.status_code}') - return visage_surgeplay_response.content - - except rq.exceptions.ConnectionError as error: - raise err.SurgeplayApiError(error) - - -class Skin: - __visage_surgeplay_url = 'https://visage.surgeplay.com/' - - def __init__(self, uuid: str): - self.__uuid = uuid - - def __str__(self): - return self.get_skin().__str__() - - def get_face(self, image_size: int = 64) -> SkinPart: - return SkinPart(f'https://visage.surgeplay.com/face/{image_size}/{self.__uuid}') - - def get_front(self, image_size: int = 64) -> SkinPart: - return SkinPart(f'https://visage.surgeplay.com/front/{image_size}/{self.__uuid}') - - def get_front_full(self, image_size: int = 64) -> SkinPart: - return SkinPart(f'https://visage.surgeplay.com/frontfull/{image_size}/{self.__uuid}') - - def get_head(self, image_size: int = 64) -> SkinPart: - return SkinPart(f'https://visage.surgeplay.com/head/{image_size}/{self.__uuid}') - - def get_bust(self, image_size: int = 64) -> SkinPart: - return SkinPart(f'https://visage.surgeplay.com/bust/{image_size}/{self.__uuid}') - - def get_full(self, image_size: int = 64) -> SkinPart: - return SkinPart(f'https://visage.surgeplay.com/full/{image_size}/{self.__uuid}') - - def get_skin(self, image_size: int = 64) -> SkinPart: - return SkinPart(f'https://visage.surgeplay.com/skin/{image_size}/{self.__uuid}') - - def get_cape(self) -> Cape: - return Cape(MojangAPI.get_profile(self.__uuid).cape_url) diff --git a/pyspw/User.py b/pyspw/User.py index 57b7b7d..7f361bf 100644 --- a/pyspw/User.py +++ b/pyspw/User.py @@ -1,37 +1,102 @@ -from typing import List, Dict, Any, Optional -from mojang import MojangAPI +from enum import Enum +from typing import Optional -from .Skin import Skin +import requests as rq +from mojang import API as MAPI +from mojang._types import UserProfile + +from . import errors as err +from .errors import MojangAccountNotFound + +mapi = MAPI() + + +class SkinVariant(Enum): + SLIM = 'slim' + CLASSIC = 'classic' + + +class _SkinPart: + def __init__(self, url: str): + self.__skin_part_url = url + + def __str__(self): + return self.get_url() + + def __bytes__(self): + return self.get_image() + + def get_url(self) -> str: + return self.__skin_part_url + + def get_image(self) -> bytes: + try: + visage_surgeplay_response = rq.get(self.__skin_part_url) + if visage_surgeplay_response.status_code != 200: + raise err.SurgeplayApiError(f'HTTP status: {visage_surgeplay_response.status_code}') + return visage_surgeplay_response.content + + except rq.exceptions.ConnectionError as error: + raise err.SurgeplayApiError(error) + + +class Skin: + __visage_surgeplay_url = 'https://visage.surgeplay.com/' + + def __init__(self, profile: UserProfile): + self._profile = profile + self._variant = SkinVariant(profile.skin_variant) + + @property + def variant(self) -> SkinVariant: + return self._variant + + def get_face(self, image_size: int = 64) -> _SkinPart: + return _SkinPart(f'https://visage.surgeplay.com/face/{image_size}/{self._profile.id}') + + def get_front(self, image_size: int = 64) -> _SkinPart: + return _SkinPart(f'https://visage.surgeplay.com/front/{image_size}/{self._profile.id}') + + def get_front_full(self, image_size: int = 64) -> _SkinPart: + return _SkinPart(f'https://visage.surgeplay.com/frontfull/{image_size}/{self._profile.id}') + + def get_head(self, image_size: int = 64) -> _SkinPart: + return _SkinPart(f'https://visage.surgeplay.com/head/{image_size}/{self._profile.id}') + + def get_bust(self, image_size: int = 64) -> _SkinPart: + return _SkinPart(f'https://visage.surgeplay.com/bust/{image_size}/{self._profile.id}') + + def get_full(self, image_size: int = 64) -> _SkinPart: + return _SkinPart(f'https://visage.surgeplay.com/full/{image_size}/{self._profile.id}') + + def get_skin(self, image_size: int = 64) -> _SkinPart: + return _SkinPart(f'https://visage.surgeplay.com/skin/{image_size}/{self._profile.id}') + + def get_cape(self) -> Optional[_SkinPart]: + if self._profile.cape_url is None: + return None + return _SkinPart(self._profile.cape_url) class User: - def __init__(self, nickname: str | None, use_mojang_api: bool = True): - self.nickname = nickname + def __init__(self, nickname: str): + self._nickname = nickname + self._uuid = mapi.get_uuid(nickname) + if self._uuid is None: + raise MojangAccountNotFound(self._nickname) + self._profile = mapi.get_profile(self._uuid) - if self.nickname is not None: - self.access = True + @property + def nickname(self) -> str: + return self._nickname - if use_mojang_api: - self.uuid = MojangAPI.get_uuid(nickname) + @property + def uuid(self) -> str: + return self._uuid - else: - self.uuid = None - self.access = False + @property + def profile(self) -> UserProfile: + return self._profile - def __str__(self): - if self.nickname is None: - return 'None' - - return self.nickname - - def get_skin(self) -> Optional[Skin]: - if self.uuid is None: - return None - - return Skin(self.uuid) - - def get_nickname_history(self) -> Optional[List[Dict[str, Any]]]: - if self.uuid is None: - return None - - return MojangAPI.get_name_history(self.uuid) + def get_skin(self) -> Skin: + return Skin(self._profile) diff --git a/pyspw/__init__.py b/pyspw/__init__.py index bf86360..fb20154 100644 --- a/pyspw/__init__.py +++ b/pyspw/__init__.py @@ -1,11 +1,3 @@ -from . import api -from . import errors -from . import User -from . import Skin -from . import Parameters +from .api import * -__all__ = ["SpApi", "errors", "User", "Skin"] - - -class SpApi(api.Py_SPW): - pass +__version__ = '1.5.0' diff --git a/pyspw/api.py b/pyspw/api.py index db58cc6..2beec60 100644 --- a/pyspw/api.py +++ b/pyspw/api.py @@ -1,42 +1,61 @@ +import platform +import sys from base64 import b64encode +from dataclasses import dataclass +from enum import Enum from hashlib import sha256 import hmac import requests as rq import time -from typing import Optional, List +from typing import List, Callable import logging -from mojang import MojangAPI +from mojang import API as MAPI from . import errors as err from .User import User -from .Parameters import PaymentParameters, TransactionParameters +from .Parameters import Payment, Transaction -# deesiigneer stole some of my ideas and improved them. But I didn't lose my head and improved what deesiigneer improved :) +mapi = MAPI() -class Py_SPW: - __spworlds_api_url = 'https://spworlds.ru/api/public' +class _RequestTypes(Enum): + POST = 'POST' + GET = 'GET' + + +@dataclass +class _Card: + id: str + token: str + + +class SpApi: + _spworlds_api_url = 'https://spworlds.ru/api/public' def __init__(self, card_id: str, card_token: str): - self.__card_token = card_token - self.__authorization = f"Bearer {str(b64encode(str(f'{card_id}:{card_token}').encode('utf-8')), 'utf-8')}" + self._card = _Card(card_id, card_token) + self._authorization = f"Bearer {str(b64encode(str(f'{card_id}:{card_token}').encode('utf-8')), 'utf-8')}" - def __get(self, path: str = None, ignore_status_code: bool = False) -> rq.Response: + def _request(self, method: _RequestTypes, path: str = '', body: dict = None, *, + ignore_codes: list = []) -> rq.Response: headers = { - 'Authorization': self.__authorization, - 'User-Agent': 'Py-SPW' + 'Authorization': self._authorization, + 'User-Agent': f'Py-SPW (Python {platform.python_version()})' } try: - response = rq.get(url=self.__spworlds_api_url + path, headers=headers) + response = rq.request(method.value, url=self._spworlds_api_url + path, headers=headers, json=body) except rq.exceptions.ConnectionError as error: raise err.SpwApiError(error) - if ignore_status_code: + if response.headers.get('Content-Type') != 'application/json': + raise err.SpwApiDDOS() + + if response.ok or response.status_code in ignore_codes: return response - if response.status_code == 200: - return response + elif response.status_code == 401: + raise err.SpwUnauthorized() elif response.status_code >= 500: raise err.SpwApiError(f'HTTP: {response.status_code}, Server Error.') @@ -45,74 +64,30 @@ class Py_SPW: raise err.SpwApiError( f'HTTP: {response.status_code} {response.json()["error"]}. Message: {response.json()["message"]}') - def __post(self, path: str = None, body: dict = None) -> rq.Response: - headers = { - 'Authorization': self.__authorization, - 'User-Agent': 'Py-SPW' - } + def ping(self) -> bool: + """ + Проверка работоспособности API + :return: Bool работает или нет + """ try: - response = rq.post(url=self.__spworlds_api_url + path, headers=headers, json=body) + self.get_balance() + return True - except rq.exceptions.ConnectionError as error: - raise err.SpwApiError(error) + except err.SpwApiError: + return False - if response.status_code == 200: - return response - - elif response.status_code >= 500: - raise err.SpwApiError(f'HTTP: {response.status_code}, Server Error.') - - else: - raise err.SpwApiError(f'HTTP: {response.status_code} {response.json()["error"]}. Message: {response.json()["message"]}') - - def get_user(self, discord_id: str, use_mojang_api: bool = True) -> User: + def get_user(self, discord_id: str) -> User: """ Получение пользователя - :param use_mojang_api: Если True то будет обращаться к Mojang API для получения UUID, иначе обращаться не будет :param discord_id: ID пользователя дискорда. :return: Class pyspw.User.User """ - response = self.__get(f'/users/{discord_id}', True) + response = self._request(_RequestTypes.GET, f'/users/{discord_id}', ignore_codes=[404]) + if response.status_code == 404: + raise err.SpwUserNotFound(discord_id) - if response.status_code == 200: - return User(response.json()['username'], use_mojang_api) - - elif response.status_code == 404: - return User(None, use_mojang_api) - - elif response.status_code >= 500: - raise err.SpwApiError(f'HTTP: {response.status_code}, Server Error.') - - else: - raise err.SpwApiError(f'HTTP: {response.status_code} {response.json()["error"]}. Message: {response.json()["message"]}') - - def get_users(self, discord_ids: List[str], delay: float = 0.5, use_mojang_api: bool = True) -> List[User]: - """ - Получение пользователей - :param use_mojang_api: Если True то будет обращаться к Mojang API для получения UUID, иначе обращаться не будет - :param delay: Значение задержки между запросами, указывается в секундах - :param discord_ids: List с IDs пользователей дискорда. - :return: List содержащий Classes pyspw.User.User - """ - - users = [] - - if len(discord_ids) > 100 and delay < 0.5: - logging.warning('You send DOS attack to SPWorlds API. Please set the delay to greater than or equal to 0.5') - - for discord_id in discord_ids: - users.append(self.get_user(discord_id, False)) - time.sleep(delay) - - if use_mojang_api: - nicknames = [user.nickname for user in users] - uuids = MojangAPI.get_uuids(nicknames) - - for user in users: - user.uuid = uuids[user.nickname] - - return users + return User(response.json()['username']) def check_access(self, discord_id: str) -> bool: """ @@ -120,31 +95,8 @@ class Py_SPW: :param discord_id: ID пользователя дискорда. :return: Bool True если у пользователя есть проходка, иначе False """ - return self.get_user(discord_id, False).access - - def check_accesses(self, discord_ids: List[str], delay: float = 0.5) -> List[bool]: - """ - Получение статуса проходок - :param delay: Значение задержки между запросами, указывается в секундах - :param discord_ids: List с IDs пользователей дискорда. - :return: List содержащий bool со значением статуса проходки - """ - - accesses = [] - - users = self.get_users(discord_ids, delay, False) - - if len(discord_ids) > 100 and delay < 0.5: - logging.warning('You send DOS attack to SPWorlds API. Please set the delay to greater than or equal to 0.5') - - for user in users: - if user is not None: - accesses.append(True) - - else: - accesses.append(False) - - return accesses + response = self._request(_RequestTypes.GET, f'/users/{discord_id}', ignore_codes=[404]) + return response.status_code != 404 def check_webhook(self, webhook_data: str, X_Body_Hash: str) -> bool: """ @@ -154,77 +106,80 @@ class Py_SPW: :return: Bool True если вебхук пришел от верифицированного сервера, иначе False """ - hmac_data = hmac.new(self.__card_token.encode('utf-8'), webhook_data.encode('utf-8'), sha256).digest() + hmac_data = hmac.new(self._card.token.encode('utf-8'), webhook_data.encode('utf-8'), sha256).digest() base64_data = b64encode(hmac_data) return hmac.compare_digest(base64_data, X_Body_Hash.encode('utf-8')) - def create_payment(self, params: PaymentParameters) -> str: + def create_payment(self, params: Payment) -> str: """ Создание ссылки на оплату :param params: class PaymentParams параметров оплаты :return: Str ссылка на страницу оплаты, на которую стоит перенаправить пользователя. """ + return self._request(_RequestTypes.POST, '/payment', params.dict()).json()['url'] - body = { - 'amount': params.amount, - 'redirectUrl': params.redirectUrl, - 'webhookUrl': params.webhookUrl, - 'data': params.data - } - return self.__post('/payment', body).json()['url'] - - def create_payments(self, payments: List[PaymentParameters], delay: float = 0.5) -> list: - """ - Создание ссылок на оплату - :param payments: Список содержащий classes PaymentParams - :param delay: Значение задержки между запросами, указывается в секундах - :return: List со ссылками на страницы оплаты, в том порядке, в котором они были в кортеже payments - """ - - answer = [] - - if len(payments) > 100 and delay < 0.5: - logging.warning('You send DOS attack to SPWorlds API. Please set the delay to greater than or equal to 0.5') - - for payment in payments: - answer.append(self.create_payment(payment)) - time.sleep(delay) - - return answer - - def send_transaction(self, params: TransactionParameters) -> None: + def send_transaction(self, params: Transaction) -> None: """ Отправка транзакции :param params: class TransactionParameters параметры транзакции :return: None. """ - - body = { - 'receiver': params.receiver, - 'amount': params.amount, - 'comment': params.comment - } - self.__post('/transactions', body) - - def send_transactions(self, transactions: List[TransactionParameters], delay: float = 0.5) -> None: - """ - Отправка транзакций - :param delay: Значение задержки между запросами, указывается в секундах - :param transactions: Список содержащий classes TransactionParameters - :return: List со ссылками на страницы оплаты, в том порядке, в котором они были в кортеже payments - """ - - if len(transactions) > 100 and delay < 0.5: - logging.warning('You send DOS attack to SPWorlds API. Please set the delay to greater than or equal to 0.5') - - for transaction in transactions: - self.send_transaction(transaction) - time.sleep(delay) + response = self._request(_RequestTypes.POST, '/transactions', params.dict(), ignore_codes=[400]) + if response.status_code == 400 and response.json()["message"] == 'Недостаточно средств на карте': + raise err.SpwInsufficientFunds() def get_balance(self) -> int: """ Получение баланса :return: Int со значением баланса """ + return self._request(_RequestTypes.GET, '/card').json()['balance'] - return self.__get('/card').json()['balance'] + # Manys + def _many_req(self, iterable: List, method: Callable, delay: float) -> List: + users = [] + + if len(iterable) > 100 and delay <= 0.5: + logging.warning('You send DOS attack to SPWorlds API. Please set the delay to greater than to 0.5') + + for i in iterable: + users.append(method(i)) + time.sleep(delay) + + return users + + def get_users(self, discord_ids: List[str], delay: float = 0.3) -> List[User]: + """ + Получение пользователей + :param delay: Значение задержки между запросами, указывается в секундах + :param discord_ids: List с IDs пользователей дискорда. + :return: List содержащий Classes pyspw.User.User + """ + return self._many_req(discord_ids, self.get_user, delay) + + def check_accesses(self, discord_ids: List[str], delay: float = 0.3) -> List[bool]: + """ + Получение статуса проходок + :param delay: Значение задержки между запросами, указывается в секундах + :param discord_ids: List с IDs пользователей дискорда. + :return: List содержащий bool со значением статуса проходки + """ + return self._many_req(discord_ids, self.check_access, delay) + + def create_payments(self, payments: List[Payment], delay: float = 0.5) -> List[str]: + """ + Создание ссылок на оплату + :param payments: Список содержащий classes PaymentParams + :param delay: Значение задержки между запросами, указывается в секундах + :return: List со ссылками на страницы оплаты, в том порядке, в котором они были в кортеже payments + """ + return self._many_req(payments, self.create_payment, delay) + + def send_transactions(self, transactions: List[Transaction], delay: float = 0.5) -> None: + """ + Отправка транзакций + :param delay: Значение задержки между запросами, указывается в секундах + :param transactions: Список содержащий classes TransactionParameters + :return: List со ссылками на страницы оплаты, в том порядке, в котором они были в кортеже payments + """ + self._many_req(transactions, self.send_transaction, delay) diff --git a/pyspw/errors.py b/pyspw/errors.py index f146581..f11e43b 100644 --- a/pyspw/errors.py +++ b/pyspw/errors.py @@ -1,34 +1,53 @@ -class Error(Exception): +class _Error(Exception): pass -class WebserverError(Error): +class _ApiError(_Error): pass -class NotFunction(WebserverError): +class SpwApiError(_ApiError): pass -class ApiError(Error): +class SpwApiDDOS(SpwApiError): + def __init__(self): + super().__init__("SPWorlds DDOS protection block your request") + + +class SpwUserNotFound(SpwApiError): + def __init__(self, discord_id: str): + self._discord_id = discord_id + super().__init__(f"User with discord id `{discord_id}` not found in spworlds") + + @property + def discord_id(self) -> str: + return self._discord_id + + +class SpwUnauthorized(SpwApiError): + def __init__(self): + super().__init__("Access details are invalid") + + +class SpwInsufficientFunds(SpwApiError): + def __init__(self): + super().__init__("Insufficient funds on the card") + + +class MojangApiError(_ApiError): pass -class SpwApiError(ApiError): - pass - - -class BadParameter(SpwApiError): - pass - - -class MojangApiError(ApiError): - pass - - -class SurgeplayApiError(ApiError): - pass - - -class BadSkinPartName(SurgeplayApiError): +class MojangAccountNotFound(MojangApiError): + def __init__(self, nickname: str): + self._nickname = nickname + super().__init__(f"Account with name `{nickname}` not found") + + @property + def nickname(self) -> str: + return self._nickname + + +class SurgeplayApiError(_ApiError): pass diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0de36b7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +requests~=2.30.0 +mojang~=1.1.0 +pydantic~=1.10.7 +setuptools~=65.5.1 +validators~=0.20.0 \ No newline at end of file diff --git a/setup.py b/setup.py index 98b614b..c894f6a 100644 --- a/setup.py +++ b/setup.py @@ -1,29 +1,28 @@ from os import path from setuptools import setup +from pyspw import __version__ + this_directory = path.abspath(path.dirname(__file__)) with open(path.join(this_directory, 'README.md'), encoding='utf-8') as f: description_md = f.read() -requirements = [ - 'requests==2.28.1', - 'mojang==0.2.0' -] +requirements = open('requirements.txt', 'r').read().split('\n') setup( name='Py-SPW', - version='1.4.3', + version=__version__, packages=['pyspw'], - url='https://github.com/teleport2/Py-SPW', + url='https://github.com/teleportx/Py-SPW', license='MIT License', author='Stepan Khozhempo', - author_email='stepan@m.khoz.ru', + author_email='stepan@khoz.ru', description='Python library for spworlds API', long_description=description_md, long_description_content_type='text/markdown', install_requires=requirements, - python_requires='>=3.10', + python_requires='>=3.7', project_urls={ "Docs": "https://github.com/teleport2/Py-SPW/wiki", "GitHub": "https://github.com/teleport2/Py-SPW"