upload v 1.4.1

This commit is contained in:
Teleport
2022-07-23 01:15:56 +03:00
parent 0711eebf75
commit 8a216358f6
6 changed files with 307 additions and 134 deletions

45
pyspw/Parameters.py Normal file
View File

@@ -0,0 +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}
'''
class TransactionParameters:
def __init__(self, receiver: str, amount: int, comment: str = 'No comment'):
"""
Отправка транзакции
:param receiver: Номер карты на которую будет совершена транзакция.
:param amount: Сумма транзакции.
:param comment: Комментарий к транзакции.
:return: None.
"""
self.receiver = receiver
self.amount = amount
self.comment = comment
def __str__(self):
return f'''
receiver: {self.receiver}
amount: {str(self.amount)}
comment: {self.comment}
'''

100
pyspw/Skin.py Normal file
View File

@@ -0,0 +1,100 @@
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)

37
pyspw/User.py Normal file
View File

@@ -0,0 +1,37 @@
from typing import List, Dict, Any, Optional
from mojang import MojangAPI
from .Skin import Skin
class User:
def __init__(self, nickname: str | None, use_mojang_api: bool = True):
self.nickname = nickname
if self.nickname is not None:
self.access = True
if use_mojang_api:
self.uuid = MojangAPI.get_uuid(nickname)
else:
self.uuid = None
self.access = False
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)

View File

@@ -1,9 +1,11 @@
from . import api from . import api
from . import errors from . import errors
from . import User
from . import Skin
from . import Parameters
__all__ = ["Api", "errors"] __all__ = ["SpApi", "errors", "User", "Skin"]
__version__ = 1.0
class Api(api.sp_api_base): class SpApi(api.Py_SPW):
pass pass

View File

@@ -3,28 +3,31 @@ from hashlib import sha256
import hmac import hmac
import requests as rq import requests as rq
import time import time
from typing import Optional, List
import logging
from mojang import MojangAPI
from . import errors as err from . import errors as err
from .User import User
from .Parameters import PaymentParameters, TransactionParameters
accessed_body_part = ['face', 'front', 'frontfull', 'head', 'bust', 'full', 'skin'] # deesiigneer stole some of my ideas and improved them. But I didn't lose my head and improved what deesiigneer improved :)
class sp_api_base: class Py_SPW:
__spworlds_api_url = 'https://spworlds.ru/api/public'
def __init__(self, card_id: str, card_token: str): def __init__(self, card_id: str, card_token: str):
self.card_token = card_token self.__card_token = card_token
self.authorization = f"Bearer {str(b64encode(str(f'{card_id}:{card_token}').encode('utf-8')), 'utf-8')}" self.__authorization = f"Bearer {str(b64encode(str(f'{card_id}:{card_token}').encode('utf-8')), 'utf-8')}"
self.host = 'https://spworlds.ru/api/public'
def __str__(self):
pass
def __get(self, path: str = None, ignore_status_code: bool = False) -> rq.Response: def __get(self, path: str = None, ignore_status_code: bool = False) -> rq.Response:
headers = { headers = {
'Authorization': self.authorization, 'Authorization': self.__authorization,
'User-Agent': 'Py-SPW' 'User-Agent': 'Py-SPW'
} }
try: try:
response = rq.get(url=self.host + path, headers=headers) response = rq.get(url=self.__spworlds_api_url + path, headers=headers)
except rq.exceptions.ConnectionError as error: except rq.exceptions.ConnectionError as error:
raise err.SpwApiError(error) raise err.SpwApiError(error)
@@ -44,11 +47,11 @@ class sp_api_base:
def __post(self, path: str = None, body: dict = None) -> rq.Response: def __post(self, path: str = None, body: dict = None) -> rq.Response:
headers = { headers = {
'Authorization': self.authorization, 'Authorization': self.__authorization,
'User-Agent': 'Py-SPW' 'User-Agent': 'Py-SPW'
} }
try: try:
response = rq.post(url=self.host + path, headers=headers, json=body) response = rq.post(url=self.__spworlds_api_url + path, headers=headers, json=body)
except rq.exceptions.ConnectionError as error: except rq.exceptions.ConnectionError as error:
raise err.SpwApiError(error) raise err.SpwApiError(error)
@@ -62,19 +65,21 @@ class sp_api_base:
else: else:
raise err.SpwApiError(f'HTTP: {response.status_code} {response.json()["error"]}. Message: {response.json()["message"]}') raise err.SpwApiError(f'HTTP: {response.status_code} {response.json()["error"]}. Message: {response.json()["message"]}')
def get_user(self, discord_id: str) -> str | None: def get_user(self, discord_id: str, use_mojang_api: bool = True) -> User:
""" """
Получение ника пользователя. Получение пользователя
:param use_mojang_api: Если True то будет обращаться к Mojang API для получения UUID, иначе обращаться не будет
:param discord_id: ID пользователя дискорда. :param discord_id: ID пользователя дискорда.
:return: Str если пользователь найден, None если пользователь не найден. В str содержиться никнейм пользователя :return: Class pyspw.User.User
""" """
response = self.__get(f'/users/{discord_id}', True) response = self.__get(f'/users/{discord_id}', True)
if response.status_code == 200: if response.status_code == 200:
return response.json()['username'] return User(response.json()['username'], use_mojang_api)
elif response.status_code == 404: elif response.status_code == 404:
return None return User(None, use_mojang_api)
elif response.status_code >= 500: elif response.status_code >= 500:
raise err.SpwApiError(f'HTTP: {response.status_code}, Server Error.') raise err.SpwApiError(f'HTTP: {response.status_code}, Server Error.')
@@ -82,69 +87,64 @@ class sp_api_base:
else: else:
raise err.SpwApiError(f'HTTP: {response.status_code} {response.json()["error"]}. Message: {response.json()["message"]}') 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
def check_access(self, discord_id: str) -> bool: def check_access(self, discord_id: str) -> bool:
""" """
Получение статуса проходки. Получение статуса проходки
:param discord_id: ID пользователя дискорда. :param discord_id: ID пользователя дискорда.
:return: Bool - False если у пользователя не имеется проходки, True если у пользователя есть проходка :return: Bool True если у пользователя есть проходка, иначе False
""" """
return False if self.get_user(discord_id) is None else True return self.get_user(discord_id, False).access
def get_user_uuid(self, discord_id: str) -> str | None: def check_accesses(self, discord_ids: List[str], delay: float = 0.5) -> List[bool]:
username = self.get_user(discord_id)
if username is None:
return None
try:
mojang_response = rq.get(f'https://api.mojang.com/users/profiles/minecraft/{username}')
if mojang_response.status_code != 200:
raise err.MojangApiError(f'HTTP status: {mojang_response.status_code}')
return mojang_response.json()['id']
except rq.exceptions.ConnectionError as error:
raise err.MojangApiError(error)
except rq.exceptions.JSONDecodeError:
return None
def get_user_skin_url(self, discord_id: str, body_part: str, image_size: int = 64) -> str | None:
""" """
Получение изображения части скина. Получение статуса проходок
:param discord_id: ID пользователя дискорда. :param delay: Значение задержки между запросами, указывается в секундах
:param body_part: Часть тела для получения. Допустимые значения - https://visage.surgeplay.com/index.html :param discord_ids: List с IDs пользователей дискорда.
:param image_size: Размер получаемого изображения. :return: List содержащий bool со значением статуса проходки
:return: Str если пользователь найден, None если пользователь не найден. В str содержится ссылка на изображение профиля
""" """
if body_part not in accessed_body_part: accesses = []
raise err.BadSkinPartName(f'"{body_part}" is not a part of the skin')
uuid = self.get_user_uuid(discord_id) users = self.get_users(discord_ids, delay, False)
if uuid is None:
return None
return f'https://visage.surgeplay.com/{body_part}/{image_size}/{uuid}' 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')
def get_user_skin(self, discord_id: str, body_part: str, image_size: int = 64) -> bytes | None: for user in users:
""" if user is not None:
Получение изображения части скина. accesses.append(True)
:param discord_id: ID пользователя дискорда.
:param body_part: Часть тела для получения. Допустимые значения - https://visage.surgeplay.com/index.html
:param image_size: Размер получаемого изображения.
:return: Bytes если пользователь найден, None если пользователь не найден. В bytes содержиться изображение профиля
"""
url = self.get_user_skin_url(discord_id, body_part, image_size)
if url is None:
return None
try: else:
surgeplay_response = rq.get(url) accesses.append(False)
if surgeplay_response.status_code != 200:
raise err.SurgeplayApiError(f'HTTP status: {surgeplay_response.status_code}')
return surgeplay_response.content
except rq.exceptions.ConnectionError as error: return accesses
raise err.SurgeplayApiError(error)
def check_webhook(self, webhook_data: str, X_Body_Hash: str) -> bool: def check_webhook(self, webhook_data: str, X_Body_Hash: str) -> bool:
""" """
@@ -153,98 +153,78 @@ class sp_api_base:
:param X_Body_Hash: Хэдер X-Body-Hash из webhook. :param X_Body_Hash: Хэдер X-Body-Hash из webhook.
:return: Bool True если вебхук пришел от верифицированного сервера, иначе False :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) base64_data = b64encode(hmac_data)
return hmac.compare_digest(base64_data, X_Body_Hash.encode('utf-8')) return hmac.compare_digest(base64_data, X_Body_Hash.encode('utf-8'))
def create_payment(self, amount: int, redirectUrl: str, webhookUrl: str, data: str = '') -> str: def create_payment(self, params: PaymentParameters) -> str:
""" """
Создание ссылки на оплату Создание ссылки на оплату
:param amount: Стоимость покупки в АРах. :param params: class PaymentParams параметров оплаты
:param redirectUrl: URL страницы, на которую попадет пользователь после оплаты.
:param webhookUrl: URL, куда наш сервер направит запрос, чтобы оповестить ваш сервер об успешной оплате.
:param data: Строка до 100 символов, сюда можно пометить любые полезные данных.
:return: Str ссылка на страницу оплаты, на которую стоит перенаправить пользователя. :return: Str ссылка на страницу оплаты, на которую стоит перенаправить пользователя.
""" """
body = { body = {
'amount': amount, 'amount': params.amount,
'redirectUrl': redirectUrl, 'redirectUrl': params.redirectUrl,
'webhookUrl': webhookUrl, 'webhookUrl': params.webhookUrl,
'data': data 'data': params.data
} }
return self.__post('/payment', body).json()['url'] return self.__post('/payment', body).json()['url']
def create_payments(self, payments: tuple, request_delay: float = 0.1) -> list: def create_payments(self, payments: List[PaymentParameters], delay: float = 0.5) -> list:
""" """
Создание ссылок на оплату Создание ссылок на оплату
:param request_delay: Значение задержки между запросами, указывается в секундах :param payments: Список содержащий classes PaymentParams
:param payments: Кортеж содержащий словари со следующими параметрами: :param delay: Значение задержки между запросами, указывается в секундах
:parameter amount: Стоимость покупки в АРах. :return: List со ссылками на страницы оплаты, в том порядке, в котором они были в кортеже payments
:parameter redirectUrl: URL страницы, на которую попадет пользователь после оплаты.
:parameter webhookUrl: URL, куда наш сервер направит запрос, чтобы оповестить ваш сервер об успешной оплате.
:parameter data: Строка до 100 символов, сюда можно пометить любые полезные данных.
:return: List с ссылками на страницы оплаты, в том порядке, в котором они были в кортеже payments
""" """
answer = [] 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: for payment in payments:
try: answer.append(self.create_payment(payment))
answer.append(self.create_payment( time.sleep(delay)
int(payment['amount']),
str(payment['redirectUrl']),
str(payment['webhookUrl']),
str(payment['data'])
))
except ValueError:
raise err.BadParameter('Amount must be int')
except KeyError as error:
raise err.BadParameter(f'Missing parameter {error}')
time.sleep(request_delay)
return answer return answer
def send_transaction(self, receiver: str, amount: int, comment: str = 'Нет комментария') -> None: def send_transaction(self, params: TransactionParameters) -> None:
""" """
Отправка транзакции Отправка транзакции
:param receiver: Номер карты на которую будет совершена транзакция. :param params: class TransactionParameters параметры транзакции
:param amount: Сумма транзакции.
:param comment: Комментарий к транзакции.
:return: None. :return: None.
""" """
body = { body = {
'receiver': receiver, 'receiver': params.receiver,
'amount': amount, 'amount': params.amount,
'comment': comment 'comment': params.comment
} }
self.__post('/transactions', body) self.__post('/transactions', body)
def send_transactions(self, transactions: tuple, request_delay: float = 0.1) -> None: def send_transactions(self, transactions: List[TransactionParameters], delay: float = 0.1) -> None:
""" """
Отправка транзакций Отправка транзакций
:param request_delay: Значение задержки между запросами, указывается в секундах :param delay: Значение задержки между запросами, указывается в секундах
:param transactions: Кортеж содержащий словари со следующими параметрами: :param transactions: Список содержащий classes TransactionParameters
:param receiver: Номер карты на которую будет совершена транзакция. :return: List со ссылками на страницы оплаты, в том порядке, в котором они были в кортеже payments
:param amount: Сумма транзакции.
:param comment: Комментарий к транзакции.
: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: for transaction in transactions:
try: self.send_transaction(transaction)
self.send_transaction( time.sleep(delay)
str(transaction['receiver']),
int(transaction['amount']),
str(transaction['comment'])
)
except ValueError:
raise err.BadParameter('Amount must be int')
except KeyError as error:
raise err.BadParameter(f'Missing parameter {error}')
time.sleep(request_delay)
def get_balance(self) -> int: def get_balance(self) -> int:
"""
Получение баланса
:return: Int со значением баланса
"""
return self.__get('/card').json()['balance'] return self.__get('/card').json()['balance']

View File

@@ -6,11 +6,14 @@ this_directory = path.abspath(path.dirname(__file__))
with open(path.join(this_directory, 'README.md'), encoding='utf-8') as f: with open(path.join(this_directory, 'README.md'), encoding='utf-8') as f:
description_md = f.read() description_md = f.read()
requires = ['requests==2.28.1'] requirements = [
'requests==2.28.1',
'mojang==0.2.0'
]
setup( setup(
name='Py-SPW', name='Py-SPW',
version='1.3', version='1.4.1',
packages=['pyspw'], packages=['pyspw'],
url='https://github.com/teleport2/Py-SPW', url='https://github.com/teleport2/Py-SPW',
license='MIT License', license='MIT License',
@@ -18,5 +21,11 @@ setup(
author_email='stepan@m.khoz.ru', author_email='stepan@m.khoz.ru',
description='Python library for spworlds API', description='Python library for spworlds API',
long_description=description_md, long_description=description_md,
long_description_content_type='text/markdown' long_description_content_type='text/markdown',
install_requires=requirements,
python_requires='>=3.10.5',
project_urls={
"Docs": "https://github.com/teleport2/Py-SPW/wiki",
"GitHub": "https://github.com/teleport2/Py-SPW"
},
) )