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"