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/boticordpy/__init__.py b/boticordpy/__init__.py index 424b322..037ce87 100644 --- a/boticordpy/__init__.py +++ b/boticordpy/__init__.py @@ -9,9 +9,8 @@ A basic wrapper for the BotiCord API. __title__ = "boticordpy" __author__ = "Marakarka" __license__ = "MIT" -__copyright__ = "Copyright 2023 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 c621cb9..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) @@ -87,7 +89,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: @@ -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 a7cea51..11a374c 100644 --- a/boticordpy/types.py +++ b/boticordpy/types.py @@ -548,7 +548,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", {})) @@ -562,7 +562,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""" @@ -703,6 +708,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""" @@ -791,6 +799,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") @@ -853,5 +862,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/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: 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: 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 b0b7a5b..63e80aa 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"], ) diff --git a/tests/test_convertation.py b/tests/test_convertation.py index 81d39df..2a5e55a 100644 --- a/tests/test_convertation.py +++ b/tests/test_convertation.py @@ -10,6 +10,20 @@ 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": {}, +} +user_profile_dict = { + "id": "585766846268047370", + "username": "Marakarka", + "bots": [resource_bot_dict], + "shortDescription": None, +} def test_resource_up_convertation(): @@ -41,3 +55,20 @@ 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"