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: