Rewrote main.py, Updated LICENSE

- Pushed bot class into its own file
- Now uses an .env file
- Listens for SIGINT and SIGTERM signals and acts accordingly
This commit is contained in:
2025-08-12 17:56:52 +02:00
parent ede0060c86
commit 8c9368865a
5 changed files with 135 additions and 127 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2024 Lars Winzer Copyright (c) 2024-2025 Lars Winzer
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
+1 -3
View File
@@ -1,5 +1,3 @@
discord.py==2.4.0 discord.py==2.4.0
asyncpraw==7.8.1
colorama==0.4.6 colorama==0.4.6
aiohttp==3.11.11 python-dotenv==1.1.1
pillow==11.1.0
+26
View File
@@ -0,0 +1,26 @@
from discord.ext import commands
import discord
from utils.datetime_tools import get_elapsed_time_small, get_elapsed_time_milliseconds
from logging import Logger
from datetime import datetime
class PostIt_Bot(commands.Bot):
"""Class to extend the built-in """
def __init__(self, startup_time:float, program_version:str, app_logger:Logger):
intents = discord.Intents.default()
intents.messages = True
super().__init__(command_prefix=None, help_command=None, intents=intents)
self.startup_time = startup_time
self.PROGRAM_VERSION = program_version
self.__logger = app_logger
async def setup_hook(self):
# # Register cogs to handle commands
# for cog_name in ["debug", "post"]:
# await self.load_extension(f"cogs.{cog_name}")
# await self.tree.sync()
pass
async def on_ready(self):
self.__logger.info(f"Successfully logged in (after {get_elapsed_time_small(datetime.now().timestamp() - self.startup_time)}) as {self.user}")
+106 -122
View File
@@ -1,141 +1,125 @@
print(" ____ ____ ___________ __________")
print(" / __ \/ __ \/ ___/_ __/ / _/_ __/")
print(" / /_/ / / / /\__ \ / /_____ / / / / ")
print(" / ____/ /_/ /___/ // /_____// / / / ")
print(" /_/ \____//____//_/ /___/ /_/ ")
print(" Copyright (c) 2024 Lars Winzer")
print()
print(" Source: https://github.com/official-Cromatin/Post-It")
print(" Report an Issue: https://github.com/official-Cromatin/Post-It/issues/new?assignees=&labels=bug&projects=&template=issue_report.yml")
print("\n")
from datetime import datetime
startup = datetime.now().timestamp()
# Initialize the logger
from utils.logger.custom_logging import Custom_Logger
Custom_Logger.initialize()
import logging
app_logger = logging.getLogger("app")
startup_logger = logging.getLogger("app.startup")
from utils.adv_configparser import Advanced_ConfigParser
from utils.datetime_tools import get_elapsed_time_smal, get_elapsed_time_big, get_elapsed_time_milliseconds
import discord
from discord.ext import commands
from pathlib import Path
import re
import sys
import traceback
from utils.portal import Portal
import asyncio import asyncio
from typing import Union from datetime import datetime
from platforms.reddit import Reddit_Adapter from utils.logger.custom_logging import Custom_Logger
import logging
from pathlib import Path
import sys, signal
import os
import dotenv
from bot import PostIt_Bot
from utils.datetime_tools import get_elapsed_time_milliseconds, get_elapsed_time_big
import discord
source_path = Path(__file__).resolve() async def main(event_loop:asyncio.AbstractEventLoop, close_event:asyncio.Event):
base_path = source_path.parents[1] PROGRAM_VERSION = "1.0"
app_logger.info(f"Using the following path as entrypoint: '{base_path}'") print(" ____ ____ ___________ __________")
print(" / __ \/ __ \/ ___/_ __/ / _/_ __/")
print(" / /_/ / / / /\__ \ / /_____ / / / / ")
print(" / ____/ /_/ /___/ // /_____// / / / ")
print(f" /_/ \____//____//_/ /___/ /_/ v{PROGRAM_VERSION}")
print(" Copyright (c) 2024-2025 Lars Winzer")
print()
print(" Source: https://github.com/official-Cromatin/Post-It")
print(" Report an Issue: https://github.com/official-Cromatin/Post-It/issues/new?assignees=&labels=bug&projects=&template=issue_report.yml")
print("\n")
intents = discord.Intents.default() # Get timestamp for begin of actual execution
intents.messages = True startup_time = datetime.now().timestamp()
class MyBot(commands.Bot): # Initialize the logger
def __init__(self): Custom_Logger.initialize()
super().__init__(command_prefix=None, help_command=None, intents=intents) app_logger = logging.getLogger("app")
self.__portal:Portal app_startup_logger = logging.getLogger("app.startup")
self.__first_on_ready = False app_startup_logger.info(f"Starting Post-It v{PROGRAM_VERSION} ...")
def set_portal(self, portal:Portal): # Detect entrypoint
self.__portal = portal source_path = Path(__file__).resolve()
base_path = source_path.parents[1]
app_startup_logger.info(f"Using the following path as entrypoint: '{base_path}'")
async def setup_hook(self): # Detect operating system and attach
# Register cogs to handle commands shutdown_event = asyncio.Event()
for cog_name in ["debug", "post"]: def signal_handler(*args):
await self.load_extension(f"cogs.{cog_name}") app_logger.info("Shutdown signal recieved, shutting down")
await self.tree.sync() event_loop.call_soon_threadsafe(shutdown_event.set)
close_event.set()
async def on_app_command_completion(self, interaction: discord.Interaction, command: Union[discord.app_commands.Command, discord.app_commands.ContextMenu]): match sys.platform:
"""Called when a `app_commands.Command` or `app_commands.ContextMenu` has successfully completed without error""" case "linux":
self.__portal.no_succeeded_commands += 1 app_startup_logger.info("Detected platform: Linux")
print("Command succeeded") event_loop.add_signal_handler(signal.SIGINT, signal_handler)
event_loop.add_signal_handler(signal.SIGTERM, signal_handler)
async def on_interaction(self, interaction: discord.Interaction): case "darwin:":
"""Called when an interaction happened""" app_startup_logger.info("Detected platform: MacOS (Darwin)")
match interaction.type.name: event_loop.add_signal_handler(signal.SIGINT, signal_handler)
case discord.InteractionType.application_command.name: event_loop.add_signal_handler(signal.SIGTERM, signal_handler)
print("Interaction with bot", interaction.command.name)
self.__portal.no_executed_commands += 1
case discord.InteractionType.ping.name:
print("App got pinged by discord")
case discord.InteractionType.autocomplete.name:
print("Interaction with autocomplete")
case discord.InteractionType.modal_submit.name:
print("Modal interaction submitted")
case discord.InteractionType.component.name:
print("Component interaction")
async def on_connect(self): case "win32":
"""A coroutine to be called to setup the bot, after the bot is logged in but before it has connected to the Websocket""" app_startup_logger.info("Detected platform: Windows (Win32)")
if not self.__first_on_ready: signal.signal(signal.SIGINT, signal_handler)
startup_logger.info(f"Beginning startup routine ...") signal.signal(signal.SIGTERM, signal_handler)
routine_begin = datetime.now().timestamp()
await self.change_presence(status = discord.Status.dnd, activity = discord.CustomActivity("Executing pre startup routine")) case _:
app_startup_logger.fatal(f"Detected unsupported platform: {sys.platform}")
quit(1)
# Create the adapters for the platforms # Load environment variables
task_start = datetime.now().timestamp() if (os.getenv("skip_dotenv", False)):
startup_logger.debug(f"Loading platforms config ...") app_startup_logger.warning("Skipped import of dotenv file")
platforms_config = Advanced_ConfigParser(Path.joinpath(base_path, "config", "platforms.ini")) else:
portal.platforms_config = platforms_config dotenv.load_dotenv(base_path / ".env")
startup_logger.info(f"Loaded platforms config after {get_elapsed_time_milliseconds(datetime.now().timestamp() - task_start)}") app_startup_logger.warning("Imported dotenv file")
# Create platforms adapter # Check if required environment variables are present
task_start = datetime.now().timestamp() check_ok = True
startup_logger.debug(f"Creating reddit adapter ...") required_variables = [
portal.reddit_adapter = Reddit_Adapter(platforms_config["REDDIT"]["CLIENT_ID"], platforms_config["REDDIT"]["CLIENT_SECRET"]) "DISCORD_TOKEN",
startup_logger.info(f"Created reddit adapter after {get_elapsed_time_milliseconds(datetime.now().timestamp() - task_start)}") ]
await self.change_presence(status = discord.Status.online, activity = None) for variable_name in required_variables:
startup_logger.info(f"Startup routine finished after {get_elapsed_time_milliseconds(datetime.now().timestamp() - routine_begin)}") if os.getenv(variable_name) is None:
self.__first_on_ready = True app_startup_logger.error(f"Environment variable '{variable_name}' is missing")
else: check_ok = False
startup_logger.info("Startup routine allready executed, omitting this execution")
async def on_ready(self): if not check_ok:
app_logger.info(f"Successfully logged in (after {get_elapsed_time_smal(datetime.now().timestamp() - startup)}) as {self.user}") app_startup_logger.critical("Multiple required variables are missing. Aborting startup")
quit(1)
bot = MyBot() # Create bot instance and start bot
bot_config = Advanced_ConfigParser(Path.joinpath(base_path, "config", "bot.ini")) app_startup_logger.info(f"Preperations complete after {get_elapsed_time_milliseconds(datetime.now().timestamp() - startup_time)}, launching bot")
if re.match(r'[A-Za-z\d]{24}\.[\w-]{6}\.[\w-]{27}', bot_config["DISCORD"]["TOKEN"]): try:
app_logger.critical("Bot (config/bot.ini) configuration invalid, please set a valid token") bot_instance = PostIt_Bot(startup_time, PROGRAM_VERSION, app_logger)
quit(1)
elif bot_config.compare_to_template() not in ("equal", "config_minus"):
app_logger.critical("Bot (config/bot.ini) configuration is missing some parts. Make sure it at least has all the same keys as the template")
quit(1)
else:
app_logger.info("Bot configuration valid, continuing with startup")
# Execute some housekeeping actions bot_task = event_loop.create_task(bot_instance.start(os.getenv("DISCORD_TOKEN")))
portal = Portal.instance() shutdown_task = event_loop.create_task(shutdown_event.wait())
portal.bot_config = bot_config
portal.STARTUP_TIMESTAMP = startup
bot.set_portal(portal)
# Setup handlers to handle states of command execution # Wait if either the bot disconnects or the shutdown event is detected
@bot.tree.error finished_task, _ = await asyncio.wait(
async def on_app_command_error(ctx:discord.Interaction, error): [shutdown_task, bot_task],
"""Executed when exception during command execution occurs""" return_when = asyncio.FIRST_COMPLETED
print('Ignoring exception in command {}:'.format(ctx.command), file=sys.stderr) )
traceback.print_exception(type(error), error, error.__traceback__)
portal.no_failed_commands += 1 if shutdown_task in finished_task:
await bot_instance.close()
app_logger.info("Bot closed connection successfully")
except discord.errors.LoginFailure:
app_logger.critical("Improper token has been passed. Aborting startup")
quit(1)
try: finally:
bot.run(bot_config["DISCORD"]["TOKEN"], log_handler = None) close_event.set()
except discord.errors.LoginFailure: app_logger.info(f"Exiting. Application ran for {get_elapsed_time_big(datetime.now().timestamp() - startup_time)}")
app_logger.critical("Improper token has been passed. Aborting startup")
quit(1)
app_logger.info("Quitting application ...") # Entry point for execution
asyncio.run(bot.close()) if __name__ == "__main__":
app_logger.info(f"Exiting. Application ran for {get_elapsed_time_big(datetime.now().timestamp() - startup)}") event_loop = asyncio.get_event_loop()
asyncio.set_event_loop(event_loop)
close_event = asyncio.Event()
event_loop.run_until_complete(main(event_loop, close_event))
# Wait until program is ready to close
event_loop.run_until_complete(close_event.wait())
event_loop.close()
+1 -1
View File
@@ -10,7 +10,7 @@ def get_elapsed_time_ms(timestamp:float) -> str:
time_elapsed = datetime.fromtimestamp(timestamp) time_elapsed = datetime.fromtimestamp(timestamp)
return f"{time_elapsed.minute // 60}min {time_elapsed.second:02}sec {time_elapsed.microsecond // 1000}ms" return f"{time_elapsed.minute // 60}min {time_elapsed.second:02}sec {time_elapsed.microsecond // 1000}ms"
def get_elapsed_time_smal(timestamp:float) -> str: def get_elapsed_time_small(timestamp:float) -> str:
"""Convert an timestamp into an predefined elapsed time format (00sec 000ms)""" """Convert an timestamp into an predefined elapsed time format (00sec 000ms)"""
time_elapsed = datetime.fromtimestamp(timestamp) time_elapsed = datetime.fromtimestamp(timestamp)
return f"{time_elapsed.second:02}sec {time_elapsed.microsecond // 1000:03}ms" return f"{time_elapsed.second:02}sec {time_elapsed.microsecond // 1000:03}ms"