From bf6ca24eeda8eec434b749a17442c7a5a6211f26 Mon Sep 17 00:00:00 2001 From: profitroll Date: Thu, 20 Feb 2025 22:51:01 +0100 Subject: [PATCH] WIP: Guilds and Wallets --- api/extensions/guild.py | 40 ++++++++++++++++++++++++++++++++++ api/extensions/user.py | 23 +++++++++++++++++++ api/extensions/utils.py | 4 ++++ classes/__init__.py | 3 ++- classes/errors/__init__.py | 1 + classes/errors/pycord_guild.py | 7 ++++++ classes/pycord_guild.py | 35 +++++++++++++++++++++++++++++ classes/pycord_user.py | 16 ++++++++++---- classes/wallet.py | 16 ++++++++++---- cli.py | 27 +++++++++++++++++++++++ cogs/wallet.py | 5 +++++ main.py | 18 ++++++++++----- 12 files changed, 181 insertions(+), 14 deletions(-) create mode 100644 api/extensions/guild.py create mode 100644 api/extensions/user.py create mode 100644 classes/errors/pycord_guild.py create mode 100644 cli.py create mode 100644 cogs/wallet.py diff --git a/api/extensions/guild.py b/api/extensions/guild.py new file mode 100644 index 0000000..27f9b7f --- /dev/null +++ b/api/extensions/guild.py @@ -0,0 +1,40 @@ +import logging +from logging import Logger + +from fastapi import HTTPException, status +from fastapi.responses import JSONResponse + +from api.app import app +from classes import PycordGuild +from classes.errors import WalletNotFoundError, GuildNotFoundError +from classes.wallet import Wallet + +logger: Logger = logging.getLogger(__name__) + + +@app.get("/v1/guilds/{guild_id}", response_class=JSONResponse) +async def get_guild_wallet(guild_id: int): + try: + guild: PycordGuild = await PycordGuild.from_id(guild_id, allow_creation=False) + except GuildNotFoundError as exc: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Guild not found" + ) from exc + except NotImplementedError as exc: + raise HTTPException( + status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented" + ) from exc + + return guild.to_dict(json_compatible=True) + + +@app.get("/v1/guilds/{guild_id}/wallets/{user_id}", response_class=JSONResponse) +async def get_guild_wallet(guild_id: int, user_id: int): + try: + wallet: Wallet = await Wallet.from_id(user_id, guild_id, allow_creation=False) + except WalletNotFoundError as exc: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Wallet not found" + ) from exc + + return wallet.to_dict(json_compatible=True) diff --git a/api/extensions/user.py b/api/extensions/user.py new file mode 100644 index 0000000..fa4614a --- /dev/null +++ b/api/extensions/user.py @@ -0,0 +1,23 @@ +import logging +from logging import Logger + +from fastapi import HTTPException, status +from fastapi.responses import JSONResponse + +from api.app import app +from classes import PycordUser +from classes.errors import UserNotFoundError + +logger: Logger = logging.getLogger(__name__) + + +@app.get("/v1/users/{user_id}", response_class=JSONResponse) +async def get_user(user_id: int): + try: + user: PycordUser = await PycordUser.from_id(user_id, allow_creation=False) + except UserNotFoundError as exc: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="User not found" + ) from exc + + return user.to_dict(json_compatible=True) diff --git a/api/extensions/utils.py b/api/extensions/utils.py index 06e60f6..e1cd90d 100644 --- a/api/extensions/utils.py +++ b/api/extensions/utils.py @@ -1,9 +1,13 @@ +import logging +from logging import Logger from pathlib import Path from fastapi.responses import FileResponse from api.app import app +logger: Logger = logging.getLogger(__name__) + @app.get("/favicon.ico", response_class=FileResponse, include_in_schema=False) async def favicon(): diff --git a/classes/__init__.py b/classes/__init__.py index ac0044a..580c84d 100644 --- a/classes/__init__.py +++ b/classes/__init__.py @@ -1,4 +1,5 @@ from .pycord_guild import PycordGuild from .pycord_guild_colors import PycordGuildColors from .pycord_user import PycordUser -from .wallet import Wallet + +# from .wallet import Wallet diff --git a/classes/errors/__init__.py b/classes/errors/__init__.py index 604e596..875289e 100644 --- a/classes/errors/__init__.py +++ b/classes/errors/__init__.py @@ -1,3 +1,4 @@ +from .pycord_guild import GuildNotFoundError from .pycord_user import UserNotFoundError from .wallet import ( WalletBalanceLimitExceeded, diff --git a/classes/errors/pycord_guild.py b/classes/errors/pycord_guild.py new file mode 100644 index 0000000..21a89b7 --- /dev/null +++ b/classes/errors/pycord_guild.py @@ -0,0 +1,7 @@ +class GuildNotFoundError(Exception): + """PycordGuild could not find guild with such an ID in the database""" + + def __init__(self, guild_id: int) -> None: + self.guild_id = guild_id + + super().__init__(f"Guild with id {self.guild_id} was not found") diff --git a/classes/pycord_guild.py b/classes/pycord_guild.py index b0889e6..abb9df9 100644 --- a/classes/pycord_guild.py +++ b/classes/pycord_guild.py @@ -1,6 +1,8 @@ from dataclasses import dataclass +from typing import Dict, Any, Optional from bson import ObjectId +from libbot.cache.classes import Cache @dataclass @@ -10,3 +12,36 @@ class PycordGuild: def __init__(self) -> None: raise NotImplementedError() + + @classmethod + async def from_id( + cls, guild_id: int, allow_creation: bool = True, cache: Optional[Cache] = None + ) -> "PycordGuild": + """Find guild in database and create new record if guild does not exist. + + Args: + guild_id (int): User's Discord ID + allow_creation (:obj:`bool`, optional): Create new guild record if none found in the database + cache (:obj:`Cache`, optional): Cache engine to get the cache from + + Returns: + PycordGuild: User object + + Raises: + GuildNotFoundError: User was not found and creation was not allowed + """ + raise NotImplementedError() + + def to_dict(self, json_compatible: bool = False) -> Dict[str, Any]: + """Convert PycordGuild object to a JSON representation. + + Args: + json_compatible (bool): Whether the JSON-incompatible objects like ObjectId need to be converted + + Returns: + Dict[str, Any]: JSON representation of PycordGuild + """ + return { + "_id": self._id if not json_compatible else str(self._id), + "id": self.id, + } diff --git a/classes/pycord_user.py b/classes/pycord_user.py index f6248ec..4dd9be8 100644 --- a/classes/pycord_user.py +++ b/classes/pycord_user.py @@ -7,8 +7,8 @@ from bson import ObjectId from libbot.cache.classes import Cache from pymongo.results import InsertOneResult -from classes import Wallet from classes.errors.pycord_user import UserNotFoundError +from classes.wallet import Wallet from modules.database import col_users logger: Logger = logging.getLogger(__name__) @@ -63,9 +63,17 @@ class PycordUser: return cls(**db_entry) - def _to_dict(self) -> Dict[str, Any]: + def to_dict(self, json_compatible: bool = False) -> Dict[str, Any]: + """Convert PycordUser object to a JSON representation. + + Args: + json_compatible (bool): Whether the JSON-incompatible objects like ObjectId need to be converted + + Returns: + Dict[str, Any]: JSON representation of PycordUser + """ return { - "_id": self._id, + "_id": self._id if not json_compatible else str(self._id), "id": self.id, } @@ -117,7 +125,7 @@ class PycordUser: if cache is None: return - user_dict: Dict[str, Any] = self._to_dict() + user_dict: Dict[str, Any] = self.to_dict() if user_dict is not None: cache.set_json(self._get_cache_key(), user_dict) diff --git a/classes/wallet.py b/classes/wallet.py index 6302335..fcbac8f 100644 --- a/classes/wallet.py +++ b/classes/wallet.py @@ -7,12 +7,12 @@ from typing import Any, Dict, Optional from bson import ObjectId from pymongo.results import InsertOneResult -from classes.errors import ( +from classes.errors.wallet import ( WalletBalanceLimitExceeded, WalletNotFoundError, WalletOverdraftLimitExceeded, + WalletInsufficientFunds, ) -from classes.errors.wallet import WalletInsufficientFunds from modules.database import col_wallets logger: Logger = logging.getLogger(__name__) @@ -46,9 +46,17 @@ class Wallet: return cls(**db_entry) - def _to_dict(self) -> Dict[str, Any]: + def to_dict(self, json_compatible: bool = False) -> Dict[str, Any]: + """Convert Wallet object to a JSON representation. + + Args: + json_compatible (bool): Whether the JSON-incompatible objects like ObjectId need to be converted + + Returns: + Dict[str, Any]: JSON representation of Wallet + """ return { - "_id": self._id, + "_id": self._id if not json_compatible else str(self._id), "owner_id": self.owner_id, "guild_id": self.guild_id, "balance": self.balance, diff --git a/cli.py b/cli.py new file mode 100644 index 0000000..6b86c88 --- /dev/null +++ b/cli.py @@ -0,0 +1,27 @@ +import logging +from argparse import ArgumentParser +from logging import Logger + +from modules.migrator import migrate_database + +logger: Logger = logging.getLogger(__name__) + +parser = ArgumentParser( + prog="Javelina", + description="Discord bot for community management.", +) + +parser.add_argument("--migrate", action="store_true") +parser.add_argument("--only-api", action="store_true") + +args = parser.parse_args() + + +def main(): + if args.migrate: + logger.info("Performing migrations...") + migrate_database() + + +if __name__ == "__main__": + main() diff --git a/cogs/wallet.py b/cogs/wallet.py new file mode 100644 index 0000000..d9bff35 --- /dev/null +++ b/cogs/wallet.py @@ -0,0 +1,5 @@ +from classes.pycord_bot import PycordBot + + +def setup(client: PycordBot) -> None: + pass diff --git a/main.py b/main.py index 5f400b1..cda9d11 100644 --- a/main.py +++ b/main.py @@ -1,23 +1,31 @@ import asyncio +import contextlib import logging +from logging import Logger from os import getpid from libbot.utils import config_get +# Import required for uvicorn +from api.app import app # noqa from classes.pycord_bot import PycordBot from modules.extensions_loader import dynamic_import_from_src from modules.scheduler import scheduler -# Import required for uvicorn -from api.app import app - logging.basicConfig( level=logging.DEBUG if config_get("debug") else logging.INFO, format="%(name)s.%(funcName)s | %(levelname)s | %(message)s", datefmt="[%X]", ) -logger = logging.getLogger(__name__) +logger: Logger = logging.getLogger(__name__) + +# Try to import the module that improves performance +# and ignore errors when module is not installed +with contextlib.suppress(ImportError): + import uvloop + + uvloop.install() async def main(): @@ -26,7 +34,7 @@ async def main(): bot.load_extension("cogs") # Import API modules - dynamic_import_from_src("api.extensions", star_import=True) + dynamic_import_from_src("api/extensions", star_import=True) try: await bot.start(config_get("bot_token", "bot"))