diff --git a/bot/core/app.py b/bot/core/app.py index b92db82..59d1eaf 100644 --- a/bot/core/app.py +++ b/bot/core/app.py @@ -1,26 +1,24 @@ from __future__ import annotations import logging -import discord import os +from datetime import date +from logging.handlers import RotatingFileHandler +from pathlib import Path +import discord from discord.ext import commands -from discord import app_commands -from typing import Dict, List -from pathlib import Path -from logging.handlers import RotatingFileHandler -from datetime import date from dotenv import load_dotenv -from ..core.config import load_env, load_config -from ..core.loader import load_features from ..core.checks import set_staff_roles +from ..core.config import load_config, load_env +from ..core.loader import load_features -def setup_logging(level : str = "INFO") -> None: +def setup_logging(level: str = "INFO") -> None: logs_dir = Path(__file__).resolve().parent.parent.parent / "logs" logs_dir.mkdir(exist_ok=True) - + fmt = logging.Formatter("%(asctime)s | %(levelname)s | %(name)s | %(message)s") root = logging.getLogger() @@ -33,7 +31,7 @@ def setup_logging(level : str = "INFO") -> None: fh = RotatingFileHandler( logs_dir / f"bot_{date.today().isoformat()}.log", - maxBytes=5_000_000, + maxBytes=5_000_000, encoding="utf-8", ) fh.setFormatter(fmt) @@ -45,32 +43,34 @@ def __init__(self, guild_id: int) -> None: intents = discord.Intents.default() super().__init__(command_prefix="!", intents=intents) self.guild = discord.Object(id=guild_id) - + async def setup_hook(self) -> None: self.tree.clear_commands(guild=None) await self.tree.sync() - + loaded, failed = load_features(self.tree, self.config) logging.getLogger(__name__).info(f"Loaded features: {list(loaded.keys())}") if failed: logging.getLogger(__name__).warning(f"Failed to load features: {failed}") - - self.tree.copy_global_to(guild=self.guild) + + self.tree.copy_global_to(guild=self.guild) synced = await self.tree.sync(guild=self.guild) logging.getLogger(__name__).info("Synced %d commands to guild %s", len(synced), self.guild.id) + def main() -> None: setup_logging(level=os.getenv("LOG_LEVEL", "INFO")) load_dotenv() env = load_env() set_staff_roles(env.staff_roles_ids) - + config = load_config(env.config_path) - + bot = BotApp(guild_id=env.guild_id) bot.config = config - + bot.run(env.discord_token) + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/bot/core/config.py b/bot/core/config.py index 3a56015..e3caaea 100644 --- a/bot/core/config.py +++ b/bot/core/config.py @@ -1,14 +1,14 @@ from __future__ import annotations +import logging +import os from dataclasses import dataclass from pathlib import Path -from typing import Dict, List, Optional -from dotenv import load_dotenv +from typing import Dict, List -import os -import importlib -import logging import tomllib +from dotenv import load_dotenv + @dataclass(frozen=True) class AppEnv: @@ -16,36 +16,37 @@ class AppEnv: guild_id: int config_path: Path staff_roles_ids: List[int] - + def _project_root() -> Path: - return Path(__file__).resolve().parents[2] + return Path(__file__).resolve().parents[2] + def load_env() -> AppEnv: root = _project_root() log = logging.getLogger(__name__) app_env = os.getenv("APP_ENV", "dev").strip().lower() - + if app_env not in ("dev", "prod"): raise ValueError("APP_ENV environment variable must be 'dev' or 'prod'.") elif app_env == "prod": log.info("Running in production environment.") - + candidates = [ root / f".env.{app_env}", root / ".env", root / "config" / f".env.{app_env}", - root / "config" / ".env" + root / "config" / ".env", ] override = False - + for path in candidates: if path.exists(): log.info(f"Loaded environment variables from {path}") load_dotenv(dotenv_path=path, override=override) break load_dotenv() - + discord_token = os.getenv("DISCORD_TOKEN").strip() guild_id_str = os.getenv("GUILD_ID").strip() config_path_str = os.getenv("CONFIG_PATH", "config.toml").strip() @@ -71,9 +72,10 @@ def load_env() -> AppEnv: discord_token=discord_token, guild_id=guild_id, config_path=config_path, - staff_roles_ids=[int(role_id) for role_id in staff_roles_ids_str.split(",") if role_id] + staff_roles_ids=[int(role_id) for role_id in staff_roles_ids_str.split(",") if role_id], ) + def load_config(config_path: Path) -> Dict: log = logging.getLogger(__name__) if not config_path.exists(): @@ -85,12 +87,12 @@ def load_config(config_path: Path) -> Dict: if key not in data: log.error(f"Missing required configuration key: {key}") raise KeyError(f"Missing required configuration key: {key}") - + if not isinstance(data["enabled_features"], list): log.error("enabled_features must be a list.") raise TypeError("enabled_features must be a list.") if not isinstance(data["features"], dict): log.error("features must be a dictionary.") raise TypeError("features must be a dictionary.") - + return data diff --git a/features/test_checks/feature.py b/features/test_checks/feature.py index cdfa0e2..8d21dfd 100644 --- a/features/test_checks/feature.py +++ b/features/test_checks/feature.py @@ -1,13 +1,7 @@ import discord from discord import app_commands -from bot.core.checks import ( - is_staff, - is_server_admin, - is_server_owner, - is_server_mod, - has_permissions, - cooldown -) + +from bot.core.checks import cooldown, has_permissions, is_server_admin, is_server_mod, is_server_owner, is_staff FEATURE = { "slug": "test_checks", @@ -16,9 +10,10 @@ "version": "1.0.0", "author": "Thomas", "requires_config": False, - "permissions": ["send_messages"] + "permissions": ["send_messages"], } + def register(tree: app_commands.CommandTree, config): group = app_commands.Group(name="checktest", description="Test checks decorators") @@ -57,16 +52,22 @@ async def test_info(interaction: discord.Interaction): if not isinstance(interaction.user, discord.Member): await interaction.response.send_message("❌ Commande serveur uniquement", ephemeral=True) return - + perms = interaction.user.guild_permissions roles = [r.name for r in interaction.user.roles if r.name != "@everyone"] - + embed = discord.Embed(title="Tes informations", color=discord.Color.blurple()) embed.add_field(name="Admin", value="✅" if perms.administrator else "❌", inline=True) - embed.add_field(name="Propriétaire", value="✅" if interaction.user.id == interaction.guild.owner_id else "❌", inline=True) - embed.add_field(name="Mod", value="✅" if (perms.manage_messages or perms.kick_members or perms.ban_members) else "❌", inline=True) + embed.add_field( + name="Propriétaire", value="✅" if interaction.user.id == interaction.guild.owner_id else "❌", inline=True + ) + embed.add_field( + name="Mod", + value="✅" if (perms.manage_messages or perms.kick_members or perms.ban_members) else "❌", + inline=True, + ) embed.add_field(name="Rôles", value=", ".join(roles) if roles else "Aucun", inline=False) - + await interaction.response.send_message(embed=embed, ephemeral=True) tree.add_command(group) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c086812 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,11 @@ +[tool.black] +line-length = 120 +target-version = ['py310'] + +[tool.ruff] +line-length = 120 +select = ["E", "F", "I"] +ignore = ["E501"] + +[tool.ruff.per-file-ignores] +"__init__.py" = ["F401"] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 9b5cc2f..9a743c8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,4 @@ discord.py==2.6.4 python-dotenv==1.2.1 +black==26.1.0 +ruff==0.15.0 \ No newline at end of file