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"))
# Create the adapters for the platforms case _:
task_start = datetime.now().timestamp() app_startup_logger.fatal(f"Detected unsupported platform: {sys.platform}")
startup_logger.debug(f"Loading platforms config ...") quit(1)
platforms_config = Advanced_ConfigParser(Path.joinpath(base_path, "config", "platforms.ini"))
portal.platforms_config = platforms_config
startup_logger.info(f"Loaded platforms config after {get_elapsed_time_milliseconds(datetime.now().timestamp() - task_start)}")
# Create platforms adapter # Load environment variables
task_start = datetime.now().timestamp() if (os.getenv("skip_dotenv", False)):
startup_logger.debug(f"Creating reddit adapter ...") app_startup_logger.warning("Skipped import of dotenv file")
portal.reddit_adapter = Reddit_Adapter(platforms_config["REDDIT"]["CLIENT_ID"], platforms_config["REDDIT"]["CLIENT_SECRET"]) else:
startup_logger.info(f"Created reddit adapter after {get_elapsed_time_milliseconds(datetime.now().timestamp() - task_start)}") dotenv.load_dotenv(base_path / ".env")
app_startup_logger.warning("Imported dotenv file")
await self.change_presence(status = discord.Status.online, activity = None) # Check if required environment variables are present
startup_logger.info(f"Startup routine finished after {get_elapsed_time_milliseconds(datetime.now().timestamp() - routine_begin)}") check_ok = True
self.__first_on_ready = True required_variables = [
else: "DISCORD_TOKEN",
startup_logger.info("Startup routine allready executed, omitting this execution") ]
async def on_ready(self): for variable_name in required_variables:
app_logger.info(f"Successfully logged in (after {get_elapsed_time_smal(datetime.now().timestamp() - startup)}) as {self.user}") if os.getenv(variable_name) is None:
app_startup_logger.error(f"Environment variable '{variable_name}' is missing")
check_ok = False
bot = MyBot() if not check_ok:
bot_config = Advanced_ConfigParser(Path.joinpath(base_path, "config", "bot.ini")) app_startup_logger.critical("Multiple required variables are missing. Aborting startup")
if re.match(r'[A-Za-z\d]{24}\.[\w-]{6}\.[\w-]{27}', bot_config["DISCORD"]["TOKEN"]): quit(1)
app_logger.critical("Bot (config/bot.ini) configuration invalid, please set a valid token")
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 # Create bot instance and start bot
portal = Portal.instance() app_startup_logger.info(f"Preperations complete after {get_elapsed_time_milliseconds(datetime.now().timestamp() - startup_time)}, launching bot")
portal.bot_config = bot_config try:
portal.STARTUP_TIMESTAMP = startup bot_instance = PostIt_Bot(startup_time, PROGRAM_VERSION, app_logger)
bot.set_portal(portal)
# Setup handlers to handle states of command execution bot_task = event_loop.create_task(bot_instance.start(os.getenv("DISCORD_TOKEN")))
@bot.tree.error shutdown_task = event_loop.create_task(shutdown_event.wait())
async def on_app_command_error(ctx:discord.Interaction, error):
"""Executed when exception during command execution occurs"""
print('Ignoring exception in command {}:'.format(ctx.command), file=sys.stderr)
traceback.print_exception(type(error), error, error.__traceback__)
portal.no_failed_commands += 1 # Wait if either the bot disconnects or the shutdown event is detected
finished_task, _ = await asyncio.wait(
[shutdown_task, bot_task],
return_when = asyncio.FIRST_COMPLETED
)
try: if shutdown_task in finished_task:
bot.run(bot_config["DISCORD"]["TOKEN"], log_handler = None) await bot_instance.close()
except discord.errors.LoginFailure: app_logger.info("Bot closed connection successfully")
app_logger.critical("Improper token has been passed. Aborting startup")
quit(1)
app_logger.info("Quitting application ...") except discord.errors.LoginFailure:
asyncio.run(bot.close()) app_logger.critical("Improper token has been passed. Aborting startup")
app_logger.info(f"Exiting. Application ran for {get_elapsed_time_big(datetime.now().timestamp() - startup)}") quit(1)
finally:
close_event.set()
app_logger.info(f"Exiting. Application ran for {get_elapsed_time_big(datetime.now().timestamp() - startup_time)}")
# Entry point for execution
if __name__ == "__main__":
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"