Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 19 additions & 19 deletions bot/core/app.py
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -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)
Expand All @@ -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()
main()
32 changes: 17 additions & 15 deletions bot/core/config.py
Original file line number Diff line number Diff line change
@@ -1,51 +1,52 @@
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:
discord_token: str
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()
Expand All @@ -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():
Expand All @@ -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
29 changes: 15 additions & 14 deletions features/test_checks/feature.py
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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")

Expand Down Expand Up @@ -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)
11 changes: 11 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"]
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
discord.py==2.6.4
python-dotenv==1.2.1
black==26.1.0
ruff==0.15.0