From ba766161dccb8393427ba549d2091c0f77d5d200 Mon Sep 17 00:00:00 2001 From: MadCat9958 Date: Sat, 3 Jun 2023 23:12:20 +0300 Subject: [PATCH 01/34] base --- boticordpy/http.py | 46 +++------------- boticordpy/types.py | 119 ++++++++++++++++-------------------------- boticordpy/webhook.py | 2 + 3 files changed, 54 insertions(+), 113 deletions(-) diff --git a/boticordpy/http.py b/boticordpy/http.py index c8ca187..93b055e 100644 --- a/boticordpy/http.py +++ b/boticordpy/http.py @@ -20,7 +20,7 @@ class HttpClient: loop: `asyncio loop` """ - def __init__(self, auth_token: str, version: int = 1, **kwargs): + def __init__(self, auth_token: str, version: int = 3, **kwargs): self.token = auth_token self.API_URL = f"https://api.boticord.top/v{version}/" @@ -60,56 +60,26 @@ class HttpClient: def get_bot_info(self, bot_id: int): """Get information about the specified bot""" - return self.make_request("GET", f"bot/{bot_id}") + return self.make_request("GET", f"bots/{bot_id}") + # TODO def get_bot_comments(self, bot_id: int): """Get list of specified bot comments""" return self.make_request("GET", f"bot/{bot_id}/comments") - def post_bot_stats(self, stats: dict): + def post_bot_stats(self, bot_id: int, stats: dict): """Post bot's stats""" - return self.make_request("POST", "stats", json=stats) + return self.make_request("POST", f"bots/{bot_id}", json=stats) def get_server_info(self, server_id: int): """Get information about specified server""" - return self.make_request("GET", f"server/{server_id}") + return self.make_request("GET", f"servers/{server_id}") + # TODO def get_server_comments(self, server_id: int): """Get list of specified server comments""" return self.make_request("GET", f"server/{server_id}/comments") - def post_server_stats(self, payload: dict): - """Post server's stats""" - return self.make_request("POST", "server", json=payload) - def get_user_info(self, user_id: int): """Get information about the user""" - return self.make_request("GET", f"profile/{user_id}") - - def get_user_comments(self, user_id: int): - """Get specified user's comments""" - return self.make_request("GET", f"user/{user_id}/comments") - - def get_user_bots(self, user_id: int): - """Get bots of specified user""" - return self.make_request("GET", f"bots/{user_id}") - - def get_my_shorted_links(self, code: str = None): - """Get shorted links of an authorized user""" - body = {"code": code} if code is not None else {} - - return self.make_request("POST", "links/get", json=body) - - def create_shorted_link(self, code: str, link: str, *, domain: LinkDomain = 1): - """Create new shorted link""" - return self.make_request( - "POST", - "links/create", - json={"code": code, "link": link, "domain": int(domain)}, - ) - - def delete_shorted_link(self, code: str, domain: LinkDomain = 1): - """Delete shorted link""" - return self.make_request( - "POST", "links/delete", json={"code": code, "domain": int(domain)} - ) + return self.make_request("GET", f"users/{user_id}") diff --git a/boticordpy/types.py b/boticordpy/types.py index 8998a36..9f44e7a 100644 --- a/boticordpy/types.py +++ b/boticordpy/types.py @@ -98,105 +98,74 @@ class Bot(ApiData): id: str """Bot's Id""" - short_code: typing.Optional[str] - """Bot's page short code""" + name: str + """Bot's name""" - page_links: list - """List of bot's page urls""" - - server: dict - """Bot's support server""" - - bumps: int - """Bumps count""" - - added: str - """How many times users have added the bot?""" - - prefix: str - """Bot's commands prefix""" - - permissions: int - """Bot's permissions""" - - tags: list - """Bot's search-tags""" - - developers: list - """List of bot's developers Ids""" - - links: typing.Optional[dict] - """Bot's social medias""" - - library: typing.Optional[str] - """Bot's library""" - - short_description: typing.Optional[str] + shortDescription: str """Bot's short description""" - long_description: typing.Optional[str] + description: str """Bot's long description""" - badge: typing.Optional[str] - """Bot's badge""" + avatar: typing.Optional[str] + """Bot's avatar""" - stats: dict - """Bot's stats""" + shortLink: typing.Optional[str] + """Bot's page short code""" - status: str - """Bot's approval status""" + inviteLink: str + """Bot's invite link""" - def __init__(self, **kwargs): - super().__init__(**parse_with_information_dict(kwargs)) + premiumActive: bool + """Bot's premium status""" + premiumSplashURL: typing.Optional[str] + """Bot's splash URL""" -class Server(ApiData): - """This model represents a server, returned from the Boticord API""" + premiumAutoFetch: typing.Optional[bool] + """Bot's auto fetch status""" - id: str - """Server's Id""" + standardBannerID: int + """Bot's standart banner ID""" - short_code: typing.Optional[str] - """Server's page short code""" + premiumBannerURL: typing.Optional[str] + """Bot's premium banner URL""" - status: str - """Server's approval status""" + owner: str + """Bot's owner""" - page_links: list - """List of server's page urls""" + status: int + """Bot's status""" - bot: dict - """Bot where this server is used for support users""" + prefix: str + """Bot's prefix""" - name: str - """Name of the server""" + discriminator: str + """Bot's discriminator (soon deprecated)""" - avatar: str - """Server's avatar""" + createdDate: str + """Bot's creation date""" - members: list - """Members counts - `[all, online]`""" + supportServerInviteLink: typing.Optional[str] + """Bot's support server""" - owner: typing.Optional[str] - """Server's owner Id""" + library: typing.Optional[int] + """Bot's library""" - bumps: int - """Bumps count""" + guilds: typing.Optional[int] + """Bot's guilds count""" - tags: list - """Server's search-tags""" + shards: typing.Optional[int] + """Bot's shards count""" - links: dict - """Server's social medias""" + members: typing.Optional[int] + """Bot's members count""" - short_description: typing.Optional[str] - """Server's short description""" + website: typing.Optional[str] + """Bot's website""" - long_description: typing.Optional[str] - """Server's long description""" - - badge: typing.Optional[str] - """Server's badge""" + upCount: int + """Bot's up count""" def __init__(self, **kwargs): super().__init__(**parse_with_information_dict(kwargs)) diff --git a/boticordpy/webhook.py b/boticordpy/webhook.py index 49aa084..2535f9d 100644 --- a/boticordpy/webhook.py +++ b/boticordpy/webhook.py @@ -11,6 +11,8 @@ class Webhook: """Represents a client that can be used to work with BotiCord Webhooks. IP of the server - your machine IP. (`0.0.0.0`) + Note: you need to have IPV6 support to use this + Args: x_hook_key (:obj:`str`) X-hook-key to check the auth of incoming request. From 7b9e341adcf9eff15f1a62bee036eadbc510aeac Mon Sep 17 00:00:00 2001 From: grey-cat-1908 Date: Sun, 4 Jun 2023 10:42:33 +0300 Subject: [PATCH 02/34] rewrited bots --- boticordpy/__init__.py | 1 - boticordpy/autopost.py | 15 +- boticordpy/client.py | 68 ++-- boticordpy/exceptions.py | 130 +++++- boticordpy/http.py | 47 +-- boticordpy/types.py | 811 +++++++++++++++++++++++-------------- boticordpy/webhook.py | 135 ------ requirements.txt | 1 + tests/test_convertation.py | 43 ++ tests/test_converting.py | 141 ------- 10 files changed, 732 insertions(+), 660 deletions(-) delete mode 100644 boticordpy/webhook.py create mode 100644 tests/test_convertation.py delete mode 100644 tests/test_converting.py diff --git a/boticordpy/__init__.py b/boticordpy/__init__.py index 5fe812c..9e868ea 100644 --- a/boticordpy/__init__.py +++ b/boticordpy/__init__.py @@ -13,6 +13,5 @@ __copyright__ = "Copyright 2022 Marakarka" __version__ = "2.2.2" from .client import BoticordClient -from .webhook import Webhook from .types import * diff --git a/boticordpy/autopost.py b/boticordpy/autopost.py index 6c09de9..c463f09 100644 --- a/boticordpy/autopost.py +++ b/boticordpy/autopost.py @@ -28,12 +28,16 @@ class AutoPost: _stats: typing.Any _task: typing.Optional["asyncio.Task[None]"] + bot_id: str + def __init__(self, client): self.client = client self._stopped: bool = False self._interval: int = 900 self._task: typing.Optional["asyncio.Task[None]"] = None + self.bot_id = None + @property def is_running(self) -> bool: """ @@ -145,7 +149,7 @@ class AutoPost: while True: stats = await self._stats() try: - await self.client.http.post_bot_stats(stats) + await self.client.http.post_bot_stats(self.bot_id, stats) except Exception as err: on_error = getattr(self, "_error", None) if on_error: @@ -160,14 +164,21 @@ class AutoPost: await asyncio.sleep(self._interval) - def start(self): + def start(self, bot_id: typing.Union[str, int]): """ Starts the loop. + Args: + bot_id ( Union[:obj:`int`, :obj:`str`] ) + Id of the bot to send stats of. + Raises: :obj:`~.exceptions.InternalException` If there's no callback (for getting stats) provided or the autopost is already running. """ + + self.bot_id = bot_id + if not hasattr(self, "_stats"): raise bexc.InternalException("You must provide stats") diff --git a/boticordpy/client.py b/boticordpy/client.py index 9cbdb76..18c7236 100644 --- a/boticordpy/client.py +++ b/boticordpy/client.py @@ -8,11 +8,6 @@ from .autopost import AutoPost class BoticordClient: """Represents a client that can be used to interact with the BotiCord API. - .. warning:: - - In BotiCord API v2 there are some changes with token. - `Read more here `_ - Note: Remember that every http method can return an http exception. @@ -20,52 +15,47 @@ class BoticordClient: token (:obj:`str`) Your bot's Boticord API Token. version (:obj:`int`) - BotiCord API version (Default: 2) + BotiCord API version (Default: 3) """ __slots__ = ("http", "_autopost", "_token") http: HttpClient - def __init__(self, token: str = None, version: int = 2): + def __init__(self, token: str = None, version: int = 3): self._token = token self._autopost: typing.Optional[AutoPost] = None self.http = HttpClient(token, version) - async def get_bot_info(self, bot_id: int) -> boticord_types.Bot: + async def get_bot_info( + self, bot_id: typing.Union[str, int] + ) -> boticord_types.ResourceBot: """Gets information about specified bot. Args: - bot_id (:obj:`int`) + bot_id (Union[:obj:`str`, :obj:`int`]) Id of the bot Returns: - :obj:`~.types.Bot`: - Bot object. + :obj:`~.types.ResourceBot`: + ResourceBot object. """ response = await self.http.get_bot_info(bot_id) - return boticord_types.Bot(**response) - - async def get_bot_comments(self, bot_id: int) -> list: - """Gets list of comments of specified bot. - - Args: - bot_id (:obj:`int`) - Id of the bot - - Returns: - :obj:`list` [ :obj:`~.types.SingleComment` ]: - List of comments. - """ - response = await self.http.get_bot_comments(bot_id) - return [boticord_types.SingleComment(**comment) for comment in response] + return boticord_types.ResourceBot.from_dict(response) async def post_bot_stats( - self, servers: int = 0, shards: int = 0, users: int = 0 - ) -> dict: + self, + bot_id: typing.Union[str, int], + *, + servers: int = 0, + shards: int = 0, + users: int = 0, + ) -> boticord_types.ResourceBot: """Post Bot's stats. Args: + bot_id (Union[:obj:`str`, :obj:`) + Id of the bot to post stats of. servers ( :obj:`int` ) Bot's servers count shards ( :obj:`int` ) @@ -73,15 +63,15 @@ class BoticordClient: users ( :obj:`int` ) Bot's users count Returns: - :obj:`dict`: - Boticord API Response status + :obj:`~.types.ResourceBot`: + ResourceBot object. """ response = await self.http.post_bot_stats( - {"servers": servers, "shards": shards, "users": users} + bot_id, {"servers": servers, "shards": shards, "users": users} ) - return response + return boticord_types.ResourceBot.from_dict(response) - async def get_server_info(self, server_id: int) -> boticord_types.Server: + async def get_server_info(self, server_id: int): """Gets information about specified server. Args: @@ -124,7 +114,7 @@ class BoticordClient: response = await self.http.post_server_stats(payload) return response - async def get_user_info(self, user_id: int) -> boticord_types.UserProfile: + async def get_user_info(self, user_id: int): """Gets information about specified user. Args: @@ -138,7 +128,7 @@ class BoticordClient: response = await self.http.get_user_info(user_id) return boticord_types.UserProfile(**response) - async def get_user_comments(self, user_id: int) -> boticord_types.UserComments: + async def get_user_comments(self, user_id: int): """Gets comments of specified user. Args: @@ -185,9 +175,7 @@ class BoticordClient: else boticord_types.ShortedLink(**response[0]) ) - async def create_shorted_link( - self, *, code: str, link: str, domain: boticord_types.LinkDomain = 1 - ): + async def create_shorted_link(self, *, code: str, link: str, domain=1): """Creates new shorted link Args: @@ -206,9 +194,7 @@ class BoticordClient: return boticord_types.ShortedLink(**response) - async def delete_shorted_link( - self, code: str, domain: boticord_types.LinkDomain = 1 - ): + async def delete_shorted_link(self, code: str, domain=1): """Deletes shorted link Args: diff --git a/boticordpy/exceptions.py b/boticordpy/exceptions.py index be501c9..0f43862 100644 --- a/boticordpy/exceptions.py +++ b/boticordpy/exceptions.py @@ -1,3 +1,6 @@ +from enum import IntEnum + + class BoticordException(Exception): """Base exception class for boticordpy. This could be caught to handle any exceptions thrown from this library. @@ -29,26 +32,131 @@ class HTTPException(BoticordException): def __init__(self, response): self.response = response - fmt = f"{self.response.reason} (Status code: {self.response.status})" + fmt = f"{HTTPErrors(self.response['error']).name} (Status code: {StatusCodes(self.response['status']).name})" super().__init__(fmt) -class Unauthorized(HTTPException): - """Exception that's thrown when status code 401 occurs.""" +class StatusCodes(IntEnum): + """Status codes of response""" + + SERVER_ERROR = 500 + """Server Error (>500)""" + + TOO_MANY_REQUESTS = 429 + """Too Many Requests""" + + NOT_FOUND = 404 + """Requested resource was not found""" + + FORBIDDEN = 403 + """You don't have access to this resource""" + + UNAUTHORIZED = 401 + """Authorization is required to access this resource""" + + BAD_REQUEST = 400 + """Bad Request""" -class Forbidden(HTTPException): - """Exception that's thrown when status code 403 occurs.""" +class HTTPErrors(IntEnum): + """Errors which BotiCord may return""" + UNKNOWN_ERROR = 0 + """Unknown error""" -class NotFound(HTTPException): - """Exception that's thrown when status code 404 occurs.""" + INTERNAL_SERVER_ERROR = 1 + """Server error (>500)""" + RATE_LIMITED = 2 + """Too many requests""" -class ToManyRequests(HTTPException): - """Exception that's thrown when status code 429 occurs.""" + NOT_FOUND = 3 + """Not found""" + FORBIDDEN = 4 + """Access denied""" -class ServerError(HTTPException): - """Exception that's thrown when status code 500 or 503 occurs.""" + BAD_REQUEST = 5 + """Bad request""" + + UNAUTHORIZED = 6 + """Unauthorized. Authorization required""" + + RPC_ERROR = 7 + """Server error (RPC)""" + + WS_ERROR = 8 + """Server error (WS)""" + + THIRD_PARTY_FAIL = 9 + """Third-party service error""" + + UNKNOWN_USER = 10 + """Unknown user""" + + SHORT_DOMAIN_TAKEN = 11 + """Short link already taken""" + + UNKNOWN_SHORT_DOMAIN = 12 + """Unknown short link""" + + UNKNOWN_LIBRARY = 13 + """Unknown library""" + + TOKEN_INVALID = 14 + """Invalid token""" + + UNKNOWN_RESOURCE = 15 + """Unknown resource""" + + UNKNOWN_TAG = 16 + """Unknown tag""" + + PERMISSION_DENIED = 17 + """Insufficient permissions""" + + UNKNOWN_COMMENT = 18 + """Unknown comment""" + + UNKNOWN_BOT = 19 + """Unknown bot""" + + UNKNOWN_SERVER = 20 + """Unknown server""" + + UNKNOWN_BADGE = 21 + """Unknown badge""" + + USER_ALREADY_HAS_A_BADGE = 22 + """User already has a badge""" + + INVALID_INVITE_CODE = 23 + """Invalid invite code""" + + SERVER_ALREADY_EXISTS = 24 + """Server already exists""" + + BOT_NOT_PRESENT_ON_QUEUE_SERVER = 25 + """Bot not present on queue server""" + + UNKNOWN_UP = 26 + """Unknown up""" + + TOO_MANY_UPS = 27 + """Too many ups""" + + INVALID_STATUS = 28 + """Invalid resource status""" + + UNKNOWN_REPORT = 29 + """Unknown report""" + + UNSUPPORTED_MEDIA_TYPE = 30 + """Unsupported media type. Should be one of""" + + UNKNOWN_APPLICATION = 31 + """Unknown application""" + + AUTOMATED_REQUESTS_NOT_ALLOWED = 32 + """Please confirm that you are not a robot by refreshing the page""" diff --git a/boticordpy/http.py b/boticordpy/http.py index c8ca187..77c0c63 100644 --- a/boticordpy/http.py +++ b/boticordpy/http.py @@ -1,4 +1,5 @@ import asyncio +import typing import aiohttp @@ -20,9 +21,9 @@ class HttpClient: loop: `asyncio loop` """ - def __init__(self, auth_token: str, version: int = 1, **kwargs): + def __init__(self, auth_token: str = None, version: int = 3, **kwargs): self.token = auth_token - self.API_URL = f"https://api.boticord.top/v{version}/" + self.API_URL = f"https://api.arbuz.pro/" loop = kwargs.get("loop") or asyncio.get_event_loop() @@ -31,44 +32,30 @@ class HttpClient: async def make_request(self, method: str, endpoint: str, **kwargs): """Send requests to the API""" - kwargs["headers"] = { - "Content-Type": "application/json", - "Authorization": self.token, - } + kwargs["headers"] = {"Content-Type": "application/json"} + + if self.token is not None: + kwargs["headers"]["Authorization"] = self.token url = f"{self.API_URL}{endpoint}" async with self.session.request(method, url, **kwargs) as response: data = await response.json() - if response.status == 200: - return data - elif response.status == 401: - raise exceptions.Unauthorized(response) - elif response.status == 403: - raise exceptions.Forbidden(response) - elif response.status == 404: - raise exceptions.NotFound(response) - elif response.status == 429: - raise exceptions.ToManyRequests(response) - elif response.status == 500: - raise exceptions.ServerError(response) - elif response.status == 503: - raise exceptions.ServerError(response) + if response.status == 200 or response.status == 201: + return data["result"] + else: + raise exceptions.HTTPException( + {"status": response.status, "error": data["errors"][0]["code"]} + ) - raise exceptions.HTTPException(response) - - def get_bot_info(self, bot_id: int): + def get_bot_info(self, bot_id: typing.Union[str, int]): """Get information about the specified bot""" - return self.make_request("GET", f"bot/{bot_id}") + return self.make_request("GET", f"bots/{bot_id}") - def get_bot_comments(self, bot_id: int): - """Get list of specified bot comments""" - return self.make_request("GET", f"bot/{bot_id}/comments") - - def post_bot_stats(self, stats: dict): + def post_bot_stats(self, bot_id: typing.Union[str, int], stats: dict): """Post bot's stats""" - return self.make_request("POST", "stats", json=stats) + return self.make_request("POST", f"bots/{bot_id}/stats", json=stats) def get_server_info(self, server_id: int): """Get information about specified server""" diff --git a/boticordpy/types.py b/boticordpy/types.py index 8998a36..7bd826a 100644 --- a/boticordpy/types.py +++ b/boticordpy/types.py @@ -1,375 +1,588 @@ -import typing -from enum import IntEnum +from datetime import datetime, timezone +from enum import IntEnum, Enum, EnumMeta +import copy +from dataclasses import _is_dataclass_instance, fields, dataclass +from typing import ( + Dict, + Union, + Generic, + Tuple, + TypeVar, + get_origin, + get_args, + Optional, + List, +) +from sys import modules +from itertools import chain -KT = typing.TypeVar("KT") -VT = typing.TypeVar("VT") +from typing_extensions import get_type_hints -def parse_response_dict(input_data: dict) -> dict: - data = input_data.copy() - - for key, value in data.copy().items(): - converted_key = "".join( - ["_" + x.lower() if x.isupper() else x for x in key] - ).lstrip("_") - - if key != converted_key: - del data[key] - - data[converted_key] = value - - return data +KT = TypeVar("KT") +VT = TypeVar("VT") +T = TypeVar("T") -def parse_with_information_dict(bot_data: dict) -> dict: - data = bot_data.copy() +class Singleton(type): + # Thanks to this stackoverflow answer (method 3): + # https://stackoverflow.com/q/6760685/12668716 + _instances = {} - for key, value in data.copy().items(): - if key.lower() == "links": - converted_key = "page_links" - else: - converted_key = "".join( - ["_" + x.lower() if x.isupper() else x for x in key] - ).lstrip("_") - - if key != converted_key: - del data[key] - - if key.lower() == "information": - for information_key, information_value in value.copy().items(): - converted_information_key = "".join( - ["_" + x.lower() if x.isupper() else x for x in information_key] - ).lstrip("_") - - data[converted_information_key] = information_value - - del data["information"] - else: - data[converted_key] = value - - return data + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) + return cls._instances[cls] -def parse_user_comments_dict(response_data: dict) -> dict: - data = response_data.copy() +class TypeCache(metaclass=Singleton): + # Thanks to Pincer Devs. This class is from the Pincer Library. + cache = {} - for key, value in data.copy().items(): - data[key] = [SingleComment(**comment) for comment in value] + def __init__(self): + lcp = modules.copy() + for module in lcp: + if not module.startswith("melisa"): + continue - return data + TypeCache.cache.update(lcp[module].__dict__) -class ApiData(dict, typing.MutableMapping[KT, VT]): - """Base class used to represent received data from the API.""" +def _asdict_ignore_none(obj: Generic[T]) -> Union[Tuple, Dict, T]: + """ + Returns a dict from a dataclass that ignores + all values that are None + Modification of _asdict_inner from dataclasses + Parameters + ---------- + obj: Generic[T] + The object to convert + Returns + ------- + A dict without None values + """ - def __init__(self, **kwargs: VT) -> None: - super().__init__(**parse_response_dict(kwargs)) - self.__dict__ = self + print(obj) + + if _is_dataclass_instance(obj): + result = [] + for f in fields(obj): + value = _asdict_ignore_none(getattr(obj, f.name)) + + if isinstance(value, Enum): + result.append((f.name, value.value)) + elif not f.name.startswith("_"): + result.append((f.name, value)) + + return dict(result) + + elif isinstance(obj, tuple) and hasattr(obj, "_fields"): + return type(obj)(*[_asdict_ignore_none(v) for v in obj]) + + elif isinstance(obj, (list, tuple)): + return type(obj)(_asdict_ignore_none(v) for v in obj) + + elif isinstance(obj, datetime): + return str(round(obj.timestamp() * 1000)) + + elif isinstance(obj, dict): + return type(obj)( + (_asdict_ignore_none(k), _asdict_ignore_none(v)) for k, v in obj.items() + ) + else: + return copy.deepcopy(obj) -class SingleComment(ApiData): - """This model represents single comment""" +class APIObjectBase: + """ + Represents an object which has been fetched from the BotiCord API. + """ - user_id: str - """Comment's author Id (`str`)""" + def __attr_convert(self, attr_value: Dict, attr_type: T) -> T: + factory = attr_type - text: str - """Comment content""" + # Always use `__factory__` over __init__ + if getattr(attr_type, "__factory__", None): + factory = attr_type.__factory__ - vote: int - """Comment vote value (`-1,` `0`, `1`)""" + if attr_value is None: + return None - is_updated: bool - """Was comment updated?""" + if attr_type is not None and isinstance(attr_value, attr_type): + return attr_value - created_at: int - """Comment Creation date timestamp""" + if isinstance(attr_value, dict): + return factory(attr_value) - updated_at: int - """Last edit date timestamp""" + return factory(attr_value) - def __init__(self, **kwargs): - super().__init__(**parse_response_dict(kwargs)) + def __post_init__(self): + TypeCache() + + attributes = chain.from_iterable( + get_type_hints(cls, globalns=TypeCache.cache).items() + for cls in chain(self.__class__.__bases__, (self,)) + ) + + for attr, attr_type in attributes: + # Ignore private attributes. + if attr.startswith("_"): + continue + + types = self.__get_types(attr, attr_type) + + types = tuple(filter(lambda tpe: tpe is not None, types)) + + if not types: + raise ValueError( + f"Attribute `{attr}` in `{type(self).__name__}` only " + "consisted of missing/optional type!" + ) + + specific_tp = types[0] + + attr_gotten = getattr(self, attr) + + if tp := get_origin(specific_tp): + specific_tp = tp + + if isinstance(specific_tp, EnumMeta) and not attr_gotten: + attr_value = None + elif tp == list and attr_gotten and (classes := get_args(types[0])): + attr_value = [ + self.__attr_convert(attr_item, classes[0]) + for attr_item in attr_gotten + ] + elif tp == dict and attr_gotten and (classes := get_args(types[0])): + attr_value = { + key: self.__attr_convert(value, classes[1]) + for key, value in attr_gotten.items() + } + else: + attr_value = self.__attr_convert(attr_gotten, specific_tp) + + setattr(self, attr, attr_value) + + def __get_types(self, attr: str, arg_type: type) -> Tuple[type]: + origin = get_origin(arg_type) + + if origin is Union: + # Ahh yes, typing module has no type annotations for this... + # noinspection PyTypeChecker + args: Tuple[type] = get_args(arg_type) + + if 2 <= len(args) < 4: + return args + + raise ValueError( + f"Attribute `{attr}` in `{type(self).__name__}` has too many " + f"or not enough arguments! (got {len(args)} expected 2-3)" + ) + + return (arg_type,) + + @classmethod + def __factory__(cls: Generic[T], *args, **kwargs) -> T: + return cls.from_dict(*args, **kwargs) + + def __repr__(self): + attrs = ", ".join( + f"{k}={v!r}" + for k, v in self.__dict__.items() + if v and not k.startswith("_") + ) + + return f"{type(self).__name__}({attrs})" + + def __str__(self): + if _name := getattr(self, "__name__", None): + return f"{_name} {self.__class__.__name__.lower()}" + + return super().__str__() + + def to_dict(self) -> Dict: + """ + Transform the current object to a dictionary representation. Parameters that + start with an underscore are not serialized. + """ + return _asdict_ignore_none(self) -class Bot(ApiData): - """This model represents a bot, returned from the BotiCord API""" +class BotLibrary(IntEnum): + """The library that the bot is based on""" + + DISCORD4J = 1 + DISCORDCR = 2 + DISCORDGO = 3 + DISCORDDOO = 4 + DSHARPPLUS = 5 + DISCORDJS = 6 + DISCORDNET = 7 + DISCORDPY = 8 + ERIS = 9 + JAVACORD = 10 + JDA = 11 + OTHER = 12 + + +class ResourceStatus(IntEnum): + """Bot status on monitoring""" + + HIDDEN = 0 + """Bot is hidden""" + + PUBLIC = 1 + """Bot is public""" + + BANNED = 2 + """Bot is banned""" + + PENDING = 3 + """Bor is pending""" + + +class BotTag(IntEnum): + """Tags of the bot""" + + MODERATION = 0 + """Moderation""" + + BOT = 1 + """Bot""" + + UTILITIES = 2 + """Utilities""" + + ENTERTAINMENT = 3 + """Entertainment""" + + MUSIC = 4 + """Music""" + + ECONOMY = 5 + """Economy""" + + LOGS = 6 + """Logs""" + + LEVELS = 7 + """Levels""" + + NSFW = 8 + """NSFW (18+)""" + + SETTINGS = 9 + """Settings""" + + ROLE_PLAY = 10 + """Role-Play""" + + MEMES = 11 + """Memes""" + + GAMES = 12 + """Games""" + + AI = 13 + """AI""" + + +@dataclass(repr=False) +class UserLinks(APIObjectBase): + """Links of the userk""" + + vk: Optional[str] + """vk.com""" + + telegram: Optional[str] + """t.me""" + + donate: Optional[str] + """Donate""" + + git: Optional[str] + """Link to git of the user""" + + custom: Optional[str] + """Custom link""" + + @classmethod + def from_dict(cls, data: dict): + """Generate a UserLinks from the given data. + + Parameters + ---------- + data: :class:`dict` + The dictionary to convert into a UserLinks. + """ + + self: ResourceUp = super().__new__(cls) + + self.vk = data.get("vk") + self.telegram = data.get("telegram") + self.donate = data.get("donate") + self.git = data.get("git") + self.custon = data.get("custom") + + return self + + +@dataclass(repr=False) +class ResourceUp(APIObjectBase): + """Information about bump (bot/server)""" id: str - """Bot's Id""" + """Bump's id""" - short_code: typing.Optional[str] - """Bot's page short code""" + expires: datetime + """Expiration date. (ATTENTION! When using `to_dict()`, the data may not correspond to the actual data due to the peculiarities of the `datetime` module)""" - page_links: list - """List of bot's page urls""" + @classmethod + def from_dict(cls, data: dict): + """Generate a ResourceUp from the given data. - server: dict - """Bot's support server""" + Parameters + ---------- + data: :class:`dict` + The dictionary to convert into a ResourceUp. + """ - bumps: int - """Bumps count""" + self: ResourceUp = super().__new__(cls) - added: str - """How many times users have added the bot?""" + self.id = data["id"] + self.expires = datetime.fromtimestamp( + int(int(data["expires"]) / 1000), tz=timezone.utc + ) - prefix: str - """Bot's commands prefix""" - - permissions: int - """Bot's permissions""" - - tags: list - """Bot's search-tags""" - - developers: list - """List of bot's developers Ids""" - - links: typing.Optional[dict] - """Bot's social medias""" - - library: typing.Optional[str] - """Bot's library""" - - short_description: typing.Optional[str] - """Bot's short description""" - - long_description: typing.Optional[str] - """Bot's long description""" - - badge: typing.Optional[str] - """Bot's badge""" - - stats: dict - """Bot's stats""" - - status: str - """Bot's approval status""" - - def __init__(self, **kwargs): - super().__init__(**parse_with_information_dict(kwargs)) + return self -class Server(ApiData): - """This model represents a server, returned from the Boticord API""" +@dataclass(repr=False) +class ResourceRating(APIObjectBase): + """Rating of bot/server""" + + count: int + """Number of ratings""" + + rating: int + """Rating (from 1 to 5)""" + + @classmethod + def from_dict(cls, data: dict): + """Generate a ResourceRating from the given data. + + Parameters + ---------- + data: :class:`dict` + The dictionary to convert into a ResourceRating. + """ + + self: ResourceRating = super().__new__(cls) + + self.count = data["count"] + self.rating = data["rating"] + + return self + + +@dataclass(repr=False) +class PartialUser(APIObjectBase): + """Partial user from BotiCord.""" + + username: str + """Username""" + + discriminator: str + """Discriminator""" + + avatar: Optional[str] + """Avatar of the user""" id: str - """Server's Id""" + """Id of the user""" - short_code: typing.Optional[str] - """Server's page short code""" + socials: UserLinks + """Links of the user""" - status: str - """Server's approval status""" + description: Optional[str] + """Description of the user""" - page_links: list - """List of server's page urls""" + short_description: Optional[str] + """Short description of the user""" - bot: dict - """Bot where this server is used for support users""" + status: Optional[str] + """Status of the user""" + + short_domain: Optional[str] + """Short domain""" + + @classmethod + def from_dict(cls, data: dict): + """Generate a PartialUser from the given data. + + Parameters + ---------- + data: :class:`dict` + The dictionary to convert into a PartialUser. + """ + + self: PartialUser = super().__new__(cls) + + self.username = data["username"] + self.discriminator = data["discriminator"] + self.avatar = data.get("avatar") + self.id = data["id"] + self.socials = UserLinks.from_dict(data["socials"]) + self.description = data.get("description") + self.short_description = data.get("shortDescription") + self.status = data.get("status") + self.short_domain = data.get("shortDomain") + + return self + + +@dataclass(repr=False) +class ResourceBot(APIObjectBase): + """Bot published on BotiCord + + .. warning:: + + The result of the reverse conversion (`.to_dict()`) may not match the actual data.""" + + id: str + """ID of the bot""" name: str - """Name of the server""" + """Name of the bot""" - avatar: str - """Server's avatar""" + short_description: str + """Short description of the bot""" - members: list - """Members counts - `[all, online]`""" + description: str + """Description of the bot""" - owner: typing.Optional[str] - """Server's owner Id""" + avatar: Optional[str] + """Avatar of the bot""" - bumps: int - """Bumps count""" + short_link: Optional[str] + """Short link to the bot's page""" - tags: list - """Server's search-tags""" + invite_link: str + """Invite link""" - links: dict - """Server's social medias""" + premium_active: bool + """Is premium status active? (True/False)""" - short_description: typing.Optional[str] - """Server's short description""" + premium_splash_url: Optional[str] + """Link to the splash""" - long_description: typing.Optional[str] - """Server's long description""" + premium_auto_fetch: Optional[bool] + """Is auto-fetch enabled? (True/False)""" - badge: typing.Optional[str] - """Server's badge""" + premium_banner_url: Optional[str] + """Premium banner URL""" - def __init__(self, **kwargs): - super().__init__(**parse_with_information_dict(kwargs)) + owner: str + """Owner of the bot""" + status: ResourceStatus + """Status of the bot""" -class UserProfile(ApiData): - """This model represents profile of user, returned from the Boticord API""" + ratings: List[ResourceRating] + """Bot's ratings""" - id: str - """Id of User""" + prefix: str + """Prefix of the bot""" - status: str - """Status of user""" + discriminator: str + """Bot's discriminator""" - badge: typing.Optional[str] - """User's badge""" + created_date: datetime + """Date when the bot was published""" - short_code: typing.Optional[str] - """User's profile page short code""" + support_server_invite_link: Optional[str] + """Link to the support server""" - site: typing.Optional[str] - """User's website""" + library: Optional[BotLibrary] + """The library that the bot is based on""" - vk: typing.Optional[str] - """User's VK Profile""" + guilds: Optional[int] + """Number of guilds""" - steam: typing.Optional[str] - """User's steam account""" + shards: Optional[int] + """Number of shards""" - youtube: typing.Optional[str] - """User's youtube channel""" + members: Optional[int] + """Number of members""" - twitch: typing.Optional[str] - """User's twitch channel""" + website: Optional[str] + """Link to bot's website""" - git: typing.Optional[str] - """User's github/gitlab (or other git-service) profile""" + tags: List[BotTag] + """List of bot tags""" - def __init__(self, **kwargs): - super().__init__(**parse_response_dict(kwargs)) + up_count: int + """Number of ups""" + ups: List[ResourceUp] + """List of bot's ups""" -class UserComments(ApiData): - """This model represents all the user's comments on every page""" + developers: List[PartialUser] + """List of bot's developers""" - bots: list - """Data from `get_bot_comments` method""" + @classmethod + def from_dict(cls, data: dict): + """Generate a ResourceBot from the given data. - servers: list - """Data from `get_server_comments` method""" + Parameters + ---------- + data: :class:`dict` + The dictionary to convert into a ResourceBot. + """ - def __init__(self, **kwargs): - super().__init__(**parse_user_comments_dict(kwargs)) + self: ResourceBot = super().__new__(cls) + self.id = data.get("id") + self.name = data.get("name") + self.short_description = data.get("shortDescription") + self.description = data.get("description") + self.avatar = data.get("avatar") + self.short_link = data.get("shortLink") + self.invite_link = data.get("inviteLink") + self.owner = data.get("owner") + self.prefix = data.get("prefix") + self.discriminator = data.get("discriminator") + self.support_server_invite_link = data.get("support_server_invite") + self.website = data.get("website") + self.up_count = data.get("upCount") -class SimpleBot(ApiData): - """This model represents a short bot information (`id`, `short`). - After that you can get more information about it using `get_bot_info` method.""" + self.premium_active = data["premium"].get("active") + self.premium_splash_url = data["premium"].get("splashURL") + self.premium_auto_fetch = data["premium"].get("autoFetch") + self.premium_banner_url = data["premium"].get("bannerURL") - id: str - """Bot's Id""" + self.status = ResourceStatus(data.get("status")) + self.ratings = [ + ResourceRating.from_dict(rating) for rating in data.get("ratings", []) + ] + self.created_date = datetime.strptime( + data["createdDate"], "%Y-%m-%dT%H:%M:%S.%f%z" + ) + self.library = ( + BotLibrary(data["library"]) if data.get("library") is not None else None + ) + self.tags = [BotTag(tag) for tag in data.get("tags", [])] + self.ups = [ResourceUp.from_dict(up) for up in data.get("ups", [])] + self.developers = [ + PartialUser.from_dict(dev) for dev in data.get("developers", []) + ] - short_code: typing.Optional[str] - """Bot's page short code""" + self.guilds = data.get("guilds") + self.shards = data.get("shards") + self.members = data.get("members") + + return self - def __init__(self, **kwargs): - super().__init__(**parse_response_dict(kwargs)) - -class CommentData(ApiData): - """This model represents comment data (from webhook response)""" - - vote: dict - """Comment vote data""" - - old: typing.Optional[str] - """Old content of the comment""" - - new: typing.Optional[str] - """New content of the comment""" - - def __init__(self, **kwargs): - super().__init__(**parse_response_dict(kwargs)) - - -def parse_webhook_response_dict(webhook_data: dict) -> dict: - data = webhook_data.copy() - - for key, value in data.copy().items(): - if key.lower() == "data": - for data_key, data_value in value.copy().items(): - if data_key == "comment": - data[data_key] = CommentData(**data_value) - else: - converted_data_key = "".join( - ["_" + x.lower() if x.isupper() else x for x in data_key] - ).lstrip("_") - - data[converted_data_key] = data_value - - del data["data"] - else: - data[key] = value - - return data - - -class BumpResponse(ApiData): - """This model represents a webhook response (`bot bump`).""" - - type: str - """Type of response (`bump`)""" - - user: str - """Id of user who did the action""" - - at: int - """Timestamp of the action""" - - def __init__(self, **kwargs): - super().__init__(**parse_webhook_response_dict(kwargs)) - - -class CommentResponse(ApiData): - """This model represents a webhook response (`comment`).""" - - type: str - """Type of response (`comment`)""" - - user: str - """Id of user who did the action""" - - comment: CommentData - """Information about the comment""" - - reason: typing.Optional[str] - """Is comment deleted? so, why?""" - - at: int - """Timestamp of the action""" - - def __init__(self, **kwargs): - super().__init__(**parse_webhook_response_dict(kwargs)) - - -class LinkDomain(IntEnum): - """Domain to short the link""" - - BCORD_CC = 1 - """``bcord.cc`` domain, default""" - - DISCORD_CAMP = 3 - """``discord.camp`` domain""" - - -class ShortedLink(ApiData): - id: int - """Id of shorted link""" - - code: str - """Code of shorted link""" - - owner_i_d: str - """Id of owner of shorted link""" - - domain: str - """Domain of shorted link""" - - views: int - """Link views count""" - - date: int - """Timestamp of link creation moment""" - - def __init__(self, **kwargs): - super().__init__(**parse_response_dict(kwargs)) +class LinkDomain: + pass diff --git a/boticordpy/webhook.py b/boticordpy/webhook.py deleted file mode 100644 index 49aa084..0000000 --- a/boticordpy/webhook.py +++ /dev/null @@ -1,135 +0,0 @@ -import asyncio -import typing - -from .types import BumpResponse, CommentResponse - -from aiohttp import web -import aiohttp - - -class Webhook: - """Represents a client that can be used to work with BotiCord Webhooks. - IP of the server - your machine IP. (`0.0.0.0`) - - Args: - x_hook_key (:obj:`str`) - X-hook-key to check the auth of incoming request. - endpoint_name (:obj:`str`) - Name of endpoint (for example: `/bot`) - - Keyword Arguments: - loop: `asyncio loop` - """ - - __slots__ = ( - "_webserver", - "_listeners", - "_is_running", - "__app", - "_endpoint_name", - "_x_hook_key", - "_loop", - ) - - __app: web.Application - _webserver: web.TCPSite - - def __init__(self, x_hook_key: str, endpoint_name: str, **kwargs) -> None: - self._x_hook_key = x_hook_key - self._endpoint_name = endpoint_name - self._listeners = {} - self.__app = web.Application() - self._is_running = False - self._loop = kwargs.get("loop") or asyncio.get_event_loop() - - def listener(self, response_type: str): - """Decorator to set the listener. - Args: - response_type (:obj:`str`) - Type of response (Check reference page) - """ - - def inner(func): - if not asyncio.iscoroutinefunction(func): - raise TypeError(f"<{func.__qualname__}> must be a coroutine function") - self._listeners[response_type] = func - return func - - return inner - - def register_listener(self, response_type: str, callback: typing.Any): - """Method to set the listener. - Args: - response_type (:obj:`str`) - Type of response (Check reference page) - callback (:obj:`function`) - Coroutine Callback Function - """ - if not asyncio.iscoroutinefunction(callback): - raise TypeError(f"<{func.__qualname__}> must be a coroutine function") - - self._listeners[response_type] = callback - return self - - async def _interaction_handler(self, request: aiohttp.web.Request) -> web.Response: - """Interaction handler""" - auth = request.headers.get("X-Hook-Key") - - if auth == self._x_hook_key: - data = await request.json() - - responder = self._listeners.get(data["type"]) - - if responder is not None: - await responder( - ( - BumpResponse - if data["type"].endswith("_bump") - else CommentResponse - )(**data) - ) - - return web.Response(status=200) - - return web.Response(status=401) - - async def _run(self, port): - self.__app.router.add_post("/" + self._endpoint_name, self._interaction_handler) - - runner = web.AppRunner(self.__app) - await runner.setup() - - self._webserver = web.TCPSite(runner, "0.0.0.0", port) - await self._webserver.start() - - self._is_running = True - - def start(self, port: int) -> None: - """Method to start the webhook server - - Args: - port (:obj:`int`) - Port to start the webserver - """ - self._loop.create_task(self._run(port)) - - @property - def is_running(self) -> bool: - """If the server running?""" - return self._is_running - - @property - def listeners(self) -> dict: - """Dictionary of listeners (`type`: `callback function`)""" - return self._listeners - - @property - def app(self) -> web.Application: - """Web application that handles incoming requests""" - return self.__app - - async def close(self) -> None: - """Stop the webhooks server""" - await self._webserver.stop() - - self._is_running = False diff --git a/requirements.txt b/requirements.txt index ee4ba4f..a05ef8d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ aiohttp +typing_extensions \ No newline at end of file diff --git a/tests/test_convertation.py b/tests/test_convertation.py new file mode 100644 index 0000000..81d39df --- /dev/null +++ b/tests/test_convertation.py @@ -0,0 +1,43 @@ +from boticordpy import types + + +resource_up_dict = {"id": "arbuz123", "expires": "1685262170000"} +resource_rating_dict = {"count": 15, "rating": 5} +resource_bot_dict = { + "id": "947141336451153931", + "name": "BumpBot", + "status": 1, + "createdDate": "2023-05-22T22:29:23.264Z", + "premium": {}, +} + + +def test_resource_up_convertation(): + model_from_dict = types.ResourceUp.from_dict(resource_up_dict) + + assert model_from_dict.id == "arbuz123" + assert ( + model_from_dict.expires.strftime("%Y.%m.%d %H:%M:%S") == "2023.05.28 08:22:50" + ) + + dict_from_model = model_from_dict.to_dict() + + assert dict_from_model == resource_up_dict + + +def test_resource_rating_convertation(): + model_from_dict = types.ResourceRating.from_dict(resource_rating_dict) + + assert model_from_dict.count == 15 + assert model_from_dict.rating == 5 + + dict_from_model = model_from_dict.to_dict() + + assert dict_from_model == resource_rating_dict + + +def test_resource_bot_convertation(): + model_from_dict = types.ResourceBot.from_dict(resource_bot_dict) + + assert int(model_from_dict.created_date.timestamp()) == 1684794563 + assert model_from_dict.status.name == "PUBLIC" diff --git a/tests/test_converting.py b/tests/test_converting.py deleted file mode 100644 index 5073fe9..0000000 --- a/tests/test_converting.py +++ /dev/null @@ -1,141 +0,0 @@ -import pytest - -from boticordpy import types - -single_comment_dict = { - "userID": "525366699969478676", - "text": "aboba", - "vote": 1, - "isUpdated": False, - "createdAt": 1644388399, -} - -bot_data_dict = { - "id": "724663360934772797", - "shortCode": "kerdoku", - "links": [ - "https://boticord.top/bot/724663360934772797", - "https://bcord.cc/b/724663360934772797", - "https://myservers.me/b/724663360934772797", - "https://boticord.top/bot/kerdoku", - "https://bcord.cc/b/kerdoku", - "https://myservers.me/b/kerdoku", - ], - "server": {"id": "724668798874943529", "approved": True}, - "information": { - "bumps": 37, - "added": 1091, - "prefix": "?", - "permissions": 1544023111, - "tags": ["комбайн", "экономика", "модерация", "приветствия"], - "developers": ["585766846268047370"], - "links": {"discord": "5qXgJvr", "github": None, "site": "https://kerdoku.top"}, - "library": "discordpy", - "shortDescription": "Удобный и дружелюбный бот, который имеет крутой функционал!", - "longDescription": "wow", - "badge": None, - "stats": {"servers": 2558, "shards": 3, "users": 348986}, - "status": "APPROVED", - }, -} - -server_data_dict = { - "id": "722424773233213460", - "shortCode": "boticord", - "status": "ACCEPT_MEMBERS", - "links": [ - "https://boticord.top/server/722424773233213460", - "https://bcord.cc/s/722424773233213460", - "https://myservers.me/s/722424773233213460", - "https://boticord.top/server/boticord", - "https://bcord.cc/s/boticord", - "https://myservers.me/s/boticord", - ], - "bot": {"id": None, "approved": False}, - "information": { - "name": "BotiCord Community", - "avatar": "https://cdn.discordapp.com/icons/722424773233213460/060188f770836697846710b109272e4c.webp", - "members": [438, 0], - "bumps": 62, - "tags": [ - "аниме", - "игры", - "поддержка", - "комьюнити", - "сообщество", - "discord", - "дискорд сервера", - "дискорд боты", - ], - "links": { - "invite": "hkHjW8a", - "site": "https://boticord.top/", - "youtube": None, - "twitch": None, - "steam": None, - "vk": None, - }, - "shortDescription": "short text", - "longDescription": "long text", - "badge": "STAFF", - }, -} - -user_profile_dict = { - "id": "178404926869733376", - "status": '"Если вы не разделяете мою точку зрения, поздравляю — вам больше достанется." © Артемий Лебедев', - "badge": "STAFF", - "shortCode": "cipherka", - "site": "https://sqdsh.top/", - "vk": None, - "steam": "sadlycipherka", - "youtube": None, - "twitch": None, - "git": "https://git.sqdsh.top/me", -} - - -@pytest.fixture -def single_comment() -> types.SingleComment: - return types.SingleComment(**single_comment_dict) - - -@pytest.fixture -def bot_data() -> types.Bot: - return types.Bot(**bot_data_dict) - - -@pytest.fixture -def server_data() -> types.Server: - return types.Bot(**server_data_dict) - - -@pytest.fixture -def user_profile_data() -> types.UserProfile: - return types.UserProfile(**user_profile_dict) - - -def test_comment_dict_fields(single_comment: types.SingleComment) -> None: - for attr in single_comment: - assert single_comment.get(attr) == getattr(single_comment, attr) - - -def test_user_profile_dict_fields(user_profile_data: types.UserProfile) -> None: - for attr in user_profile_data: - assert user_profile_data.get(attr) == getattr(user_profile_data, attr) - - -def test_bot_dict_fields(bot_data: types.Bot) -> None: - for attr in bot_data: - if attr.lower() == "information": - assert bot_data["information"].get(attr) == getattr(bot_data, attr) - else: - assert bot_data[attr] == getattr(bot_data, attr) - - -def test_server_dict_fields(server_data: types.Server) -> None: - for attr in server_data: - if attr.lower() == "information": - assert server_data["information"].get(attr) == getattr(bot_data, attr) - else: - assert server_data[attr] == getattr(server_data, attr) From 1aac25cb63c465ab8de512e36836b74e0c2555df Mon Sep 17 00:00:00 2001 From: MadCat9958 Date: Sun, 4 Jun 2023 11:37:43 +0300 Subject: [PATCH 03/34] vs code users moment --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 3280530..bdf246b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ boticordpy.egg-info _testing.py /.pytest_cache /docs/build/ +.vscode \ No newline at end of file From 745e0e512328cba69e1fbc802f0751ef4725fe83 Mon Sep 17 00:00:00 2001 From: grey-cat-1908 Date: Sun, 4 Jun 2023 12:21:28 +0300 Subject: [PATCH 04/34] better docs --- boticordpy/client.py | 143 +-------------------------------- boticordpy/types.py | 35 ++++++-- docs/source/api.rst | 3 +- docs/source/api/autopost.rst | 4 +- docs/source/api/client.rst | 8 +- docs/source/api/exceptions.rst | 4 +- docs/source/api/types.rst | 38 ++++++++- docs/source/api/webhook.rst | 7 -- docs/source/conf.py | 42 +++++----- docs/source/index.rst | 1 - docs/source/other.rst | 38 --------- 11 files changed, 99 insertions(+), 224 deletions(-) delete mode 100644 docs/source/api/webhook.rst delete mode 100644 docs/source/other.rst diff --git a/boticordpy/client.py b/boticordpy/client.py index 18c7236..1d8aaed 100644 --- a/boticordpy/client.py +++ b/boticordpy/client.py @@ -54,7 +54,7 @@ class BoticordClient: """Post Bot's stats. Args: - bot_id (Union[:obj:`str`, :obj:`) + bot_id (Union[:obj:`str`, :obj:`int`]) Id of the bot to post stats of. servers ( :obj:`int` ) Bot's servers count @@ -62,6 +62,7 @@ class BoticordClient: Bot's shards count users ( :obj:`int` ) Bot's users count + Returns: :obj:`~.types.ResourceBot`: ResourceBot object. @@ -71,146 +72,6 @@ class BoticordClient: ) return boticord_types.ResourceBot.from_dict(response) - async def get_server_info(self, server_id: int): - """Gets information about specified server. - - Args: - server_id (:obj:`int`) - Id of the server - - Returns: - :obj:`~.types.Server`: - Server object. - """ - response = await self.http.get_server_info(server_id) - return boticord_types.Server(**response) - - async def get_server_comments(self, server_id: int) -> list: - """Gets list of comments of specified server. - - Args: - server_id (:obj:`int`) - Id of the server - - Returns: - :obj:`list` [ :obj:`~.types.SingleComment` ]: - List of comments. - """ - response = await self.http.get_server_comments(server_id) - return [boticord_types.SingleComment(**comment) for comment in response] - - async def post_server_stats(self, payload: dict) -> dict: - """Post Server's stats. You must be Boticord-Service bot. - Payload is raw, because if you use it - you know what you are doing. - You can find more information about payload `in BotiCord API Docs `_ - - Args: - payload (:obj:`dict`) - Custom data (Use Boticord API docs.) - Returns: - :obj:`dict`: - Boticord API Response. - """ - response = await self.http.post_server_stats(payload) - return response - - async def get_user_info(self, user_id: int): - """Gets information about specified user. - - Args: - user_id (:obj:`int`) - Id of the user - - Returns: - :obj:`~.types.UserProfile`: - User Profile object. - """ - response = await self.http.get_user_info(user_id) - return boticord_types.UserProfile(**response) - - async def get_user_comments(self, user_id: int): - """Gets comments of specified user. - - Args: - user_id (:obj:`int`) - Id of the user - - Returns: - :obj:`~.types.UserComments`: - User comments on Bots and Servers pages. - """ - response = await self.http.get_user_comments(user_id) - return boticord_types.UserComments(**response) - - async def get_user_bots(self, user_id: int) -> list: - """Gets list of bots of specified user. - - Args: - user_id (:obj:`int`) - Id of the user - - Returns: - :obj:`list` [ :obj:`~.types.SimpleBot` ]: - List of simple information about users bots. - """ - response = await self.http.get_user_bots(user_id) - return [boticord_types.SimpleBot(**bot) for bot in response] - - async def get_my_shorted_links(self, *, code: str = None): - """Gets shorted links of an authorized user - - Args: - code (:obj:`str`) - Code of shorted link. Could be None. - - Returns: - Union[:obj:`list` [ :obj:`~.types.ShortedLink` ], :obj:`~types.ShortedLink`]: - List of shorted links if none else shorted link - """ - response = await self.http.get_my_shorted_links(code) - - return ( - [boticord_types.ShortedLink(**link) for link in response] - if code is None - else boticord_types.ShortedLink(**response[0]) - ) - - async def create_shorted_link(self, *, code: str, link: str, domain=1): - """Creates new shorted link - - Args: - code (:obj:`str`) - Code of link to short. - link (:obj:`str`) - Link to short. - domain (:obj:`~.types.LinkDomain`) - Domain to use in shorted link - - Returns: - :obj:`~types.ShortedLink`: - Shorted Link - """ - response = await self.http.create_shorted_link(code, link, domain=domain) - - return boticord_types.ShortedLink(**response) - - async def delete_shorted_link(self, code: str, domain=1): - """Deletes shorted link - - Args: - code (:obj:`str`) - Code of link to delete. - domain (:obj:`~.types.LinkDomain`) - Domain that is used in shorted link - - Returns: - :obj:`bool`: - Is link deleted successfully? - """ - response = await self.http.delete_shorted_link(code, domain) - - return response.get("ok", False) - def autopost(self) -> AutoPost: """Returns a helper instance for auto-posting. diff --git a/boticordpy/types.py b/boticordpy/types.py index 7bd826a..c3d2bf7 100644 --- a/boticordpy/types.py +++ b/boticordpy/types.py @@ -212,33 +212,56 @@ class BotLibrary(IntEnum): """The library that the bot is based on""" DISCORD4J = 1 + """Discord4j""" + DISCORDCR = 2 + """Discordcr""" + DISCORDGO = 3 + """DiscordGO""" + DISCORDDOO = 4 + """Discordoo""" + DSHARPPLUS = 5 + """DSharpPlus""" + DISCORDJS = 6 + """Discord.js""" + DISCORDNET = 7 + """Discord.Net""" + DISCORDPY = 8 + """discord.py""" + ERIS = 9 + """eris""" + JAVACORD = 10 + """JavaCord""" + JDA = 11 + """JDA""" + OTHER = 12 + """Other""" class ResourceStatus(IntEnum): - """Bot status on monitoring""" + """Status of the project on monitoring""" HIDDEN = 0 - """Bot is hidden""" + """is hidden""" PUBLIC = 1 - """Bot is public""" + """is public""" BANNED = 2 - """Bot is banned""" + """is banned""" PENDING = 3 - """Bor is pending""" + """is pending""" class BotTag(IntEnum): @@ -316,7 +339,7 @@ class UserLinks(APIObjectBase): The dictionary to convert into a UserLinks. """ - self: ResourceUp = super().__new__(cls) + self: UserLinks = super().__new__(cls) self.vk = data.get("vk") self.telegram = data.get("telegram") diff --git a/docs/source/api.rst b/docs/source/api.rst index 706644a..6540202 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -14,5 +14,4 @@ API Reference for the boticordpy Module api/client api/autopost api/exceptions - api/types - api/webhook \ No newline at end of file + api/types \ No newline at end of file diff --git a/docs/source/api/autopost.rst b/docs/source/api/autopost.rst index 82e9f81..30ae03b 100644 --- a/docs/source/api/autopost.rst +++ b/docs/source/api/autopost.rst @@ -1,6 +1,6 @@ -#################### +########################### AutoPost API Reference -#################### +########################### .. automodule:: boticordpy.autopost :members: diff --git a/docs/source/api/client.rst b/docs/source/api/client.rst index cb589f2..6ea5f18 100644 --- a/docs/source/api/client.rst +++ b/docs/source/api/client.rst @@ -1,7 +1,11 @@ +.. currentmodule:: boticordpy + #################### Client API Reference #################### -.. automodule:: boticordpy.client - :members: +BoticordClient +----------------- + +.. autoclass:: BoticordClient :inherited-members: diff --git a/docs/source/api/exceptions.rst b/docs/source/api/exceptions.rst index 5fbedef..92ffe2c 100644 --- a/docs/source/api/exceptions.rst +++ b/docs/source/api/exceptions.rst @@ -1,6 +1,6 @@ -#################### +########################## Exceptions API Reference -#################### +########################## .. automodule:: boticordpy.exceptions :members: diff --git a/docs/source/api/types.rst b/docs/source/api/types.rst index dc0a263..3f4771d 100644 --- a/docs/source/api/types.rst +++ b/docs/source/api/types.rst @@ -1,8 +1,40 @@ +.. currentmodule:: boticordpy.types + #################### Models API Reference #################### -We recommend you to read the `boticordpy/types.py `_ file, because it is much easier to read than here. - -.. automodule:: boticordpy.types +.. autoclass:: ResourceRating + :members: + +.. autoclass:: ResourceUp + :members: + +Enums +------- + +.. autoclass:: BotLibrary + :members: + +.. autoclass:: BotTag + :members: + +.. autoclass:: ResourceStatus + :members: + + +Bots +------ + +.. autoclass:: ResourceBot + :members: + + +Users +------ + +.. autoclass:: UserLinks + :members: + +.. autoclass:: PartialUser :members: diff --git a/docs/source/api/webhook.rst b/docs/source/api/webhook.rst deleted file mode 100644 index 59198b0..0000000 --- a/docs/source/api/webhook.rst +++ /dev/null @@ -1,7 +0,0 @@ -#################### -Webhook API Reference -#################### - -.. automodule:: boticordpy.webhook - :members: - :inherited-members: diff --git a/docs/source/conf.py b/docs/source/conf.py index a79baa0..961f62b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -23,11 +23,11 @@ import os sys.path.insert(0, os.path.abspath("../..")) project = "BoticordPY" -copyright = "2022, Victor Kotlin (Marakarka)" -author = "Victor Kotlin (Marakarka)" +copyright = "2022 - 2023, Viktor K (Marakarka)" +author = "Viktor K (Marakarka)" # The full version, including alpha/beta/rc tags -release = "2.2.2" +release = "3.0.0a" # -- General configuration --------------------------------------------------- @@ -36,41 +36,43 @@ release = "2.2.2" # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ + "sphinx_design", "sphinx.ext.napoleon", "sphinx.ext.autodoc", "sphinx.ext.viewcode", "sphinx.ext.autosectionlabel", "sphinx.ext.extlinks", + "sphinxcontrib_trio" ] +autodoc_default_options = {"members": True, "show-inheritance": True, 'member-order': 'bysource'} + # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path. +add_module_names = False + exclude_patterns = [] intersphinx_mapping = { "py": ("https://docs.python.org/3", None), - "discord": ("https://discordpy.readthedocs.io/en/latest/", None), "aiohttp": ("https://docs.aiohttp.org/en/stable/", None), } # -- Options for HTML output ------------------------------------------------- -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = "sphinxawesome_theme" - -html_theme_options = {} - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "custom.css" will overwrite the builtin "custom.css". +html_theme = "furo" +html_theme_options = { + "sidebar_hide_name": True, +} +pygments_style = "monokai" +default_dark_mode = True html_static_path = ["_static"] +html_css_files = ["custom.css"] - -def setup(app): - app.add_css_file("custom.css") +rst_prolog = """ +.. |coro| replace:: This function is a |coroutine_link|_. +.. |maybecoro| replace:: This function *could be a* |coroutine_link|_. +.. |coroutine_link| replace:: *coroutine* +.. _coroutine_link: https://docs.python.org/3/library/asyncio-task.html#coroutine +""" diff --git a/docs/source/index.rst b/docs/source/index.rst index 63629a7..c903f84 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -11,7 +11,6 @@ This is a documentation for wrapper for BotiCord API. quickstart api - other Links ===== diff --git a/docs/source/other.rst b/docs/source/other.rst deleted file mode 100644 index eb26853..0000000 --- a/docs/source/other.rst +++ /dev/null @@ -1,38 +0,0 @@ -.. currentmodule:: boticordpy - -.. other: - -Other Information -================= - -########## -Listeners -########## - -When you work with BotiCord Webhooks you may receive a lot of events. -To make it easier to handle them there is a list of the events you can receive: - -.. csv-table:: - :header: "BotiCord Events", "Meaning" - :widths: 20, 20 - - "test_webhook_message", "Test message." - "new_bot_comment", "On new bot comment" - "edit_bot_comment", "On bot comment edit" - "delete_bot_comment", "On bot comment delete" - "new_bot_bump", "On new bot bump" - "new_server_comment", "On new server comment" - "edit_server_comment", "On server comment edit" - "delete_server_comment", "On server comment delete" - "new_server_bump", "On new server bump" - - -################## -Callback functions -################## - -.. warning:: - - Callback functions must be a **coroutine**. If they aren't, then you might get unexpected - errors. In order to turn a function into a coroutine they must be ``async def`` - functions. From 7ff39c97b3ae5a9dbc2a3f37ee2cb5804a7dfa6f Mon Sep 17 00:00:00 2001 From: grey-cat-1908 Date: Sun, 4 Jun 2023 12:25:54 +0300 Subject: [PATCH 05/34] update docs-requirements --- docs-requirements.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs-requirements.txt b/docs-requirements.txt index 2ae7816..d1c21fe 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -1,2 +1,4 @@ -sphinxawesome_theme +furo +sphinxcontrib_trio +sphinx_design sphinx \ No newline at end of file From ffdd54439be212633a1520c5a79b5cfcf971735d Mon Sep 17 00:00:00 2001 From: MadCat9958 Date: Sun, 4 Jun 2023 12:27:17 +0300 Subject: [PATCH 06/34] UserProfile + ResourceServer --- boticordpy/types.py | 256 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 249 insertions(+), 7 deletions(-) diff --git a/boticordpy/types.py b/boticordpy/types.py index 7bd826a..c72bf0f 100644 --- a/boticordpy/types.py +++ b/boticordpy/types.py @@ -226,19 +226,80 @@ class BotLibrary(IntEnum): class ResourceStatus(IntEnum): - """Bot status on monitoring""" + """Bot/server status on monitoring""" HIDDEN = 0 - """Bot is hidden""" + """Bot/server is hidden""" PUBLIC = 1 - """Bot is public""" + """Bot/server is public""" BANNED = 2 - """Bot is banned""" + """Bot/server is banned""" PENDING = 3 - """Bor is pending""" + """Bot/server is pending""" + + +class ServerTag(IntEnum): + """Tags of the server""" + + SPEAKING = 130 + """Speaking""" + + FUN = 131 + """Fun""" + + GAMES = 132 + """Games""" + + CINEMA = 133 + """Cinema""" + + ANIME = 134 + """Anime""" + + ART = 135 + """Art""" + + CODING = 136 + """Coding""" + + MUSIC = 137 + """Music""" + + ADULT = 138 + """18+""" + + ROLEPLAY = 139 + """Role-Play""" + + HUMOUR = 140 + """Humour""" + + GENSHIN = 160 + """Genshin""" + + MINECRAFT = 161 + """Minecraft""" + + GTA = 162 + """GTA""" + + CS = 163 + """CS""" + + DOTA = 164 + """Dota""" + + AMONG_US = 165 + """Among Us""" + + FORTNITE = 166 + """Fortnite""" + + BRAWL_STARS = 167 + """Brawl Stars""" class BotTag(IntEnum): @@ -316,7 +377,7 @@ class UserLinks(APIObjectBase): The dictionary to convert into a UserLinks. """ - self: ResourceUp = super().__new__(cls) + self: UserLinks = super().__new__(cls) self.vk = data.get("vk") self.telegram = data.get("telegram") @@ -325,6 +386,37 @@ class UserLinks(APIObjectBase): self.custon = data.get("custom") return self + + +@dataclass(repr=False) +class UserBadge(APIObjectBase): + """Information about user's profile badge""" + + id: int + """Badge's ID""" + + name: str + """Badge's name""" + + asset_url: str + """Badge's icon URL""" + + @classmethod + def from_dict(cls, data: dict): + """Generate a UserBadge from the given data. + + Parameters + ---------- + data: :class:`dict` + The dictionary to convert into a UserBadge. + """ + self: UserBadge = super().__new__(cls) + + self.id = data['id'] + self.name = data['name'] + self.asset_url = data['assetURL'] + + return self @dataclass(repr=False) @@ -426,7 +518,7 @@ class PartialUser(APIObjectBase): The dictionary to convert into a PartialUser. """ - self: PartialUser = super().__new__(cls) + self: cls = super().__new__(cls) self.username = data["username"] self.discriminator = data["discriminator"] @@ -439,6 +531,156 @@ class PartialUser(APIObjectBase): self.short_domain = data.get("shortDomain") return self + + +@dataclass(repr=False) +class ResourceBot(APIObjectBase): + """Tak nado""" + + +@dataclass(repr=False) +class ResourceServer(APIObjectBase): + """Information about server from BotiCord.""" + + id: str + """Server's ID""" + + name: str + """Server's name""" + + short_description: str + """Server's short description""" + + description: str + """Server's description""" + + avatar: Optional[str] + """Server's avatar""" + + short_link: Optional[str] + """Server's short link""" + + invite_link: str + """Server's invite link""" + + premium_active: bool + """Server's premium state""" + + premium_auto_fetch: Optional[bool] + """Server's premium auto fetch state""" + + premium_banner_url: Optional[str] + """Server's premium banner URL""" + + premium_splash_url: Optional[str] + """Server's premium splash URL""" + + standart_banner_id: int + """Server's standart banner ID""" + + owner: str + """Server's owner ID""" + + status: ResourceStatus + """Server's status""" + + ratings: List[ResourceRating] + """Server's ratings""" + + created_date: datetime + """Server's creation time""" + + members: Optional[int] + """Server's members count""" + + website: Optional[str] + """Server's website""" + + tags: List[ServerTag] + """Server's tags""" + + moderators: List[PartialUser] + """Server's moderators""" + + up_count: int + """Server's up count""" + + ups: Optional[ResourceUp] + """Server's ups""" + + @classmethod + def from_dict(cls, data: dict): + """Generate a ResourceServer from the given data. + + Parameters + ---------- + data: :class:`dict` + The dictionary to convert into a ResourceServer.""" + + self = super().__new__(cls) + + self.id = data['id'] + self.name = data['name'] + self.short_description = data['shortDescription'] + self.description = data['description)'] + self.avatar = data.get("avatar") + self.short_link = data.get("shortLink") + self.invite_link = data.get("inviteLink") + self.owner = data.get("owner") + self.website = data.get("website") + self.up_count = data.get("upCount") + + self.premium_active = data['premium'].get('active') + self.premium_splash_url = data['premium'].get('splashURL') + self.premium_auto_fetch = data['premium'].get('autoFetch') + self.premium_banner_url = data['premium'].get('bannerURL') + + self.status = ResourceStatus(data.get("status")) + self.ratings = [ + ResourceRating.from_dict(rating) for rating in data.get("ratings", []) + ] + self.created_date = datetime.strptime( + data["createdDate"], "%Y-%m-%dT%H:%M:%S.%f%z" + ) + self.tags = [ServerTag(tag) for tag in data.get("tags", [])] + self.ups = [ResourceUp.from_dict(up) for up in data.get("ups", [])] + self.moderators = [ + PartialUser.from_dict(mod) for mod in data.get("moderators", []) + ] + + self.members = data.get("memberCount") + + return self + + +@dataclass(repr=False) +class UserProfile(PartialUser): + """Information about user's profile from BotiCord. + + It has all from PartialUser and some more params: 'bots', 'servers', 'badges'""" + + badges: List[UserBadge] + """User's badges list.""" + + bots: List[ResourceBot] + """User's bots list""" + + @classmethod + def from_dict(cls, data: dict): + """Generate a UserProfile from the given data. + + Parameters + ---------- + data: :class:`dict` + The dictionary to convert into a UserProfile.""" + + self = super().from_dict(data) + + self.badges = [UserBadge.from_dict(badge) for badge in data.get('badges', [])] + self.bots = [ResourceBot.from_dict(bot) for bot in data.get('bots', [])] + self.servers = [ResourceServer.from_dict(server) for server in data.get('servers', [])] + + return self @dataclass(repr=False) From b99954a59777dd806ab88cb09e5cd97dbd658caf Mon Sep 17 00:00:00 2001 From: MadCat9958 Date: Sun, 4 Jun 2023 12:33:54 +0300 Subject: [PATCH 07/34] =?UTF-8?q?=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- boticordpy/types.py | 71 ++++----------------------------------------- 1 file changed, 5 insertions(+), 66 deletions(-) diff --git a/boticordpy/types.py b/boticordpy/types.py index bcbdc5d..57d286d 100644 --- a/boticordpy/types.py +++ b/boticordpy/types.py @@ -249,80 +249,19 @@ class BotLibrary(IntEnum): class ResourceStatus(IntEnum): - """Status of the project on monitoring""" + """Bot status on monitoring""" HIDDEN = 0 - """is hidden""" + """Bot is hidden""" PUBLIC = 1 - """is public""" + """Bot is public""" BANNED = 2 - """is banned""" + """Bot is banned""" PENDING = 3 - """is pending""" - - -class ServerTag(IntEnum): - """Tags of the server""" - - SPEAKING = 130 - """Speaking""" - - FUN = 131 - """Fun""" - - GAMES = 132 - """Games""" - - CINEMA = 133 - """Cinema""" - - ANIME = 134 - """Anime""" - - ART = 135 - """Art""" - - CODING = 136 - """Coding""" - - MUSIC = 137 - """Music""" - - ADULT = 138 - """18+""" - - ROLEPLAY = 139 - """Role-Play""" - - HUMOUR = 140 - """Humour""" - - GENSHIN = 160 - """Genshin""" - - MINECRAFT = 161 - """Minecraft""" - - GTA = 162 - """GTA""" - - CS = 163 - """CS""" - - DOTA = 164 - """Dota""" - - AMONG_US = 165 - """Among Us""" - - FORTNITE = 166 - """Fortnite""" - - BRAWL_STARS = 167 - """Brawl Stars""" + """Bor is pending""" class ServerTag(IntEnum): From ea4f7e33e8c5c23fd59ce488978e3b43dc502c1e Mon Sep 17 00:00:00 2001 From: MadCat9958 Date: Sun, 4 Jun 2023 12:41:25 +0300 Subject: [PATCH 08/34] fix --- boticordpy/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/boticordpy/http.py b/boticordpy/http.py index bac1f75..a0f2776 100644 --- a/boticordpy/http.py +++ b/boticordpy/http.py @@ -55,7 +55,7 @@ class HttpClient: def post_bot_stats(self, bot_id: typing.Union[str, int], stats: dict): """Post bot's stats""" - return self.make_request("POST", f"bots/{bot_id}", json=stats) + return self.make_request("POST", f"bots/{bot_id}/stats", json=stats) def get_server_info(self, server_id: int): """Get information about specified server""" From cf69cc2cfc8608135b01628b1a35c157efda3537 Mon Sep 17 00:00:00 2001 From: MadCat9958 Date: Sun, 4 Jun 2023 12:42:50 +0300 Subject: [PATCH 09/34] ono sluchaino --- boticordpy/types.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/boticordpy/types.py b/boticordpy/types.py index 57d286d..48bf1b9 100644 --- a/boticordpy/types.py +++ b/boticordpy/types.py @@ -249,19 +249,19 @@ class BotLibrary(IntEnum): class ResourceStatus(IntEnum): - """Bot status on monitoring""" + """Status of the project on monitoring""" HIDDEN = 0 - """Bot is hidden""" + """is hidden""" PUBLIC = 1 - """Bot is public""" + """is public""" BANNED = 2 - """Bot is banned""" + """is banned""" PENDING = 3 - """Bor is pending""" + """is pending""" class ServerTag(IntEnum): From 6c768137ea5e441398e18edebe98d386bb688c3f Mon Sep 17 00:00:00 2001 From: MadCat9958 Date: Sun, 4 Jun 2023 12:54:05 +0300 Subject: [PATCH 10/34] Docs update (am i right?) --- docs/source/api/types.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/source/api/types.rst b/docs/source/api/types.rst index 3f4771d..2bb9136 100644 --- a/docs/source/api/types.rst +++ b/docs/source/api/types.rst @@ -30,6 +30,13 @@ Bots :members: +Servers +--------- + +.. autoclass:: ResourceServer + :members: + + Users ------ @@ -38,3 +45,6 @@ Users .. autoclass:: PartialUser :members: + +.. autoclass:: UserProfile + :members: From 11f5ca3fc7c23019597d796381486fc2aecf01fa Mon Sep 17 00:00:00 2001 From: MadCat9958 Date: Sun, 4 Jun 2023 12:55:38 +0300 Subject: [PATCH 11/34] =?UTF-8?q?=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/source/api/types.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/source/api/types.rst b/docs/source/api/types.rst index 2bb9136..c7cb70b 100644 --- a/docs/source/api/types.rst +++ b/docs/source/api/types.rst @@ -19,6 +19,9 @@ Enums .. autoclass:: BotTag :members: +.. autoclass:: ServerTag + :members: + .. autoclass:: ResourceStatus :members: From 51bc49689343b6383d45a2ef7d29782dbcda775f Mon Sep 17 00:00:00 2001 From: MadCat9958 Date: Sun, 4 Jun 2023 13:08:30 +0300 Subject: [PATCH 12/34] =?UTF-8?q?<@585766846268047370>,=20=D1=81=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D0=B0=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- boticordpy/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/boticordpy/http.py b/boticordpy/http.py index a0f2776..77c82cc 100644 --- a/boticordpy/http.py +++ b/boticordpy/http.py @@ -42,7 +42,7 @@ class HttpClient: async with self.session.request(method, url, **kwargs) as response: data = await response.json() - if response.status == 200 or response.status == 201: + if (200, 201).__contains__(response.status): return data["result"] else: raise exceptions.HTTPException( From 388e8934407036a0115b2d3d17fa0ea3dacba0a3 Mon Sep 17 00:00:00 2001 From: MadCat9958 Date: Sun, 4 Jun 2023 13:09:58 +0300 Subject: [PATCH 13/34] Fix UserProfile args --- boticordpy/types.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/boticordpy/types.py b/boticordpy/types.py index 48bf1b9..c10a442 100644 --- a/boticordpy/types.py +++ b/boticordpy/types.py @@ -688,6 +688,9 @@ class UserProfile(PartialUser): bots: List[ResourceBot] """User's bots list""" + servers: List[ResourceServer] + """User's servers list""" + @classmethod def from_dict(cls, data: dict): """Generate a UserProfile from the given data. From 82a94cac7afe4bde895813d665986b197793426f Mon Sep 17 00:00:00 2001 From: grey-cat-1908 Date: Sun, 4 Jun 2023 13:21:23 +0300 Subject: [PATCH 14/34] fix && formation --- boticordpy/client.py | 2 +- boticordpy/types.py | 126 +++++++++++++++++++++---------------------- docs/source/conf.py | 8 ++- 3 files changed, 69 insertions(+), 67 deletions(-) diff --git a/boticordpy/client.py b/boticordpy/client.py index 1d8aaed..e353f2c 100644 --- a/boticordpy/client.py +++ b/boticordpy/client.py @@ -62,7 +62,7 @@ class BoticordClient: Bot's shards count users ( :obj:`int` ) Bot's users count - + Returns: :obj:`~.types.ResourceBot`: ResourceBot object. diff --git a/boticordpy/types.py b/boticordpy/types.py index c10a442..22a60fe 100644 --- a/boticordpy/types.py +++ b/boticordpy/types.py @@ -213,37 +213,37 @@ class BotLibrary(IntEnum): DISCORD4J = 1 """Discord4j""" - + DISCORDCR = 2 """Discordcr""" - + DISCORDGO = 3 """DiscordGO""" - + DISCORDDOO = 4 """Discordoo""" - + DSHARPPLUS = 5 """DSharpPlus""" - + DISCORDJS = 6 """Discord.js""" - + DISCORDNET = 7 """Discord.Net""" - + DISCORDPY = 8 """discord.py""" - + ERIS = 9 """eris""" - + JAVACORD = 10 """JavaCord""" - + JDA = 11 """JDA""" - + OTHER = 12 """Other""" @@ -409,7 +409,7 @@ class UserLinks(APIObjectBase): self.custon = data.get("custom") return self - + @dataclass(repr=False) class UserBadge(APIObjectBase): @@ -435,9 +435,9 @@ class UserBadge(APIObjectBase): """ self: UserBadge = super().__new__(cls) - self.id = data['id'] - self.name = data['name'] - self.asset_url = data['assetURL'] + self.id = data["id"] + self.name = data["name"] + self.asset_url = data["assetURL"] return self @@ -554,11 +554,6 @@ class PartialUser(APIObjectBase): self.short_domain = data.get("shortDomain") return self - - -@dataclass(repr=False) -class ResourceBot(APIObjectBase): - """Tak nado""" @dataclass(repr=False) @@ -639,24 +634,24 @@ class ResourceServer(APIObjectBase): ---------- data: :class:`dict` The dictionary to convert into a ResourceServer.""" - + self = super().__new__(cls) - self.id = data['id'] - self.name = data['name'] - self.short_description = data['shortDescription'] - self.description = data['description)'] + self.id = data["id"] + self.name = data["name"] + self.short_description = data["shortDescription"] + self.description = data["description)"] self.avatar = data.get("avatar") self.short_link = data.get("shortLink") self.invite_link = data.get("inviteLink") self.owner = data.get("owner") self.website = data.get("website") self.up_count = data.get("upCount") - - self.premium_active = data['premium'].get('active') - self.premium_splash_url = data['premium'].get('splashURL') - self.premium_auto_fetch = data['premium'].get('autoFetch') - self.premium_banner_url = data['premium'].get('bannerURL') + + self.premium_active = data["premium"].get("active") + self.premium_splash_url = data["premium"].get("splashURL") + self.premium_auto_fetch = data["premium"].get("autoFetch") + self.premium_banner_url = data["premium"].get("bannerURL") self.status = ResourceStatus(data.get("status")) self.ratings = [ @@ -676,46 +671,14 @@ class ResourceServer(APIObjectBase): return self -@dataclass(repr=False) -class UserProfile(PartialUser): - """Information about user's profile from BotiCord. - - It has all from PartialUser and some more params: 'bots', 'servers', 'badges'""" - - badges: List[UserBadge] - """User's badges list.""" - - bots: List[ResourceBot] - """User's bots list""" - - servers: List[ResourceServer] - """User's servers list""" - - @classmethod - def from_dict(cls, data: dict): - """Generate a UserProfile from the given data. - - Parameters - ---------- - data: :class:`dict` - The dictionary to convert into a UserProfile.""" - - self = super().from_dict(data) - - self.badges = [UserBadge.from_dict(badge) for badge in data.get('badges', [])] - self.bots = [ResourceBot.from_dict(bot) for bot in data.get('bots', [])] - self.servers = [ResourceServer.from_dict(server) for server in data.get('servers', [])] - - return self - - @dataclass(repr=False) class ResourceBot(APIObjectBase): """Bot published on BotiCord .. warning:: - The result of the reverse conversion (`.to_dict()`) may not match the actual data.""" + The result of the reverse conversion (`.to_dict()`) may not match the actual data. + """ id: str """ID of the bot""" @@ -852,5 +815,40 @@ class ResourceBot(APIObjectBase): return self +@dataclass(repr=False) +class UserProfile(PartialUser): + """Information about user's profile from BotiCord. + + It has all from PartialUser and some more params: 'bots', 'servers', 'badges'""" + + badges: List[UserBadge] + """User's badges list.""" + + bots: List[ResourceBot] + """User's bots list""" + + servers: List[ResourceServer] + """User's servers list""" + + @classmethod + def from_dict(cls, data: dict): + """Generate a UserProfile from the given data. + + Parameters + ---------- + data: :class:`dict` + The dictionary to convert into a UserProfile.""" + + self = super().from_dict(data) + + self.badges = [UserBadge.from_dict(badge) for badge in data.get("badges", [])] + self.bots = [ResourceBot.from_dict(bot) for bot in data.get("bots", [])] + self.servers = [ + ResourceServer.from_dict(server) for server in data.get("servers", []) + ] + + return self + + class LinkDomain: pass diff --git a/docs/source/conf.py b/docs/source/conf.py index 961f62b..88ac24a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -42,10 +42,14 @@ extensions = [ "sphinx.ext.viewcode", "sphinx.ext.autosectionlabel", "sphinx.ext.extlinks", - "sphinxcontrib_trio" + "sphinxcontrib_trio", ] -autodoc_default_options = {"members": True, "show-inheritance": True, 'member-order': 'bysource'} +autodoc_default_options = { + "members": True, + "show-inheritance": True, + "member-order": "bysource", +} # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] From 08fb473865337884233ba4e6100d4ecf8ffaf98e Mon Sep 17 00:00:00 2001 From: MadCat9958 Date: Sun, 4 Jun 2023 13:25:22 +0300 Subject: [PATCH 15/34] no more webhooks --- examples/webhooks.py | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 examples/webhooks.py diff --git a/examples/webhooks.py b/examples/webhooks.py deleted file mode 100644 index 5b937a6..0000000 --- a/examples/webhooks.py +++ /dev/null @@ -1,20 +0,0 @@ -# You can use any library to interact with the Discord API. -# This example uses discord.py. -# You can install it with `pip install discord.py`. - -from discord.ext import commands -from boticordpy import webhook - -bot = commands.Bot(command_prefix="!") - - -async def edit_bot_comment(data): - print(data.comment.new) - - -boticord_webhook = webhook.Webhook("x-hook-key", "bot").register_listener( - "edit_bot_comment", edit_bot_comment -) -boticord_webhook.start(5000) - -bot.run("bot_token") From fbef6fe29abf2c9c64ab26598a77cfff73bfdc05 Mon Sep 17 00:00:00 2001 From: grey-cat-1908 Date: Sun, 4 Jun 2023 13:32:13 +0300 Subject: [PATCH 16/34] fixes && get_server_info() method --- LICENSE.txt | 2 +- boticordpy/client.py | 18 +++++++++++++++++- boticordpy/http.py | 11 +---------- boticordpy/types.py | 10 +++++----- 4 files changed, 24 insertions(+), 17 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index 7ab55d1..df4f53d 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright 2021 Victor Kotlin +Copyright 2021 - 2023 Viktor K Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/boticordpy/client.py b/boticordpy/client.py index e353f2c..55ad05f 100644 --- a/boticordpy/client.py +++ b/boticordpy/client.py @@ -43,7 +43,7 @@ class BoticordClient: response = await self.http.get_bot_info(bot_id) return boticord_types.ResourceBot.from_dict(response) - async def post_bot_stats( + async def get_server_info( self, bot_id: typing.Union[str, int], *, @@ -72,6 +72,22 @@ class BoticordClient: ) return boticord_types.ResourceBot.from_dict(response) + async def get_server_info( + self, server_id: typing.Union[str, int] + ) -> boticord_types.ResourceServer: + """Gets information about specified server. + + Args: + server_id (Union[:obj:`str`, :obj:`int`]) + Id of the server + + Returns: + :obj:`~.types.ResourceServer`: + ResourceServer object. + """ + response = await self.http.get_server_info(server_id) + return boticord_types.ResourceServer.from_dict(response) + def autopost(self) -> AutoPost: """Returns a helper instance for auto-posting. diff --git a/boticordpy/http.py b/boticordpy/http.py index 77c82cc..55eda40 100644 --- a/boticordpy/http.py +++ b/boticordpy/http.py @@ -57,15 +57,6 @@ class HttpClient: """Post bot's stats""" return self.make_request("POST", f"bots/{bot_id}/stats", json=stats) - def get_server_info(self, server_id: int): + def get_server_info(self, server_id: typing.Union[str, int]): """Get information about specified server""" return self.make_request("GET", f"servers/{server_id}") - - # TODO - def get_server_comments(self, server_id: int): - """Get list of specified server comments""" - return self.make_request("GET", f"server/{server_id}/comments") - - def get_user_info(self, user_id: int): - """Get information about the user""" - return self.make_request("GET", f"users/{user_id}") diff --git a/boticordpy/types.py b/boticordpy/types.py index 22a60fe..66667d3 100644 --- a/boticordpy/types.py +++ b/boticordpy/types.py @@ -637,10 +637,10 @@ class ResourceServer(APIObjectBase): self = super().__new__(cls) - self.id = data["id"] - self.name = data["name"] - self.short_description = data["shortDescription"] - self.description = data["description)"] + self.id = data.get("id") + self.name = data.get("name") + self.short_description = data.get("shortDescription") + self.description = data.get("description") self.avatar = data.get("avatar") self.short_link = data.get("shortLink") self.invite_link = data.get("inviteLink") @@ -658,7 +658,7 @@ class ResourceServer(APIObjectBase): ResourceRating.from_dict(rating) for rating in data.get("ratings", []) ] self.created_date = datetime.strptime( - data["createdDate"], "%Y-%m-%dT%H:%M:%S.%f%z" + data.get("createdDate"), "%Y-%m-%dT%H:%M:%S.%f%z" ) self.tags = [ServerTag(tag) for tag in data.get("tags", [])] self.ups = [ResourceUp.from_dict(up) for up in data.get("ups", [])] From 324b70dd2eff458729acfa88938862478de41be1 Mon Sep 17 00:00:00 2001 From: grey-cat-1908 Date: Sun, 4 Jun 2023 13:40:59 +0300 Subject: [PATCH 17/34] better docs for exceptions, UserProfile --- boticordpy/types.py | 4 +--- docs/source/api/exceptions.rst | 17 +++++++++++++++-- docs/source/api/types.rst | 2 ++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/boticordpy/types.py b/boticordpy/types.py index 66667d3..4d455c9 100644 --- a/boticordpy/types.py +++ b/boticordpy/types.py @@ -817,9 +817,7 @@ class ResourceBot(APIObjectBase): @dataclass(repr=False) class UserProfile(PartialUser): - """Information about user's profile from BotiCord. - - It has all from PartialUser and some more params: 'bots', 'servers', 'badges'""" + """Information about user's profile from BotiCord.'""" badges: List[UserBadge] """User's badges list.""" diff --git a/docs/source/api/exceptions.rst b/docs/source/api/exceptions.rst index 92ffe2c..1767b4a 100644 --- a/docs/source/api/exceptions.rst +++ b/docs/source/api/exceptions.rst @@ -1,7 +1,20 @@ +.. currentmodule:: boticordpy.exceptions + ########################## Exceptions API Reference ########################## -.. automodule:: boticordpy.exceptions +.. autoclass:: BoticordException + :members: + +.. autoclass:: InternalException + :members: + +.. autoclass:: HTTPException + :members: + +.. autoclass:: StatusCodes + :members: + +.. autoclass:: HTTPErrors :members: - :inherited-members: diff --git a/docs/source/api/types.rst b/docs/source/api/types.rst index c7cb70b..90945e4 100644 --- a/docs/source/api/types.rst +++ b/docs/source/api/types.rst @@ -51,3 +51,5 @@ Users .. autoclass:: UserProfile :members: + :exclude-members: to_dict + :inherited-members: From 1c9de56267947c761f002040988263d3e973d446 Mon Sep 17 00:00:00 2001 From: grey-cat-1908 Date: Sun, 4 Jun 2023 13:42:18 +0300 Subject: [PATCH 18/34] added UserBadge to docs --- docs/source/api/types.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/source/api/types.rst b/docs/source/api/types.rst index 90945e4..675d748 100644 --- a/docs/source/api/types.rst +++ b/docs/source/api/types.rst @@ -46,6 +46,9 @@ Users .. autoclass:: UserLinks :members: +.. autoclass:: UserBadge + :members: + .. autoclass:: PartialUser :members: From 238ff0ef7995d8e59d609cba463cbdd3bc578503 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80?= <61203964+grey-cat-1908@users.noreply.github.com> Date: Sun, 4 Jun 2023 13:46:15 +0300 Subject: [PATCH 19/34] fix mistake --- boticordpy/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/boticordpy/client.py b/boticordpy/client.py index 55ad05f..5a605a3 100644 --- a/boticordpy/client.py +++ b/boticordpy/client.py @@ -43,7 +43,7 @@ class BoticordClient: response = await self.http.get_bot_info(bot_id) return boticord_types.ResourceBot.from_dict(response) - async def get_server_info( + async def post_bot_stats( self, bot_id: typing.Union[str, int], *, From dcd77bacc36d3b1f2ed8cb7ecdf348719f22feb8 Mon Sep 17 00:00:00 2001 From: MadCat9958 Date: Sun, 4 Jun 2023 13:50:32 +0300 Subject: [PATCH 20/34] http make_request typehint + get_user_info --- boticordpy/client.py | 16 ++++++++++++++++ boticordpy/http.py | 6 +++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/boticordpy/client.py b/boticordpy/client.py index 5a605a3..c621cb9 100644 --- a/boticordpy/client.py +++ b/boticordpy/client.py @@ -87,6 +87,22 @@ class BoticordClient: """ response = await self.http.get_server_info(server_id) return boticord_types.ResourceServer.from_dict(response) + + async def get_user_info( + self, user_id: typing.Union[str, int] + ) -> boticord_types.UserProfile: + """Gets information about specified user. + + Args: + user_id (Union[:obj:`str`, :obj:`int`]) + Id of the user + + Returns: + :obj:`~.types.UserProfile`: + UserProfile object. + """ + response = await self.http.get_user_info(user_id) + return boticord_types.UserProfile.from_dict(response) def autopost(self) -> AutoPost: """Returns a helper instance for auto-posting. diff --git a/boticordpy/http.py b/boticordpy/http.py index 55eda40..954a102 100644 --- a/boticordpy/http.py +++ b/boticordpy/http.py @@ -29,7 +29,7 @@ class HttpClient: self.session = kwargs.get("session") or aiohttp.ClientSession(loop=loop) - async def make_request(self, method: str, endpoint: str, **kwargs): + async def make_request(self, method: str, endpoint: str, **kwargs) -> dict: """Send requests to the API""" kwargs["headers"] = {"Content-Type": "application/json"} @@ -60,3 +60,7 @@ class HttpClient: def get_server_info(self, server_id: typing.Union[str, int]): """Get information about specified server""" return self.make_request("GET", f"servers/{server_id}") + + def get_user_info(self, user_id: typing.Union[str, int]): + """Get information about specified user""" + return self.make_request("GET", f"users/{user_id}") From feb79398e968d7e883ea4f1ad0a01b5b8cf8f23a Mon Sep 17 00:00:00 2001 From: MadCat9958 Date: Sun, 4 Jun 2023 14:06:20 +0300 Subject: [PATCH 21/34] fixes --- boticordpy/types.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/boticordpy/types.py b/boticordpy/types.py index 4d455c9..918f088 100644 --- a/boticordpy/types.py +++ b/boticordpy/types.py @@ -401,12 +401,13 @@ class UserLinks(APIObjectBase): """ self: UserLinks = super().__new__(cls) + data = data or {} self.vk = data.get("vk") self.telegram = data.get("telegram") self.donate = data.get("donate") self.git = data.get("git") - self.custon = data.get("custom") + self.custom = data.get("custom") return self @@ -547,7 +548,7 @@ class PartialUser(APIObjectBase): self.discriminator = data["discriminator"] self.avatar = data.get("avatar") self.id = data["id"] - self.socials = UserLinks.from_dict(data["socials"]) + self.socials = UserLinks.from_dict(data.get("socials", {})) self.description = data.get("description") self.short_description = data.get("shortDescription") self.status = data.get("status") @@ -647,6 +648,7 @@ class ResourceServer(APIObjectBase): self.owner = data.get("owner") self.website = data.get("website") self.up_count = data.get("upCount") + self.standart_banner_id = data.get("standartBannerID") self.premium_active = data["premium"].get("active") self.premium_splash_url = data["premium"].get("splashURL") From 7feacc7540fddd31b652c94b8ecd53511584df70 Mon Sep 17 00:00:00 2001 From: MadCat9958 Date: Sun, 4 Jun 2023 18:50:22 +0300 Subject: [PATCH 22/34] fix typo --- setuo.cfg => setup.cfg | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename setuo.cfg => setup.cfg (100%) diff --git a/setuo.cfg b/setup.cfg similarity index 100% rename from setuo.cfg rename to setup.cfg From 241158b87a1215a7e8f16cb55864c0b613eb6147 Mon Sep 17 00:00:00 2001 From: MadCat9958 Date: Sun, 4 Jun 2023 19:04:51 +0300 Subject: [PATCH 23/34] year change --- boticordpy/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/boticordpy/__init__.py b/boticordpy/__init__.py index 9e868ea..424b322 100644 --- a/boticordpy/__init__.py +++ b/boticordpy/__init__.py @@ -2,14 +2,14 @@ Boticord API Wrapper ~~~~~~~~~~~~~~~~~~~ A basic wrapper for the BotiCord API. -:copyright: (c) 2022 Marakarka +:copyright: (c) 2023 Marakarka :license: MIT, see LICENSE for more details. """ __title__ = "boticordpy" __author__ = "Marakarka" __license__ = "MIT" -__copyright__ = "Copyright 2022 Marakarka" +__copyright__ = "Copyright 2023 Marakarka" __version__ = "2.2.2" from .client import BoticordClient From f1012e94651f734fb35549277c2bcc7456b08e9b Mon Sep 17 00:00:00 2001 From: grey-cat-1908 Date: Mon, 5 Jun 2023 12:51:48 +0300 Subject: [PATCH 24/34] test: ResourceServer convertation --- tests/test_convertation.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_convertation.py b/tests/test_convertation.py index 81d39df..a9d38b2 100644 --- a/tests/test_convertation.py +++ b/tests/test_convertation.py @@ -10,6 +10,17 @@ resource_bot_dict = { "createdDate": "2023-05-22T22:29:23.264Z", "premium": {}, } +resource_server_dict = { + "id": "722424773233213460", + "name": "BotiCord.top", + "tags": [ + 134, + 132 + ], + "status": 1, + "createdDate": "2023-05-23T15:16:45.387Z", + "premium": {}, +} def test_resource_up_convertation(): @@ -41,3 +52,10 @@ def test_resource_bot_convertation(): assert int(model_from_dict.created_date.timestamp()) == 1684794563 assert model_from_dict.status.name == "PUBLIC" + +def test_resource_server_convertation(): + model_from_dict = types.ResourceServer.from_dict(resource_server_dict) + + assert int(model_from_dict.created_date.timestamp()) == 1684855005 + assert model_from_dict.name == "BotiCord.top" + assert model_from_dict.tags[1].name == "GAMES" From d546175131f627b8b4ca2ced6526dddbf13e8014 Mon Sep 17 00:00:00 2001 From: grey-cat-1908 Date: Mon, 5 Jun 2023 13:00:28 +0300 Subject: [PATCH 25/34] test: UserProfile convertation --- boticordpy/client.py | 2 +- boticordpy/types.py | 2 +- tests/test_convertation.py | 21 +++++++++++++++++---- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/boticordpy/client.py b/boticordpy/client.py index c621cb9..6816833 100644 --- a/boticordpy/client.py +++ b/boticordpy/client.py @@ -87,7 +87,7 @@ class BoticordClient: """ response = await self.http.get_server_info(server_id) return boticord_types.ResourceServer.from_dict(response) - + async def get_user_info( self, user_id: typing.Union[str, int] ) -> boticord_types.UserProfile: diff --git a/boticordpy/types.py b/boticordpy/types.py index 918f088..917317d 100644 --- a/boticordpy/types.py +++ b/boticordpy/types.py @@ -545,7 +545,7 @@ class PartialUser(APIObjectBase): self: cls = super().__new__(cls) self.username = data["username"] - self.discriminator = data["discriminator"] + self.discriminator = data.get("discriminator") self.avatar = data.get("avatar") self.id = data["id"] self.socials = UserLinks.from_dict(data.get("socials", {})) diff --git a/tests/test_convertation.py b/tests/test_convertation.py index a9d38b2..2a5e55a 100644 --- a/tests/test_convertation.py +++ b/tests/test_convertation.py @@ -13,14 +13,17 @@ resource_bot_dict = { resource_server_dict = { "id": "722424773233213460", "name": "BotiCord.top", - "tags": [ - 134, - 132 - ], + "tags": [134, 132], "status": 1, "createdDate": "2023-05-23T15:16:45.387Z", "premium": {}, } +user_profile_dict = { + "id": "585766846268047370", + "username": "Marakarka", + "bots": [resource_bot_dict], + "shortDescription": None, +} def test_resource_up_convertation(): @@ -53,9 +56,19 @@ def test_resource_bot_convertation(): assert int(model_from_dict.created_date.timestamp()) == 1684794563 assert model_from_dict.status.name == "PUBLIC" + def test_resource_server_convertation(): model_from_dict = types.ResourceServer.from_dict(resource_server_dict) assert int(model_from_dict.created_date.timestamp()) == 1684855005 assert model_from_dict.name == "BotiCord.top" assert model_from_dict.tags[1].name == "GAMES" + + +def test_user_profile_convertation(): + model_from_dict = types.UserProfile.from_dict(user_profile_dict) + + assert model_from_dict.id == "585766846268047370" + assert model_from_dict.username == "Marakarka" + assert model_from_dict.short_description == None + assert model_from_dict.bots[0].id == "947141336451153931" From bde481924ef30a34bf6b2ffa19a0c8bd11712c4e Mon Sep 17 00:00:00 2001 From: grey-cat-1908 Date: Mon, 5 Jun 2023 13:06:50 +0300 Subject: [PATCH 26/34] updated setup.py --- LICENSE.txt | 2 +- setup.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index df4f53d..0463ab7 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright 2021 - 2023 Viktor K +Copyright 2021 - 2023 Viktor K (Marakarka) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/setup.py b/setup.py index b0b7a5b..9da7f02 100644 --- a/setup.py +++ b/setup.py @@ -44,10 +44,11 @@ setup( }, packages=find_packages(), version=version, - python_requires=">= 3.6", + python_requires=">= 3.8", description="A Python wrapper for BotiCord API", long_description=README, long_description_content_type="text/markdown", + include_package_data=True, url="https://github.com/boticord/boticordpy", author="Marakarka", author_email="support@kerdoku.top", @@ -55,7 +56,9 @@ setup( classifiers=[ "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", ], - install_requires=["aiohttp"], + install_requires=["aiohttp", "typing_extensions"], ) From e26366da74f8f87c3e75a254e3a7e086e15be288 Mon Sep 17 00:00:00 2001 From: grey-cat-1908 Date: Mon, 5 Jun 2023 13:11:25 +0300 Subject: [PATCH 27/34] updated examples/autopost.py --- examples/autopost.py | 10 ++++++---- setup.py | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/examples/autopost.py b/examples/autopost.py index ac370f2..6d41e6f 100644 --- a/examples/autopost.py +++ b/examples/autopost.py @@ -15,15 +15,17 @@ async def get_stats(): # Function that will be called if stats are posted successfully. async def on_success_posting(): - print("stats posting successfully") + print("wow stats posting works") -boticord_client = BoticordClient("Bot your_api_token", version=2) +boticord_client = BoticordClient( + "your_boticord_api_token", version=3 +) # <--- BotiCord API token autopost = ( boticord_client.autopost() .init_stats(get_stats) .on_success(on_success_posting) - .start() + .start("id_of_your_bot") # <--- ID of your bot ) -bot.run("bot token") +bot.run("bot token") # <--- Discord bot's token diff --git a/setup.py b/setup.py index 9da7f02..63e80aa 100644 --- a/setup.py +++ b/setup.py @@ -48,7 +48,7 @@ setup( description="A Python wrapper for BotiCord API", long_description=README, long_description_content_type="text/markdown", - include_package_data=True, + include_package_data=True, url="https://github.com/boticord/boticordpy", author="Marakarka", author_email="support@kerdoku.top", From 3244e70ebc3726a55f46265f9158d55145445e4e Mon Sep 17 00:00:00 2001 From: MadCat9958 Date: Tue, 6 Jun 2023 21:09:29 +0300 Subject: [PATCH 28/34] None BotLibrary --- boticordpy/types.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/boticordpy/types.py b/boticordpy/types.py index 918f088..a7cea51 100644 --- a/boticordpy/types.py +++ b/boticordpy/types.py @@ -247,6 +247,9 @@ class BotLibrary(IntEnum): OTHER = 12 """Other""" + NONE = 0 + """Bot's library doesn't specified""" + class ResourceStatus(IntEnum): """Status of the project on monitoring""" From daa2e317328c6044c47e3e35c78074087d01304e Mon Sep 17 00:00:00 2001 From: grey-cat-1908 Date: Wed, 7 Jun 2023 12:31:52 +0300 Subject: [PATCH 29/34] search --- boticordpy/__init__.py | 7 +- boticordpy/client.py | 77 +++++++++++- boticordpy/exceptions.py | 19 ++- boticordpy/http.py | 32 ++++- boticordpy/types.py | 241 +++++++++++++++++++++++++++++++++++++- docs/source/api/types.rst | 13 ++ 6 files changed, 374 insertions(+), 15 deletions(-) diff --git a/boticordpy/__init__.py b/boticordpy/__init__.py index 9e868ea..037ce87 100644 --- a/boticordpy/__init__.py +++ b/boticordpy/__init__.py @@ -2,16 +2,15 @@ Boticord API Wrapper ~~~~~~~~~~~~~~~~~~~ A basic wrapper for the BotiCord API. -:copyright: (c) 2022 Marakarka +:copyright: (c) 2023 Marakarka :license: MIT, see LICENSE for more details. """ __title__ = "boticordpy" __author__ = "Marakarka" __license__ = "MIT" -__copyright__ = "Copyright 2022 Marakarka" -__version__ = "2.2.2" +__copyright__ = "Copyright 2021 - 2023 Marakarka" +__version__ = "3.0.0a" from .client import BoticordClient - from .types import * diff --git a/boticordpy/client.py b/boticordpy/client.py index 6816833..4220d10 100644 --- a/boticordpy/client.py +++ b/boticordpy/client.py @@ -3,6 +3,7 @@ import typing from . import types as boticord_types from .http import HttpClient from .autopost import AutoPost +from .exceptions import MeilisearchException class BoticordClient: @@ -18,12 +19,13 @@ class BoticordClient: BotiCord API version (Default: 3) """ - __slots__ = ("http", "_autopost", "_token") + __slots__ = ("http", "_autopost", "_token", "_meilisearch_api_key") http: HttpClient def __init__(self, token: str = None, version: int = 3): self._token = token + self._meilisearch_api_key = None self._autopost: typing.Optional[AutoPost] = None self.http = HttpClient(token, version) @@ -104,6 +106,79 @@ class BoticordClient: response = await self.http.get_user_info(user_id) return boticord_types.UserProfile.from_dict(response) + async def __search_for(self, index, data): + """Search for smth on BotiCord""" + if self._meilisearch_api_key is None: + token_response = await self.http.get_search_key() + self._meilisearch_api_key = token_response["key"] + + try: + response = await self.http.search_for( + index, self._meilisearch_api_key, data + ) + except MeilisearchException: + token_response = await self.http.get_search_key() + self._meilisearch_api_key = token_response["key"] + + response = await self.http.search_for( + index, self._meilisearch_api_key, data + ) + + return response["hits"] + + async def search_for_bots( + self, **kwargs + ) -> typing.List[boticord_types.MeiliIndexedBot]: + """Search for bots on BotiCord. + + Note: + You can find every keyword argument `here `_. + + Returns: + List[:obj:`~.types.MeiliIndexedBot`]: + List of found bots + """ + + response = await self.__search_for("bots", kwargs) + return [boticord_types.MeiliIndexedBot.from_dict(bot) for bot in response] + + async def search_for_servers( + self, **kwargs + ) -> typing.List[boticord_types.MeiliIndexedServer]: + """Search for servers on BotiCord. + + Note: + You can find every keyword argument `here `_. + + Returns: + List[:obj:`~.types.MeiliIndexedServer`]: + List of found servers + """ + + response = await self.__search_for("servers", kwargs) + return [ + boticord_types.MeiliIndexedServer.from_dict(server) for server in response + ] + + async def search_for_comments( + self, **kwargs + ) -> typing.List[boticord_types.MeiliIndexedComment]: + """Search for comments on BotiCord. + + Note: + You can find every keyword argument `here `_. + + Returns: + List[:obj:`~.types.MeiliIndexedComment`]: + List of found comments + """ + + response = await self.__search_for("comments", kwargs) + return [ + boticord_types.MeiliIndexedComment.from_dict(comment) + for comment in response + ] + def autopost(self) -> AutoPost: """Returns a helper instance for auto-posting. diff --git a/boticordpy/exceptions.py b/boticordpy/exceptions.py index 0f43862..e1401c5 100644 --- a/boticordpy/exceptions.py +++ b/boticordpy/exceptions.py @@ -21,7 +21,7 @@ class InternalException(BoticordException): class HTTPException(BoticordException): - """Exception that's thrown when an HTTP request operation fails. + """Exception that's thrown when request to BotiCord API operation fails. Attributes ---------- @@ -37,6 +37,23 @@ class HTTPException(BoticordException): super().__init__(fmt) +class MeilisearchException(BoticordException): + """Exception that's thrown when request to Meilisearch API operation fails. + + Attributes + ---------- + response: + The response of the failed HTTP request. + """ + + def __init__(self, response): + self.response = response + + fmt = f"{self.response['code']} ({self.response['message']})" + + super().__init__(fmt) + + class StatusCodes(IntEnum): """Status codes of response""" diff --git a/boticordpy/http.py b/boticordpy/http.py index 954a102..10689b8 100644 --- a/boticordpy/http.py +++ b/boticordpy/http.py @@ -1,10 +1,10 @@ +from urllib.parse import urlparse import asyncio import typing import aiohttp from . import exceptions -from .types import LinkDomain class HttpClient: @@ -29,13 +29,17 @@ class HttpClient: self.session = kwargs.get("session") or aiohttp.ClientSession(loop=loop) - async def make_request(self, method: str, endpoint: str, **kwargs) -> dict: + async def make_request( + self, method: str, endpoint: str, *, meilisearch_token: str = None, **kwargs + ) -> dict: """Send requests to the API""" kwargs["headers"] = {"Content-Type": "application/json"} if self.token is not None: kwargs["headers"]["Authorization"] = self.token + if meilisearch_token is not None: + kwargs["headers"]["Authorization"] = f"Bearer {meilisearch_token}" url = f"{self.API_URL}{endpoint}" @@ -43,11 +47,14 @@ class HttpClient: data = await response.json() if (200, 201).__contains__(response.status): - return data["result"] + return data["result"] if not meilisearch_token else data else: - raise exceptions.HTTPException( - {"status": response.status, "error": data["errors"][0]["code"]} - ) + if not meilisearch_token: + raise exceptions.HTTPException( + {"status": response.status, "error": data["errors"][0]["code"]} + ) + else: + raise exceptions.MeilisearchException(data) def get_bot_info(self, bot_id: typing.Union[str, int]): """Get information about the specified bot""" @@ -64,3 +71,16 @@ class HttpClient: def get_user_info(self, user_id: typing.Union[str, int]): """Get information about specified user""" return self.make_request("GET", f"users/{user_id}") + + def get_search_key(self): + """Get API key for Meilisearch""" + return self.make_request("GET", f"search-key") + + def search_for(self, index: str, api_key: str, data: dict): + """Search for something on BotiCord.""" + return self.make_request( + "POST", + f"search/indexes/{index}/search", + meilisearch_token=api_key, + json=data, + ) diff --git a/boticordpy/types.py b/boticordpy/types.py index 917317d..4852bcf 100644 --- a/boticordpy/types.py +++ b/boticordpy/types.py @@ -559,7 +559,12 @@ class PartialUser(APIObjectBase): @dataclass(repr=False) class ResourceServer(APIObjectBase): - """Information about server from BotiCord.""" + """Information about server from BotiCord. + + .. warning:: + + The result of the reverse conversion (`.to_dict()`) may not match the actual data. + """ id: str """Server's ID""" @@ -700,6 +705,9 @@ class ResourceBot(APIObjectBase): short_link: Optional[str] """Short link to the bot's page""" + standart_banner_id: int + """Server's standart banner ID""" + invite_link: str """Invite link""" @@ -788,6 +796,7 @@ class ResourceBot(APIObjectBase): self.support_server_invite_link = data.get("support_server_invite") self.website = data.get("website") self.up_count = data.get("upCount") + self.standart_banner_id = data.get("standartBannerID") self.premium_active = data["premium"].get("active") self.premium_splash_url = data["premium"].get("splashURL") @@ -850,5 +859,231 @@ class UserProfile(PartialUser): return self -class LinkDomain: - pass +@dataclass(repr=False) +class MeiliIndexedBot(APIObjectBase): + """Bot found on BotiCord + + .. warning:: + + The result of the reverse conversion (`.to_dict()`) may not match the actual data. + """ + + id: str + """ID of the bot""" + + name: str + """Name of the bot""" + + short_description: str + """Short description of the bot""" + + description: str + """Description of the bot""" + + avatar: Optional[str] + """Avatar of the bot""" + + invite: str + """Invite link""" + + premium_active: bool + """Is premium status active? (True/False)""" + + premium_banner: Optional[str] + """Premium banner URL""" + + banner: int + """Standart banner""" + + rating: int + """Bot's rating""" + + discriminator: str + """Bot's discriminator""" + + library: Optional[BotLibrary] + """The library that the bot is based on""" + + guilds: Optional[int] + """Number of guilds""" + + shards: Optional[int] + """Number of shards""" + + members: Optional[int] + """Number of members""" + + tags: List[BotTag] + """List of bot tags""" + + ups: int + """List of bot's ups""" + + @classmethod + def from_dict(cls, data: dict): + """Generate a MeiliIndexedBot from the given data. + + Parameters + ---------- + data: :class:`dict` + The dictionary to convert into a MeiliIndexedBot. + """ + + self: MeiliIndexedBot = super().__new__(cls) + + self.id = data.get("id") + self.name = data.get("name") + self.short_description = data.get("shortDescription") + self.description = data.get("description") + self.avatar = data.get("avatar") + self.invite = data.get("invite") + self.discriminator = data.get("discriminator") + self.ups = data.get("ups") + self.rating = data.get("rating") + self.banner = data.get("banner") + + self.premium_active = data.get("premiumActive") + self.premium_banner = data.get("premiumBanner") + + self.library = ( + BotLibrary(data["library"]) if data.get("library") is not None else None + ) + self.tags = [BotTag(tag) for tag in data.get("tags", [])] + + self.guilds = data.get("guilds") + self.shards = data.get("shards") + self.members = data.get("members") + + return self + + +@dataclass(repr=False) +class MeiliIndexedServer(APIObjectBase): + """Server found on BotiCord + + .. warning:: + + The result of the reverse conversion (`.to_dict()`) may not match the actual data. + """ + + id: str + """ID of the server""" + + name: str + """Name of the server""" + + short_description: str + """Short description of the server""" + + description: str + """Description of the server""" + + avatar: Optional[str] + """Avatar of the server""" + + invite: str + """Invite link""" + + premium_active: bool + """Is premium status active? (True/False)""" + + premium_banner: Optional[str] + """Premium banner URL""" + + banner: int + """Standart banner""" + + discord_banner: Optional[str] + """Discord banner URL""" + + rating: int + """Server's rating""" + + members: Optional[int] + """Number of members""" + + tags: List[ServerTag] + """List of server tags""" + + ups: int + """List of server's ups""" + + @classmethod + def from_dict(cls, data: dict): + """Generate a MeiliIndexedServer from the given data. + + Parameters + ---------- + data: :class:`dict` + The dictionary to convert into a MeiliIndexedServer. + """ + + self: MeiliIndexedServer = super().__new__(cls) + + self.id = data.get("id") + self.name = data.get("name") + self.short_description = data.get("shortDescription") + self.description = data.get("description") + self.avatar = data.get("avatar") + self.invite = data.get("invite") + self.ups = data.get("ups") + self.rating = data.get("rating") + self.banner = data.get("banner") + + self.premium_active = data.get("premiumActive") + self.premium_banner = data.get("premiumBanner") + self.discord_banner = data.get("discordBanner") + + self.tags = [ServerTag(tag) for tag in data.get("tags", [])] + + self.members = data.get("members") + + return self + + +@dataclass(repr=False) +class MeiliIndexedComment(APIObjectBase): + """Comment found on BotiCord""" + + id: str + """ID of the comment""" + + author: str + """Id of the author of the comment""" + + rating: int + """Comment's rating""" + + content: str + """Content of the comment""" + + resource: str + """Id of the resource""" + + created: datetime + """When the comment was created""" + + mod_reply: Optional[str] + """Reply to the comment""" + + @classmethod + def from_dict(cls, data: dict): + """Generate a MeiliIndexedComment from the given data. + + Parameters + ---------- + data: :class:`dict` + The dictionary to convert into a MeiliIndexedComment. + """ + + self: MeiliIndexedComment = super().__new__(cls) + + self.id = data.get("id") + self.rating = data.get("rating") + self.author = data.get("author") + self.content = data.get("content") + self.resource = data.get("resource") + self.mod_reply = data.get("modReply") + self.created = datetime.utcfromtimestamp(data.get("created") / 1000) + + return self diff --git a/docs/source/api/types.rst b/docs/source/api/types.rst index 675d748..959cb54 100644 --- a/docs/source/api/types.rst +++ b/docs/source/api/types.rst @@ -56,3 +56,16 @@ Users :members: :exclude-members: to_dict :inherited-members: + + +MeiliSearch +------------ + +.. autoclass:: MeiliIndexedBot + :members: + +.. autoclass:: MeiliIndexedServer + :members: + +.. autoclass:: MeiliIndexedComment + :members: From 3c1a002e815c0cb4047ec9197547d00fcf1bdc11 Mon Sep 17 00:00:00 2001 From: grey-cat-1908 Date: Wed, 7 Jun 2023 12:35:35 +0300 Subject: [PATCH 30/34] update docs --- docs/source/api/exceptions.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/source/api/exceptions.rst b/docs/source/api/exceptions.rst index 1767b4a..ac2cea0 100644 --- a/docs/source/api/exceptions.rst +++ b/docs/source/api/exceptions.rst @@ -13,6 +13,9 @@ Exceptions API Reference .. autoclass:: HTTPException :members: +.. autoclass:: MeilisearchException + :members: + .. autoclass:: StatusCodes :members: From 0ad0180f7eeffe75b913e3c7a5881d7aa89b1b84 Mon Sep 17 00:00:00 2001 From: grey-cat-1908 Date: Tue, 27 Jun 2023 16:07:40 +0300 Subject: [PATCH 31/34] BotiCord Websocket --- boticordpy/__init__.py | 1 + boticordpy/websocket.py | 124 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 boticordpy/websocket.py diff --git a/boticordpy/__init__.py b/boticordpy/__init__.py index 037ce87..422174b 100644 --- a/boticordpy/__init__.py +++ b/boticordpy/__init__.py @@ -14,3 +14,4 @@ __version__ = "3.0.0a" from .client import BoticordClient from .types import * +from .websocket import BotiCordWebsocket diff --git a/boticordpy/websocket.py b/boticordpy/websocket.py new file mode 100644 index 0000000..5692f41 --- /dev/null +++ b/boticordpy/websocket.py @@ -0,0 +1,124 @@ +# Copyright Marakarka (Viktor K) 2021 - Present +# Full MIT License can be found in `LICENSE.txt` at the project root. + +import logging +import json +import asyncio +import typing + +import aiohttp + +_logger = logging.getLogger("boticord.websocket") + + +class BotiCordWebsocket: + def __init__(self, token: str): + self.__session = None + self.loop = asyncio.get_event_loop() + self.ws = None + self._listeners = {} + self.not_closed = True + + self._token = token + + def listener(self): + """Decorator to set the listener.""" + + def inner(func): + if not asyncio.iscoroutinefunction(func): + raise TypeError(f"<{func.__qualname__}> must be a coroutine function") + self._listeners[func.__qualname__] = func + return func + + return inner + + def register_listener(self, notification_type: str, callback: typing.Any): + """Method to set the listener. + Args: + notify_type (:obj:`str`) + Type of notification (Check reference page) + callback (:obj:`function`) + Coroutine Callback Function + """ + if not asyncio.iscoroutinefunction(callback): + raise TypeError(f"<{callback.__qualname__}> must be a coroutine function") + + self._listeners[notification_type] = callback + return self + + async def connect(self) -> None: + """Connect to BotiCord.""" + try: + self.__session = aiohttp.ClientSession() + self.ws = await self.__session.ws_connect( + "wss://gateway.arbuz.pro/websocket/", + timeout=30.0, + ) + + _logger.info("Connected to BotiCord.") + + self.not_closed = True + + self.loop.create_task(self._receive()) + await self._send_identify() + except Exception as exc: + _logger.error("Connecting failed!") + + raise exc + + async def _send_identify(self) -> None: + await self.ws.send_json({"event": "auth", "data": {"token": self._token}}) + + async def _receive(self) -> None: + while self.not_closed: + async for msg in self.ws: + if msg.type == aiohttp.WSMsgType.TEXT: + await self._handle_data(msg.data) + else: + raise RuntimeError + + close_code = self.ws.close_code + + if close_code is not None: + await self._handle_close(close_code) + + async def _handle_data(self, data): + data = json.loads(data) + + if data["event"] == "hello": + _logger.info("Authorized successfully.") + self.loop.create_task(self._send_ping()) + elif data["event"] == "notify": + listener = self._listeners.get(data["data"]["type"]) + if listener: + self.loop.create_task(listener(data["data"])) + elif data["event"] == "pong": + _logger.info("Received pong-response.") + self.loop.create_task(self._send_ping()) + else: + _logger.error("An error has occurred.") + + async def _handle_close(self, code: int) -> None: + self.not_closed = False + await self.__session.close() + + if code == 4000: + _logger.info("Closed connection successfully.") + return + elif code == 1006: + _logger.error("Token is invalid.") + return + + _logger.info("Disconnected from BotiCord. Reconnecting...") + + await self.connect() + + async def _send_ping(self) -> None: + if not self.ws.closed: + await asyncio.sleep(45) + await self.ws.send_json({"event": "ping"}) + + async def close(self) -> None: + if self.ws: + self.not_closed = False + await self.ws.close(code=4000) From e7ed02dee0998722e025b0b24c8f85e5b4812da5 Mon Sep 17 00:00:00 2001 From: grey-cat-1908 Date: Tue, 27 Jun 2023 16:24:03 +0300 Subject: [PATCH 32/34] docs for websockets --- boticordpy/websocket.py | 15 ++++++++++++++- docs/source/index.rst | 1 + docs/source/websocket.rst | 22 ++++++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 docs/source/websocket.rst diff --git a/boticordpy/websocket.py b/boticordpy/websocket.py index 5692f41..7c890dd 100644 --- a/boticordpy/websocket.py +++ b/boticordpy/websocket.py @@ -12,6 +12,8 @@ _logger = logging.getLogger("boticord.websocket") class BotiCordWebsocket: + """Represents a client that can be used to interact with the BotiCord by websocket connection.""" + def __init__(self, token: str): self.__session = None self.loop = asyncio.get_event_loop() @@ -22,7 +24,16 @@ class BotiCordWebsocket: self._token = token def listener(self): - """Decorator to set the listener.""" + """Decorator to set the listener (must be a coroutine function). + + For example: + + .. code-block:: python + + @websocket.listener() + async def comment_removed(data): + pass + """ def inner(func): if not asyncio.iscoroutinefunction(func): @@ -34,6 +45,7 @@ class BotiCordWebsocket: def register_listener(self, notification_type: str, callback: typing.Any): """Method to set the listener. + Args: notify_type (:obj:`str`) Type of notification (Check reference page) @@ -119,6 +131,7 @@ class BotiCordWebsocket: await self.ws.send_json({"event": "ping"}) async def close(self) -> None: + """Close websocket connection with BotiCord""" if self.ws: self.not_closed = False await self.ws.close(code=4000) diff --git a/docs/source/index.rst b/docs/source/index.rst index c903f84..1d2dd05 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -11,6 +11,7 @@ This is a documentation for wrapper for BotiCord API. quickstart api + websocket Links ===== diff --git a/docs/source/websocket.rst b/docs/source/websocket.rst new file mode 100644 index 0000000..5fcc5ec --- /dev/null +++ b/docs/source/websocket.rst @@ -0,0 +1,22 @@ +.. currentmodule:: boticordpy.websocket + +########### +WebSocket +########### + +BotiCord Websocket +------------------- + +.. autoclass:: BotiCordWebsocket + :exclude-members: listener + :inherited-members: + + .. automethod:: BotiCordWebsocket.listener() + :decorator: + + +Notification types +------------------- +.. function:: comment_removed(data) + + Called when comment is deleted. \ No newline at end of file From 783c6cb9deb80e16cdab16f08a3db475027ff96a Mon Sep 17 00:00:00 2001 From: grey-cat-1908 Date: Tue, 27 Jun 2023 16:33:35 +0300 Subject: [PATCH 33/34] logging --- boticordpy/autopost.py | 15 +++++++++++++++ boticordpy/client.py | 16 +++++++++++++++- boticordpy/websocket.py | 16 +++++++++++++++- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/boticordpy/autopost.py b/boticordpy/autopost.py index c463f09..5ac5697 100644 --- a/boticordpy/autopost.py +++ b/boticordpy/autopost.py @@ -1,8 +1,11 @@ import asyncio import typing +import logging from . import exceptions as bexc +_logger = logging.getLogger("boticord.autopost") + class AutoPost: """ @@ -69,6 +72,8 @@ class AutoPost: self._success = callback return func + _logger.info("Registering success callback") + return inner def on_error(self, callback: typing.Any = None): @@ -95,6 +100,8 @@ class AutoPost: self._error = callback return func + _logger.info("Registering error callback") + return inner def init_stats(self, callback: typing.Any = None): @@ -120,6 +127,8 @@ class AutoPost: self._stats = callback return func + _logger.info("Registered stats initialization function") + return inner @property @@ -150,6 +159,7 @@ class AutoPost: stats = await self._stats() try: await self.client.http.post_bot_stats(self.bot_id, stats) + _logger.info("Tried to post bot stats") except Exception as err: on_error = getattr(self, "_error", None) if on_error: @@ -189,6 +199,9 @@ class AutoPost: task = asyncio.ensure_future(self._internal_loop()) self._task = task + + _logger.info("Started autoposting") + return task def stop(self) -> None: @@ -199,3 +212,5 @@ class AutoPost: return None self._stopped = True + + _logger.info("Stopped autoposting") diff --git a/boticordpy/client.py b/boticordpy/client.py index 4220d10..fdfb9fc 100644 --- a/boticordpy/client.py +++ b/boticordpy/client.py @@ -1,10 +1,13 @@ import typing +import logging from . import types as boticord_types from .http import HttpClient from .autopost import AutoPost from .exceptions import MeilisearchException +_logger = logging.getLogger("boticord") + class BoticordClient: """Represents a client that can be used to interact with the BotiCord API. @@ -42,6 +45,8 @@ class BoticordClient: :obj:`~.types.ResourceBot`: ResourceBot object. """ + _logger.info("Requesting information about bot") + response = await self.http.get_bot_info(bot_id) return boticord_types.ResourceBot.from_dict(response) @@ -69,6 +74,8 @@ class BoticordClient: :obj:`~.types.ResourceBot`: ResourceBot object. """ + _logger.info("Posting bot stats") + response = await self.http.post_bot_stats( bot_id, {"servers": servers, "shards": shards, "users": users} ) @@ -87,6 +94,8 @@ class BoticordClient: :obj:`~.types.ResourceServer`: ResourceServer object. """ + _logger.info("Requesting information about server") + response = await self.http.get_server_info(server_id) return boticord_types.ResourceServer.from_dict(response) @@ -103,11 +112,13 @@ class BoticordClient: :obj:`~.types.UserProfile`: UserProfile object. """ + _logger.info("Requesting information about user") + response = await self.http.get_user_info(user_id) return boticord_types.UserProfile.from_dict(response) async def __search_for(self, index, data): - """Search for smth on BotiCord""" + """Search for something on BotiCord""" if self._meilisearch_api_key is None: token_response = await self.http.get_search_key() self._meilisearch_api_key = token_response["key"] @@ -138,6 +149,7 @@ class BoticordClient: List[:obj:`~.types.MeiliIndexedBot`]: List of found bots """ + _logger.info("Searching for bots on BotiCord") response = await self.__search_for("bots", kwargs) return [boticord_types.MeiliIndexedBot.from_dict(bot) for bot in response] @@ -154,6 +166,7 @@ class BoticordClient: List[:obj:`~.types.MeiliIndexedServer`]: List of found servers """ + _logger.info("Searching for servers on BotiCord") response = await self.__search_for("servers", kwargs) return [ @@ -172,6 +185,7 @@ class BoticordClient: List[:obj:`~.types.MeiliIndexedComment`]: List of found comments """ + _logger.info("Searching for comments on BotiCord") response = await self.__search_for("comments", kwargs) return [ diff --git a/boticordpy/websocket.py b/boticordpy/websocket.py index 7c890dd..8ab5f72 100644 --- a/boticordpy/websocket.py +++ b/boticordpy/websocket.py @@ -24,7 +24,13 @@ class BotiCordWebsocket: self._token = token def listener(self): - """Decorator to set the listener (must be a coroutine function). + """Decorator to set the listener. + + .. warning:: + + Callback functions must be a **coroutine**. If they aren't, then you might get unexpected + errors. In order to turn a function into a coroutine they must be ``async def`` + functions. For example: @@ -39,6 +45,7 @@ class BotiCordWebsocket: if not asyncio.iscoroutinefunction(func): raise TypeError(f"<{func.__qualname__}> must be a coroutine function") self._listeners[func.__qualname__] = func + _logger.debug(f"Listener {func.__qualname__} added successfully!") return func return inner @@ -51,11 +58,18 @@ class BotiCordWebsocket: Type of notification (Check reference page) callback (:obj:`function`) Coroutine Callback Function + + .. warning:: + + Callback functions must be a **coroutine**. If they aren't, then you might get unexpected + errors. In order to turn a function into a coroutine they must be ``async def`` + functions. """ if not asyncio.iscoroutinefunction(callback): raise TypeError(f"<{callback.__qualname__}> must be a coroutine function") self._listeners[notification_type] = callback + _logger.debug(f"Listener {callback.__qualname__} added successfully!") return self async def connect(self) -> None: From f036b31f21132e6ba35556205eff0dc518f9643e Mon Sep 17 00:00:00 2001 From: grey-cat-1908 Date: Tue, 27 Jun 2023 18:26:42 +0300 Subject: [PATCH 34/34] Made project ready to release --- ACKNOWLEDGEMENTS.txt | 8 ++++++++ README.md | 19 ++++++++++++------- boticordpy/__init__.py | 2 +- boticordpy/autopost.py | 4 ++-- boticordpy/client.py | 8 ++++---- boticordpy/http.py | 3 +-- boticordpy/websocket.py | 14 +++++++------- examples/stats_melisa.py | 16 ++++++++++++++++ examples/websocket.py | 23 +++++++++++++++++++++++ 9 files changed, 74 insertions(+), 23 deletions(-) create mode 100644 ACKNOWLEDGEMENTS.txt create mode 100644 examples/stats_melisa.py create mode 100644 examples/websocket.py diff --git a/ACKNOWLEDGEMENTS.txt b/ACKNOWLEDGEMENTS.txt new file mode 100644 index 0000000..b34de64 --- /dev/null +++ b/ACKNOWLEDGEMENTS.txt @@ -0,0 +1,8 @@ +These are the open source libraries we use: +- aiohttp (https://github.com/aio-libs/aiohttp) +- typing_extensions (https://github.com/python/typing_extensions) +- sphinx (https://github.com/sphinx-doc/sphinx) +- furo (https://github.com/pradyunsg/furo) + +It is also important to note that some developments of Melisa (https://github.com/MelisaDev/melisa) were used in the development of the project. +I would also like to express my gratitude to the former admin staff of the BotiCord service (until 06/02/2023) and the entire project community. \ No newline at end of file diff --git a/README.md b/README.md index 0cf1b2f..fc3edaa 100644 --- a/README.md +++ b/README.md @@ -22,12 +22,12 @@ * Object-oriented * Full BotiCord API Coverage * Modern Pythonic API using `async`/`await` syntax -* BotiCord Webhooks +* BotiCord Websocket * It is not necessary to use any particular library used to interact with the Discord API.

Installation

-Python 3.6 or newer is required. +Python 3.8 or newer is required. Enter one of these commands to install the library: @@ -49,28 +49,33 @@ You can find other examples in an examples folder. ```py from discord.ext import commands - from boticordpy import BoticordClient bot = commands.Bot(command_prefix="!") +# Function that will return the current bot's stats. async def get_stats(): return {"servers": len(bot.guilds), "shards": 0, "users": len(bot.users)} +# Function that will be called if stats are posted successfully. async def on_success_posting(): - print("stats posting successfully") + print("wow stats posting works") -boticord_client = BoticordClient("your_api_token") + +boticord_client = BoticordClient( + "your_boticord_api_token", version=3 +) # <--- BotiCord API token autopost = ( boticord_client.autopost() .init_stats(get_stats) .on_success(on_success_posting) - .start() + .start("id_of_your_bot") # <--- ID of your bot ) -bot.run("bot token") +bot.run("bot token") # <--- Discord bot's token + ```

Links

diff --git a/boticordpy/__init__.py b/boticordpy/__init__.py index 422174b..596f461 100644 --- a/boticordpy/__init__.py +++ b/boticordpy/__init__.py @@ -10,7 +10,7 @@ __title__ = "boticordpy" __author__ = "Marakarka" __license__ = "MIT" __copyright__ = "Copyright 2021 - 2023 Marakarka" -__version__ = "3.0.0a" +__version__ = "3.0.0" from .client import BoticordClient from .types import * diff --git a/boticordpy/autopost.py b/boticordpy/autopost.py index 5ac5697..321e050 100644 --- a/boticordpy/autopost.py +++ b/boticordpy/autopost.py @@ -199,9 +199,9 @@ class AutoPost: task = asyncio.ensure_future(self._internal_loop()) self._task = task - + _logger.info("Started autoposting") - + return task def stop(self) -> None: diff --git a/boticordpy/client.py b/boticordpy/client.py index fdfb9fc..aab9970 100644 --- a/boticordpy/client.py +++ b/boticordpy/client.py @@ -46,7 +46,7 @@ class BoticordClient: ResourceBot object. """ _logger.info("Requesting information about bot") - + response = await self.http.get_bot_info(bot_id) return boticord_types.ResourceBot.from_dict(response) @@ -75,7 +75,7 @@ class BoticordClient: ResourceBot object. """ _logger.info("Posting bot stats") - + response = await self.http.post_bot_stats( bot_id, {"servers": servers, "shards": shards, "users": users} ) @@ -95,7 +95,7 @@ class BoticordClient: ResourceServer object. """ _logger.info("Requesting information about server") - + response = await self.http.get_server_info(server_id) return boticord_types.ResourceServer.from_dict(response) @@ -113,7 +113,7 @@ class BoticordClient: UserProfile object. """ _logger.info("Requesting information about user") - + response = await self.http.get_user_info(user_id) return boticord_types.UserProfile.from_dict(response) diff --git a/boticordpy/http.py b/boticordpy/http.py index 10689b8..4a62222 100644 --- a/boticordpy/http.py +++ b/boticordpy/http.py @@ -1,4 +1,3 @@ -from urllib.parse import urlparse import asyncio import typing @@ -23,7 +22,7 @@ class HttpClient: def __init__(self, auth_token: str = None, version: int = 3, **kwargs): self.token = auth_token - self.API_URL = f"https://api.arbuz.pro/" + self.API_URL = f"https://api.boticord.top/v{version}" loop = kwargs.get("loop") or asyncio.get_event_loop() diff --git a/boticordpy/websocket.py b/boticordpy/websocket.py index 8ab5f72..3b2bb70 100644 --- a/boticordpy/websocket.py +++ b/boticordpy/websocket.py @@ -13,7 +13,7 @@ _logger = logging.getLogger("boticord.websocket") class BotiCordWebsocket: """Represents a client that can be used to interact with the BotiCord by websocket connection.""" - + def __init__(self, token: str): self.__session = None self.loop = asyncio.get_event_loop() @@ -25,17 +25,17 @@ class BotiCordWebsocket: def listener(self): """Decorator to set the listener. - + .. warning:: Callback functions must be a **coroutine**. If they aren't, then you might get unexpected errors. In order to turn a function into a coroutine they must be ``async def`` functions. - + For example: - + .. code-block:: python - + @websocket.listener() async def comment_removed(data): pass @@ -58,7 +58,7 @@ class BotiCordWebsocket: Type of notification (Check reference page) callback (:obj:`function`) Coroutine Callback Function - + .. warning:: Callback functions must be a **coroutine**. If they aren't, then you might get unexpected @@ -77,7 +77,7 @@ class BotiCordWebsocket: try: self.__session = aiohttp.ClientSession() self.ws = await self.__session.ws_connect( - "wss://gateway.arbuz.pro/websocket/", + "wss://gateway.boticord.top/websocket/", timeout=30.0, ) diff --git a/examples/stats_melisa.py b/examples/stats_melisa.py new file mode 100644 index 0000000..a6bb181 --- /dev/null +++ b/examples/stats_melisa.py @@ -0,0 +1,16 @@ +import melisa +from boticordpy import BoticordClient + +bot = melisa.Bot("your_discord_bot_token") + +boticord = BoticordClient("your_boticord_api_token") + + +@bot.listen +async def on_message_create(message): + if message.content.startswith("!guilds"): + data = await boticord.get_bot_info(bot.user.id) + await bot.rest.create_message(message.channel.id, data.guilds) + + +bot.run_autosharded() diff --git a/examples/websocket.py b/examples/websocket.py new file mode 100644 index 0000000..7505050 --- /dev/null +++ b/examples/websocket.py @@ -0,0 +1,23 @@ +# You can use any library to interact with the Discord API. +# This example uses discord.py. +# You can install it with `pip install discord.py`. + +from discord.ext import commands +from boticordpy import BotiCordWebsocket + +bot = commands.Bot(command_prefix="!") + +websocket = BotiCordWebsocket("your_boticord_api_token") # <--- BotiCord API token + + +@websocket.listener() +async def comment_removed(data): + print(data["payload"]) + + +@bot.event +async def on_ready(): + await websocket.connect() + + +bot.run("bot token") # <--- Discord bot's token