From c86ea54e8614bf243fab7622058b6e2b38237680 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Math=C3=A9o=20=27Nebulo9=27=20DESSAUVAGES?= Date: Sat, 12 Sep 2026 23:16:43 +0200 Subject: [PATCH] feat: Refactored all cogs implementation --- cogs/__init__.py | 42 ++++++++++ cogs/cs.py | 19 +++-- cogs/insult.py | 86 +++++++++++++++----- cogs/intern.py | 55 +++++++++++++ cogs/roles.py | 201 +++++++++++++++++++++++++++++------------------ cogs/roulette.py | 9 ++- main.py | 55 +++++++++---- setup/bot.py | 32 +++++++- setup/config.py | 58 +++++++++++++- setup/logger.py | 17 +++- 10 files changed, 451 insertions(+), 123 deletions(-) create mode 100644 cogs/__init__.py create mode 100644 cogs/intern.py diff --git a/cogs/__init__.py b/cogs/__init__.py new file mode 100644 index 0000000..29eaaea --- /dev/null +++ b/cogs/__init__.py @@ -0,0 +1,42 @@ +import discord, json, csv +from discord.ext import commands +from typing import Union, Any, Optional, Callable +from setup.config import Config + +class BaseCog(commands.Cog): + def __init__(self,bot:discord.Bot): + self.bot = bot + +class ConfigurableCog(BaseCog): + def __init__(self, bot: discord.Bot): + super().__init__(bot) + + +class FeatureCog(BaseCog): + """Base Cog that provides per-guild feature flag checks. + + Subclasses should set `feature_name` or pass it to the constructor. + This class reads the feature flags from `Config.GUILD_CONFIGS[guild_id]["features"]`. + """ + + def __init__(self, bot: discord.Bot, feature_name: Optional[str] = None): + super().__init__(bot) + self.feature_name = feature_name + + def is_enabled(self, guild_id: int) -> bool: + if not self.feature_name: + return False + guild_cfg = Config.GUILD_CONFIGS.get(guild_id) + if not guild_cfg: + return False + features = guild_cfg.get("features", {}) + return bool(features.get(self.feature_name, False)) + + def require_enabled_check(self) -> Callable[[commands.Context], bool]: + def predicate(ctx: commands.Context) -> bool: + if not ctx.guild: + return False + return self.is_enabled(ctx.guild.id) + + return commands.check(predicate) + diff --git a/cogs/cs.py b/cogs/cs.py index b936753..20153bd 100644 --- a/cogs/cs.py +++ b/cogs/cs.py @@ -7,6 +7,7 @@ from discord.ext import commands, tasks, bridge from setup.logger import LOGGER from setup.config import Config from models.cs_models import * +from cogs import FeatureCog BASE_URL = "https://www.hltv.org" @@ -24,12 +25,20 @@ OLD_MATCH_DURATION = 3600 * 24 * 2 EMOJI_CREATE_EVENT = "✅" # Create guild event for the match EMOJI_REMOVE_EVENT = "❌" # Remove guild event for the match -class CSCog(commands.Cog): - def __init__(self, bot:discord.Bot): - self.bot = bot - self.matches:list[Match] = [] +class CSCog(FeatureCog): + def __init__(self, bot: discord.Bot): + super().__init__(bot, feature_name="cs") + self.matches: list[Match] = [] self.message_match_map = {} # Maps message IDs to Match objects - self.get_matches.start() + + # Start scraping loop only if at least one guild enabled CS feature + try: + enabled_any = any(cfg.get("features", {}).get("cs") for cfg in Config.GUILD_CONFIGS.values()) + except Exception: + enabled_any = False + + if enabled_any: + self.get_matches.start() def is_up_to_date(self) -> bool: """Check if PAGE_FILE and MATCHES_FILE are up to date.""" diff --git a/cogs/insult.py b/cogs/insult.py index c73ec48..8e31ef4 100644 --- a/cogs/insult.py +++ b/cogs/insult.py @@ -1,32 +1,76 @@ -import discord, json, os, random, re -import collections +import discord, random, re from discord.ext import commands from setup.logger import LOGGER from setup.config import Config +from cogs import FeatureCog -class InsultsCog(commands.Cog): - def __init__(self, bot:discord.Bot): - self.bot = bot +class InsultsCog(FeatureCog): + def __init__(self, bot: discord.Bot): + super().__init__(bot, feature_name="insults") + LOGGER.debug("InsultsCog initialized") - with open(os.path.join(Config.CONFIG_PATH, "insults.txt")) as f: - self.insults = [l.strip() for l in f.readlines()] + def reload_men_list(self): + self.men: list[discord.Member] = [] + for guild in self.bot.guilds: + for role in guild.roles: + if role.name and re.match(r'^([hH]im|[hH]e).*$', role.name): + self.men.extend(role.members) + # dedupe by id + self.men = list({m.id: m for m in self.men}.values()) @commands.Cog.listener() - async def on_message(self, message:discord.Message): - if len(message.content.split()) > 3: - author = message.author - if any(author.id == u.id for u in self.men): - r = int(random.random()*100) - print(r) - if r < 5: - insult = random.choice(self.insults) + async def on_message(self, message: discord.Message): + if not message.guild or message.author.bot: + return + + # only run if feature enabled for this guild + if not self.is_enabled(message.guild.id): + return + + if len(message.content.split()) <= 3: + return + + author = message.author + if not hasattr(self, 'men') or not self.men: + self.reload_men_list() + + if any(author.id == u.id for u in self.men): + guild_cfg = Config.GUILD_CONFIGS.get(message.guild.id, {}) + insults_cfg = guild_cfg.get("configs", {}).get("insults", {}) + safeguard_patterns = insults_cfg.get("safeguard_patterns", []) + safeguard_match = any(re.match(pattern, message.content) for pattern in safeguard_patterns) + if safeguard_match: + return + + r = random.random() * 100 + exceptions = insults_cfg.get("exceptions", []) + filtered = [ex for ex in exceptions if ex.get("user_id") == author.id] + if filtered: + try: + r *= float(filtered[0].get("factor", 1)) + except Exception: + pass + if r < 5: + insults = insults_cfg.get("insults", []) + if insults: + insult = random.choice(insults) await message.reply(insult) @commands.Cog.listener() async def on_ready(self): - self.men:list[discord.Member] = [] - for guild in self.bot.guilds: - for role in guild.roles: - if re.match(r'^([hH]im|[hH]e).*$',role.name): - self.men.extend(role.members) - self.men = list(set(self.men)) \ No newline at end of file + self.reload_men_list() + + @commands.Cog.listener() + async def on_member_update(self, before: discord.Member, after: discord.Member): + before_roles = set(before.roles) + after_roles = set(after.roles) + removed = before_roles.difference(after_roles) + added = after_roles.difference(before_roles) + role = None + if removed: + role = list(removed).pop() + elif added: + role = list(added).pop() + + if role and re.match(r'^([hH]im|[hH]e).*$', role.name): + self.reload_men_list() \ No newline at end of file diff --git a/cogs/intern.py b/cogs/intern.py new file mode 100644 index 0000000..0d6fd4f --- /dev/null +++ b/cogs/intern.py @@ -0,0 +1,55 @@ +import random +from discord.ext import commands + + +class InternCog(commands.Cog): + def __init__(self, bot=None): + self.bot = bot + # `data` expected to be a list of dicts with 'guild_id' and 'mentor_id' + self.data = [] + self.connected_guild_ids = set() + self.in_voice_channel = False + + async def on_voice_state_update(self, member, before, after): + # determine guild context + guild = None + if after and getattr(after, 'channel', None): + guild = after.channel.guild + elif before and getattr(before, 'channel', None): + guild = before.channel.guild + if not guild: + return + + # find matching mentor entry for this guild + entry = None + for e in getattr(self, 'data', []): + if e.get('guild_id') == getattr(guild, 'id', None) and e.get('mentor_id') == getattr(member, 'id', None): + entry = e + break + if not entry: + return + + # find existing voice client for this guild (if any) + voice_client = None + for vc in getattr(self.bot, 'voice_clients', []): + if getattr(vc, 'guild', None) and getattr(vc.guild, 'id', None) == getattr(guild, 'id', None): + voice_client = vc + break + + # mentor joined a channel (initial join) + if (not before or not getattr(before, 'channel', None)) and after and getattr(after, 'channel', None): + # if bot is not connected, connect with 25% chance + if not voice_client: + if random.random() < 0.25: + await after.channel.connect() + return + + # mentor moved channels while bot is connected -> move bot + if voice_client and after and getattr(after, 'channel', None): + await voice_client.move_to(after.channel) + return + + # mentor left channel -> disconnect bot if connected + if voice_client and (not after or not getattr(after, 'channel', None)): + await voice_client.disconnect(force=True) + return diff --git a/cogs/roles.py b/cogs/roles.py index 7a5ab13..6fe445b 100644 --- a/cogs/roles.py +++ b/cogs/roles.py @@ -1,95 +1,146 @@ -import discord, os, json +import discord +from cogs import FeatureCog from discord import option from discord.ext import commands, bridge from setup.config import Config from setup.logger import LOGGER -CONFIG_FILE = os.path.join(Config.CONFIG_PATH,"roles.json") -class RolesCog(commands.Cog): - def __init__(self,bot:discord.Bot): - self.bot = bot - self.load_config() - - def load_config(self): - with open(CONFIG_FILE) as file: - self.config = json.load(file) - - def save_config(self): - with open(CONFIG_FILE,"w") as file: - json.dump(self.config,file) +class RolesCog(FeatureCog): + def __init__(self, bot: discord.Bot): + super().__init__(bot, feature_name="roles") @bridge.bridge_command() - async def roles(self,ctx: discord.ApplicationContext, category:bridge.BridgeOption(name="category",description="The category to manipulate", input_type=str,required=True)): - filtered = [config_category for config_category in self.config if config_category["category_id"] == category] - if len(filtered): - await ctx.defer() - config_category = filtered.pop() - embed = discord.Embed( - title=config_category["category_name"], - description=config_category["message_header"], - color=discord.Color.blue() - ) - roles = [ - self.bot.get_guild(897157935006900254).get_role(role_config["role_id"]) - for role_config in config_category["roles"] - ] - for role_instance,role_config in zip(roles,config_category["roles"]): - embed.add_field(name=role_config["reaction"],value=role_instance.mention) - message = await ctx.send(embed=embed) - if message: - config_category["message_id"] = message.id - for config_role in config_category["roles"]: - await message.add_reaction(config_role["reaction"]) - self.save_config() - await ctx.followup.send("Role message generated",ephemeral=True) + async def roles(self, ctx: discord.ApplicationContext, category: bridge.BridgeOption(name="category", description="The category to manipulate", input_type=str, required=True)): + if not ctx.guild: + await ctx.respond("This command must be used in a guild.", ephemeral=True) + return + + guild_id = ctx.guild.id + if not self.is_enabled(guild_id): + await ctx.respond("Roles feature is disabled for this guild.", ephemeral=True) + return + + guild_cfg = Config.GUILD_CONFIGS.get(guild_id) + if not guild_cfg: + await ctx.respond("No configuration for this guild.", ephemeral=True) + return + + categories = guild_cfg.get("configs", {}).get("roles", {}).get("categories", []) + filtered = [c for c in categories if c.get("category_id") == category] + if not filtered: + await ctx.respond("Category not found.", ephemeral=True) + return + + await ctx.defer() + config_category = filtered.pop() + embed = discord.Embed( + title=config_category.get("category_name", ""), + description=config_category.get("message_header", ""), + color=discord.Color.blue() + ) + + roles_list = [ + ctx.guild.get_role(role_config.get("role_id")) + for role_config in config_category.get("roles", []) + ] + for role_instance, role_config in zip(roles_list, config_category.get("roles", [])): + mention = role_instance.mention if role_instance else "(role missing)" + embed.add_field(name=role_config.get("reaction", ""), value=mention) + + message = await ctx.send(embed=embed) + if message: + config_category["message_id"] = message.id + # persist the updated guild config + Config.save_guild_config(guild_id) + + for config_role in config_category.get("roles", []): + reaction = config_role.get("reaction") + if reaction: + await message.add_reaction(reaction) + + await ctx.followup.send("Role message generated", ephemeral=True) @commands.Cog.listener() - async def on_raw_reaction_add(self, payload:discord.RawReactionActionEvent): + async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent): if payload.user_id == self.bot.user.id: return - message_id = payload.message_id - filtered = [config_category for config_category in self.config if config_category["message_id"] == message_id] - if len(filtered): - config_category = filtered.pop() - role_ids = [config_role["role_id"] for config_role in config_category["roles"] if config_role["reaction"] == payload.emoji.name] - if len(role_ids): - role_id = role_ids.pop() + guild_id = payload.guild_id + guild_cfg = Config.GUILD_CONFIGS.get(guild_id) + if not guild_cfg: + return + if not self.is_enabled(guild_id): + return - guild = self.bot.get_guild(payload.guild_id) - if guild: - role = guild.get_role(role_id) - if role: - user = guild.get_member(payload.user_id) - if user: - if "parent_role_id" in config_category.keys(): - parent_role = guild.get_role(config_category["parent_role_id"]) - await user.add_roles(parent_role,role) - else: - await user.add_roles(role) + categories = guild_cfg.get("configs", {}).get("roles", {}).get("categories", []) + filtered = [c for c in categories if c.get("message_id") == payload.message_id] + if not filtered: + return + + config_category = filtered.pop() + role_ids = [config_role.get("role_id") for config_role in config_category.get("roles", []) if config_role.get("reaction") == payload.emoji.name] + if not role_ids: + return + + role_id = role_ids.pop() + guild = self.bot.get_guild(guild_id) + if not guild: + return + role = guild.get_role(role_id) + if not role: + return + + user = guild.get_member(payload.user_id) + if not user: + return + + if "parent_role_id" in config_category: + parent_role = guild.get_role(config_category.get("parent_role_id")) + if parent_role: + await user.add_roles(parent_role, role) + return + + await user.add_roles(role) @commands.Cog.listener() - async def on_raw_reaction_remove(self, payload:discord.RawReactionActionEvent): + async def on_raw_reaction_remove(self, payload: discord.RawReactionActionEvent): if payload.user_id == self.bot.user.id: return - message_id = payload.message_id - filtered = [config_category for config_category in self.config if config_category["message_id"] == message_id] - if len(filtered): - config_category = filtered.pop() - role_ids = [config_role["role_id"] for config_role in config_category["roles"] if config_role["reaction"] == payload.emoji.name] - if len(role_ids): - role_id = role_ids.pop() + guild_id = payload.guild_id + guild_cfg = Config.GUILD_CONFIGS.get(guild_id) + if not guild_cfg: + return + if not self.is_enabled(guild_id): + return - guild = self.bot.get_guild(payload.guild_id) - if guild: - role = guild.get_role(role_id) - if role: - user = guild.get_member(payload.user_id) - if user: - if "parent_role_id" in config_category.keys(): - parent_role = guild.get_role(config_category["parent_role_id"]) - await user.remove_roles(parent_role,role) - else: - await user.remove_roles(role) \ No newline at end of file + categories = guild_cfg.get("configs", {}).get("roles", {}).get("categories", []) + filtered = [c for c in categories if c.get("message_id") == payload.message_id] + if not filtered: + return + + config_category = filtered.pop() + role_ids = [config_role.get("role_id") for config_role in config_category.get("roles", []) if config_role.get("reaction") == payload.emoji.name] + if not role_ids: + return + + role_id = role_ids.pop() + guild = self.bot.get_guild(guild_id) + if not guild: + return + role = guild.get_role(role_id) + if not role: + return + + user = guild.get_member(payload.user_id) + if not user: + return + + if "parent_role_id" in config_category: + parent_role = guild.get_role(config_category.get("parent_role_id")) + if parent_role: + await user.remove_roles(parent_role, role) + return + + await user.remove_roles(role) \ No newline at end of file diff --git a/cogs/roulette.py b/cogs/roulette.py index 6c4c2ca..d5e42e3 100644 --- a/cogs/roulette.py +++ b/cogs/roulette.py @@ -5,11 +5,12 @@ import random import discord from discord import option from discord.ext import commands +from cogs import FeatureCog -class RouletteCog(commands.Cog): +class RouletteCog(FeatureCog): def __init__(self, bot: discord.Bot): - self.bot = bot + super().__init__(bot, feature_name="roulette") self.cooldown = 60 * 3 self.active_games = {} @@ -155,6 +156,10 @@ class RouletteCog(commands.Cog): ctx: discord.ApplicationContext, target: discord.Member, ): + # ensure feature enabled for this guild + if not ctx.guild or not self.is_enabled(ctx.guild.id): + await ctx.respond("Roulette feature is disabled for this guild.", ephemeral=True) + return author = ctx.author self_test = author.id == target.id diff --git a/main.py b/main.py index 8d0f5b9..3af8d71 100644 --- a/main.py +++ b/main.py @@ -1,29 +1,54 @@ -import os, discord, json, sys, random +import os +import sys +import discord from argparse import ArgumentParser -from datetime import datetime -from asyncio import create_task -from discord import option -from discord.ext import tasks, commands + +# Parse minimal CLI args early so we can configure logging before imports that +# initialize loggers. Keep only `--log-level`; other args were unused. +PARSER = ArgumentParser(description='BotMafieux for Discord.') +PARSER.add_argument('--log-level', type=str, help='Logging level (DEBUG, INFO, WARNING, ERROR)') +EXEC_ARGS = PARSER.parse_args() + +if EXEC_ARGS.log_level: + os.environ['LOG_LEVEL'] = str(EXEC_ARGS.log_level) + + from setup.bot import bot, reload_feature, register_cogs from setup.logger import LOGGER from setup.config import Config - -PARSER = ArgumentParser(description='BotMafieux for Discord.') -PARSER.add_argument('--guild_id',type=int,help='The guild ID.') -PARSER.add_argument('--channel_id',type=int,help='The channel ID.') -PARSER.add_argument('-r','--reload',action='store_true',help='Reload the bot.') -PARSER.add_argument('--cogs',nargs='+',default=None,help='Select which cogs to load (for example: --cogs cs insult).') -EXEC_ARGS = PARSER.parse_args() -selected_cogs = EXEC_ARGS.cogs if EXEC_ARGS.cogs is not None else Config.get_cogs() -register_cogs(selected_cogs) +# Register cogs from configuration (no CLI activation) +register_cogs() @bot.event async def on_ready(): if bot.user: LOGGER.info(f'{bot.user.name} has connected to Discord!') - LOGGER.debug(f"Guilds: {','.join([guild.name for guild in bot.guilds])}") + LOGGER.info(f"Guilds: {','.join([guild.name for guild in bot.guilds])}") + # Log configuration for each guild we are connected to + try: + for guild in bot.guilds: + cfg = Config.GUILD_CONFIGS.get(guild.id) + if cfg is not None: + LOGGER.info(f"Guild config for {guild.name} ({guild.id}): {cfg}") + else: + LOGGER.info(f"No guild config for {guild.name} ({guild.id})") + except Exception as e: + LOGGER.warning(f"Failed to log guild configs: {e}") + @bot.event + async def on_guild_join(guild: discord.Guild): + # Refresh configs and log the joined guild configuration + try: + Config.load_guilds_configs() + except Exception: + LOGGER.debug('Could not reload guild configs during on_guild_join.') + + cfg = Config.GUILD_CONFIGS.get(guild.id) + if cfg is not None: + LOGGER.info(f"Joined guild {guild.name} ({guild.id}) with config: {cfg}") + else: + LOGGER.info(f"Joined guild {guild.name} ({guild.id}) with no configuration present.") if __name__ == '__main__': token = Config.TOKEN diff --git a/setup/bot.py b/setup/bot.py index bb0612d..b7ae522 100644 --- a/setup/bot.py +++ b/setup/bot.py @@ -5,7 +5,9 @@ from discord.ext import bridge from cogs.cs import CSCog from cogs.insult import InsultsCog from cogs.roulette import RouletteCog +from cogs.roles import RolesCog from setup.logger import LOGGER +from setup.config import Config bot_intents = discord.Intents.default() bot_intents.message_content = True @@ -19,9 +21,26 @@ bot = bridge.Bot(command_prefix='$', intents=bot_intents) COG_REGISTRY = { 'cs': CSCog, 'insult': InsultsCog, - 'roulette': RouletteCog + 'roulette': RouletteCog, + 'roles': RolesCog, } +# Register a global admin slash command directly on the bot to avoid circular imports. +from discord import option as discord_option + + +@bot.slash_command(name="reload", description="Reload a feature (cog) by name, or 'all')") +@discord_option(name="feature", description="Feature to reload (e.g. cs, insult, roles, roulette, all)", input_type=str, required=True) +async def reload(ctx: discord.ApplicationContext, feature: str): + # Only allow administrators + if not ctx.author or not getattr(ctx.author, 'guild_permissions', None) or not ctx.author.guild_permissions.administrator: + await ctx.respond("You must be a server administrator to use this command.", ephemeral=True) + return + + await ctx.defer() + reload_feature(feature) + await ctx.followup.send(f"Reload requested for feature: {feature}", ephemeral=True) + def _normalize_cog_names(cogs=None): if cogs is None: @@ -46,6 +65,12 @@ def _normalize_cog_names(cogs=None): def register_cogs(cogs=None): + # Ensure guild configs are loaded before instantiating cogs + try: + Config.load_guilds_configs() + except Exception: + LOGGER.debug('Could not reload guild configs during register_cogs.') + selected_cogs = _normalize_cog_names(cogs) existing_cog_names = set(COG_REGISTRY.values()) @@ -75,6 +100,11 @@ def reload_feature(feature: str): return LOGGER.debug(f'Reloading {feature}...') + # Refresh guild configs before reloading a single feature + try: + Config.load_guilds_configs() + except Exception: + LOGGER.debug('Could not reload guild configs during reload_feature.') target_cog_name = COG_REGISTRY[normalized_feature].__name__ for existing_name in list(bot.cogs.keys()): if existing_name == target_cog_name: diff --git a/setup/config.py b/setup/config.py index 2678ff2..a8ba049 100644 --- a/setup/config.py +++ b/setup/config.py @@ -1,6 +1,6 @@ -import os +import os, pathlib, json from dotenv import load_dotenv - +from setup.logger import LOGGER class Config: load_dotenv() @@ -9,6 +9,10 @@ class Config: CONFIG_PATH = str(os.getenv("CONFIG_PATH")) COGS = os.getenv("COGS", "") + GUILDS_IDS:list[int] = [] + + GUILD_CONFIGS:dict[int,dict] = {} + @classmethod def get_cogs(cls): raw_value = cls.COGS.strip() @@ -16,3 +20,53 @@ class Config: return None return [item.strip().lower() for item in raw_value.split(",") if item.strip()] + @classmethod + def load_guilds_configs(cls): + with os.scandir(cls.CONFIG_PATH) as d: + guilds_config_files = [f.path for f in d if f.is_file() and f.name.endswith(".json")] + for guild_config_file in guilds_config_files: + with open(guild_config_file) as f: + guild_config = json.load(f) + guild_id = guild_config["guild_id"] + cls.GUILDS_IDS.append(guild_id) + cls.GUILD_CONFIGS[guild_id] = {k:v for k,v in guild_config.items() if k != "guild_id"} + cls.GUILD_CONFIGS[guild_id]["config_path"] = guild_config_file + + @classmethod + def load_guild_config(cls,guild_id:int): + config_path = cls.GUILD_CONFIGS[guild_id]["config_path"] + with open(config_path) as f: + guild_config = json.load(f) + # Preserve config_path when re-loading + cls.GUILD_CONFIGS[guild_id] = {k: v for k, v in guild_config.items() if k != "guild_id"} + cls.GUILD_CONFIGS[guild_id]["config_path"] = config_path + + @classmethod + def save_guilds_configs(cls): + for guild_id, guild_config in cls.GUILD_CONFIGS.items(): + config_path = guild_config.get("config_path") + if not config_path: + continue + with open(config_path, 'w') as file: + json_config = {k: v for k, v in guild_config.items() if k != "config_path"} + json_config["guild_id"] = guild_id + json.dump(json_config, file, ensure_ascii=False, indent=2) + + @classmethod + def save_guild_config(cls,guild_id:int): + config = cls.GUILD_CONFIGS.get(guild_id) + if not config: + return + config_path = config.get("config_path") + if not config_path: + return + with open(config_path, 'w') as file: + json_config = {k: v for k, v in config.items() if k != "config_path"} + json_config["guild_id"] = guild_id + json.dump(json_config, file, ensure_ascii=False, indent=2) + + @classmethod + def get_guild_config(cls,guild_id): + return cls.GUILD_CONFIGS[guild_id] + +Config.load_guilds_configs() \ No newline at end of file diff --git a/setup/logger.py b/setup/logger.py index b5bc5cb..3abf1f3 100644 --- a/setup/logger.py +++ b/setup/logger.py @@ -1,8 +1,21 @@ import logging +import os -logging.basicConfig(level=logging.INFO, format='[%(levelname)s] %(asctime)s: %(name)s: %(message)s') +# Determine log level from environment variable `LOG_LEVEL` (e.g. DEBUG, INFO) +raw_level = os.getenv("LOG_LEVEL", "INFO").upper() +level = logging.INFO +if raw_level.isdigit(): + try: + level = int(raw_level) + except Exception: + level = logging.INFO +else: + level = getattr(logging, raw_level, logging.INFO) + +logging.basicConfig(level=level, format='[%(levelname)s] %(asctime)s: %(name)s: %(message)s') LOGGER = logging.getLogger('botmafieux') -LOGGER.setLevel(logging.DEBUG) +LOGGER.setLevel(level) +# File handler (append) handler = logging.FileHandler(filename='botmafieux.log', encoding='utf-8', mode='a') handler.setFormatter(logging.Formatter('%(asctime)s:%(levelname)s:%(name)s: %(message)s')) LOGGER.addHandler(handler) \ No newline at end of file -- 2.49.1