diff --git a/aw.py b/aw.py index 57197d8..7a210cd 100644 --- a/aw.py +++ b/aw.py @@ -5,6 +5,7 @@ from discord.ext import commands from dotenv import load_dotenv from bot.ai.handle_request import DiscourseSummarizer +from bot.log import logger from database import initialize_db GUILD_ID = 1110531063161299074 @@ -16,7 +17,7 @@ intents = discord.Intents.all() bot = commands.Bot(command_prefix="!", intents=intents) # Load environment variables from .env file (if it exists) -load_dotenv(override=True) +load_dotenv(os.path.join(os.path.dirname(__file__), ".env")) initialize_db() @@ -25,13 +26,13 @@ bot.ai_helper = DiscourseSummarizer() @bot.event async def on_ready(): - print(f"{bot.user.name} has connected to Discord!") + logger.info(f"{bot.user.name} has connected to Discord!") try: await bot.tree.sync(guild=discord.Object(id=GUILD_ID)) - print("Slash commands synchronized!") + logger.info("Slash commands synchronized!") except Exception as e: - print(f"Failed to sync commands: {e}") + logger.error("Failed to sync commands: %s", e) # Load extensions asynchronously await bot.load_extension("bot.tasks") @@ -39,4 +40,4 @@ async def on_ready(): await bot.load_extension("bot.commands") -bot.run(os.getenv("BOT_TOKEN")) +bot.run(os.getenv("BOT_TOKEN"), log_handler=None) diff --git a/bot/ai/handle_request.py b/bot/ai/handle_request.py index d83e117..ace6508 100644 --- a/bot/ai/handle_request.py +++ b/bot/ai/handle_request.py @@ -4,6 +4,8 @@ import requests from google import genai from google.genai import types +from bot.log import logger + API_KEY = os.getenv("GOOGLE_API_KEY") GENERIC_INSTRUCTION = "You are a Discord chatbot named 'AlterWare' who helps users with all kinds of topics across various subjects. You should limit your answers to fewer than 2000 characters." @@ -19,7 +21,7 @@ class DiscourseSummarizer: self.discourse_data = None if not API_KEY: - print("Google API key is not set. Please contact the administrator.") + logger.error("Google API key is not set. Please contact the administrator.") return self.client = genai.Client(api_key=API_KEY) @@ -50,7 +52,7 @@ class DiscourseSummarizer: ttl=self.ttl, ), ) - print(f"Cached content created: {self.cache.name}") + logger.info("Cached content created: %s", self.cache.name) def update_cache(self): """ @@ -64,7 +66,7 @@ class DiscourseSummarizer: self.client.caches.update( name=self.cache.name, config=types.UpdateCachedContentConfig(ttl="21600s") ) - print("Cache updated.") + logger.info("Cache updated.") def ask(self, prompt): """ diff --git a/bot/commands.py b/bot/commands.py index cf61b74..d0d3150 100644 --- a/bot/commands.py +++ b/bot/commands.py @@ -5,6 +5,7 @@ import discord from discord import app_commands from bot.config import message_patterns, update_patterns +from bot.log import logger from bot.utils import compile_stats, fetch_game_stats, perform_search from database import add_pattern, add_user_to_blacklist, is_user_blacklisted @@ -128,4 +129,4 @@ async def setup(bot): await bot.tree.sync(guild=discord.Object(id=GUILD_ID)) # Force sync - print("Commands extension loaded!") + logger.info("Commands extension loaded!") diff --git a/bot/config.py b/bot/config.py index f3bf9d8..00cfc81 100644 --- a/bot/config.py +++ b/bot/config.py @@ -1,6 +1,7 @@ import csv import os +from bot.log import logger from database import get_patterns message_patterns = get_patterns() @@ -9,7 +10,7 @@ message_patterns = get_patterns() def update_patterns(regex: str, response: str): """update patterns in memory.""" message_patterns.append({"regex": regex, "response": response}) - print(f"Pattern added in memory: {regex}") + logger.info(f"Pattern added in memory: {regex}") def load_chat_messages(csv_path="chat/chat_messages.csv"): @@ -24,7 +25,7 @@ def load_chat_messages(csv_path="chat/chat_messages.csv"): """ messages = [] if not os.path.exists(csv_path): - print(f"CSV file not found: {csv_path}") + logger.info(f"CSV file not found: {csv_path}") return messages with open(csv_path, newline="", encoding="utf-8") as csvfile: diff --git a/bot/discourse/handle_request.py b/bot/discourse/handle_request.py index 2fef2ff..116691b 100644 --- a/bot/discourse/handle_request.py +++ b/bot/discourse/handle_request.py @@ -3,6 +3,8 @@ import os import requests from bs4 import BeautifulSoup +from bot.log import logger + DISCOURSE_BASE_URL = os.getenv("DISCOURSE_BASE_URL") API_KEY = os.getenv("DISCOURSE_API_KEY") API_USERNAME = os.getenv("DISCOURSE_API_USERNAME") @@ -23,8 +25,11 @@ def get_topics_by_id(topic_id): response = requests.get(f"{DISCOURSE_BASE_URL}/t/{topic_id}.json", headers=headers) if response.status_code == 200: return response.json() + elif response.status_code == 403: + logger.error(f"Access forbidden for topic {topic_id}: {response.status_code}") + return None else: - print( + logger.error( f"Error fetching topic {topic_id}: {response.status_code} - {response.text}" ) return None @@ -56,8 +61,11 @@ def get_topics_by_tag(tag_name): for post in posts: cooked_strings.append(post.get("cooked", "")) return cooked_strings + elif response.status_code == 403: + logger.error(f"Access forbidden for topic {topic_id}: {response.status_code}") + return None else: - print( + logger.error( f"Error fetching topics with tag '{tag_name}': {response.status_code} - {response.text}" ) return [] diff --git a/bot/events.py b/bot/events.py index b8e552a..69ee01e 100644 --- a/bot/events.py +++ b/bot/events.py @@ -8,6 +8,7 @@ from bot.events_handlers import ( handle_reaction_add, handle_voice_state_update, ) +from bot.log import logger async def setup(bot): @@ -43,4 +44,4 @@ async def setup(bot): async def on_voice_state_update(member, before, after): await handle_voice_state_update(member, before, after, bot) - print("Events extension loaded!") + logger.info("Events extension loaded!") diff --git a/bot/events_handlers/message_events.py b/bot/events_handlers/message_events.py index 8c84e9d..916a0fe 100644 --- a/bot/events_handlers/message_events.py +++ b/bot/events_handlers/message_events.py @@ -4,6 +4,7 @@ from datetime import timedelta import discord from bot.ai.handle_request import forward_to_google_api +from bot.log import logger from bot.utils import aware_utcnow, timeout_member from database import add_user_to_role @@ -89,11 +90,15 @@ async def handle_bot_mention(message, bot, no_context=False): image_object = fetch_image_from_message(referenced_message) except discord.NotFound: - print("Referenced message not found.") + logger.error("Referenced message not found.") except discord.Forbidden: - print("Bot does not have permission to fetch the referenced message.") + logger.error( + "Bot does not have permission to fetch the referenced message." + ) except discord.HTTPException as e: - print(f"An error occurred while fetching the referenced message: {e}") + logger.error( + "An error occurred while fetching the referenced message: %s", e + ) # Pass the reply content to forward_to_google_api await forward_to_google_api(message, bot, image_object, reply_content, no_context) @@ -162,7 +167,7 @@ async def is_message_a_duplicate(message): time_difference = current_time - message_time if time_difference >= timedelta(minutes=5): - print( + logger.debug( f"Message is probably not a duplicate. Time difference: {time_difference}" ) continue @@ -174,9 +179,11 @@ async def is_message_a_duplicate(message): await timeout_member(member) return except discord.Forbidden: - print(f"Bot does not have permission to read messages in {channel.name}.") + logger.error( + f"Bot does not have permission to read messages in {channel.name}." + ) except discord.HTTPException as e: - print(f"An error occurred: {e}") + logger.error("An error occurred: %s", e) async def was_message_replied_by_bot(message, bot): diff --git a/bot/events_handlers/voice_events.py b/bot/events_handlers/voice_events.py index 83bfca9..8f36530 100644 --- a/bot/events_handlers/voice_events.py +++ b/bot/events_handlers/voice_events.py @@ -2,6 +2,8 @@ import asyncio import discord +from bot.log import logger + MP3_PATH = "sounds/hello.mp3" @@ -29,4 +31,4 @@ async def handle_voice_state_update(member, before, after, bot): await vc.disconnect() except Exception as e: - print(f"Error: {e}") + logger.error("Error: %s", e) diff --git a/bot/log.py b/bot/log.py new file mode 100644 index 0000000..670f3a6 --- /dev/null +++ b/bot/log.py @@ -0,0 +1,23 @@ +import logging +import logging.handlers +import os + +log_dir = "/bot-data" +os.makedirs(log_dir, exist_ok=True) + +logger = logging.getLogger("discord") +logger.setLevel(logging.INFO) +logging.getLogger("discord.http").setLevel(logging.INFO) + +handler = logging.handlers.RotatingFileHandler( + filename=os.path.join(log_dir, "discord.log"), + encoding="utf-8", + maxBytes=32 * 1024 * 1024, # 32 MiB + backupCount=5, # Rotate through 5 files +) +dt_fmt = "%Y-%m-%d %H:%M:%S" +formatter = logging.Formatter( + "[{asctime}] [{levelname:<8}] {name}: {message}", dt_fmt, style="{" +) +handler.setFormatter(formatter) +logger.addHandler(handler) diff --git a/bot/tasks.py b/bot/tasks.py index 332ba51..d871c5a 100644 --- a/bot/tasks.py +++ b/bot/tasks.py @@ -7,6 +7,7 @@ from discord.ext import commands, tasks from bot.config import schizo_messages from bot.discourse.handle_request import combine_posts_text, fetch_cooked_posts +from bot.log import logger from bot.utils import aware_utcnow, fetch_api_data from database import migrate_users_with_role @@ -53,7 +54,9 @@ class SteamSaleChecker(commands.Cog): channel = self.bot.get_channel(channel_id) if channel is None: - print(f"Error: Channel ID {channel_id} for {game_name} not found.") + logger.error( + f"Error: Channel ID {channel_id} for {game_name} not found." + ) return steam_api_url = ( @@ -65,7 +68,9 @@ class SteamSaleChecker(commands.Cog): data = response.json().get(str(app_id), {}).get("data", {}) if not data: - print(f"Warning: No data returned for {game_name}. Skipping...") + logger.warning( + f"Warning: No data returned for {game_name}. Skipping..." + ) return price_info = data.get("price_overview", {}) @@ -113,7 +118,7 @@ class SteamSaleChecker(commands.Cog): await channel.send(embed=embed) except requests.RequestException as e: - print(f"Error fetching Steam sale data for {game_name}: {e}") + logger.error("Error fetching Steam sale data for %s: %s", game_name, e) @check_steam_sale.before_loop async def before_check_steam_sale(self): @@ -134,16 +139,16 @@ class DiscourseUpdater(commands.Cog): Periodically fetches and updates Discourse data for the bot. """ tag_name = "docs" - print("Fetching Discourse data...") + logger.info("Fetching Discourse data...") cooked_posts = fetch_cooked_posts(tag_name) if cooked_posts: combined_text = combine_posts_text( [{"cooked": post} for post in cooked_posts] ) self.bot.ai_helper.set_discourse_data(combined_text) - print("Discourse data updated successfully.") + logger.info("Discourse data updated successfully.") else: - print(f"No posts found for tag '{tag_name}'.") + logger.warning(f"No posts found for tag '{tag_name}'.") @update_discourse_data.before_loop async def before_update_discourse_data(self): @@ -167,7 +172,7 @@ async def setup(bot): now = aware_utcnow() remaining_seconds = int((TARGET_DATE - now).total_seconds()) - print(f"Seconds until August 12, 2036, UTC: {remaining_seconds}") + logger.info(f"Seconds until August 12, 2036, UTC: {remaining_seconds}") channel = bot.get_channel(OFFTOPIC_CHANNEL) if channel: @@ -180,9 +185,11 @@ async def setup(bot): "The heat death of the universe has come and gone. We exist in a post-time void. Nothing remains but this message." ) else: - print("Debug: Channel not found. Check the OFFTOPIC_CHANNEL variable.") + logger.debug( + "Debug: Channel not found. Check the OFFTOPIC_CHANNEL variable." + ) except Exception as e: - print(f"An error occurred in heat_death task: {e}") + logger.error("An error occurred in heat_death task: %s", e) @tasks.loop(hours=5) async def shizo_message(): @@ -191,7 +198,7 @@ async def setup(bot): message = random.choice(schizo_messages) await channel.send(message) else: - print("Debug: Channel not found or schizo_messages is empty.") + logger.debug("Debug: Channel not found or schizo_messages is empty.") @tasks.loop(hours=24) async def share_dementia_image(): @@ -200,7 +207,9 @@ async def setup(bot): for _ in range(3): await channel.send(DEMENTIA_URL) else: - print("Debug: Channel not found. Check the OFFTOPIC_CHANNEL variable.") + logger.debug( + "Debug: Channel not found. Check the OFFTOPIC_CHANNEL variable." + ) await migrate_all_users(bot) @@ -212,4 +221,4 @@ async def setup(bot): await bot.add_cog(SteamSaleChecker(bot)) await bot.add_cog(DiscourseUpdater(bot)) - print("Tasks extension loaded!") + logger.info("Tasks extension loaded!") diff --git a/bot/utils.py b/bot/utils.py index 60d8935..a08c0b0 100644 --- a/bot/utils.py +++ b/bot/utils.py @@ -5,6 +5,8 @@ from datetime import datetime, timedelta, timezone import discord import requests +from bot.log import logger + def aware_utcnow(): return datetime.now(timezone.utc) @@ -73,24 +75,24 @@ async def timeout_member( reason: str = "Requested by the bot", ): if not member: - print("Debug: Member is None. Skipping timeout.") + logger.error("Member is None. Skipping timeout.") return try: # Debug: Print the member object and timeout duration - print(f"Debug: Attempting to timeout member {member} (ID: {member.id}).") - print(f"Debug: Timeout duration set to {duration}.") - print(f"Debug: Reason: {reason}") + logger.debug(f"Debug: Attempting to timeout member {member} (ID: {member.id}).") + logger.debug(f"Debug: Timeout duration set to {duration}.") + logger.debug(f"Debug: Reason: {reason}") await member.timeout(duration, reason=reason) - print(f"Debug: Successfully timed out {member}.") + logger.info(f"Successfully timed out {member}.") except discord.Forbidden: - print(f"Debug: Bot lacks permissions to timeout member {member}.") + logger.error(f"Bot lacks permissions to timeout member {member}.") except discord.HTTPException as e: - print(f"Debug: HTTPException occurred: {e}") + logger.error("HTTPException occurred: %s", e) except Exception as e: - print(f"Debug: Unexpected error occurred: {e}") + logger.error("Unexpected error occurred: %s", e) # Check if a username is valid diff --git a/database/__init__.py b/database/__init__.py index 60471ca..497905e 100644 --- a/database/__init__.py +++ b/database/__init__.py @@ -1,6 +1,7 @@ import os import sqlite3 +from bot.log import logger from bot.utils import aware_utcnow DB_DIR = os.getenv("BOT_DATA_DIR", "/bot-data") @@ -11,7 +12,7 @@ os.makedirs(DB_DIR, exist_ok=True) def initialize_db(): """Creates the database tables if they don't exist.""" - print(f"Opening database: {DB_PATH}") + logger.info("Opening database: %s", DB_PATH) conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() @@ -21,7 +22,7 @@ def initialize_db(): conn.commit() conn.close() - print(f"Done loading database: {DB_PATH}") + logger.info("Done loading database: %s", DB_PATH) def add_pattern(regex: str, response: str):