diff --git a/config/.bot.template b/config/.bot.template index 8ff3390..a3adab1 100644 --- a/config/.bot.template +++ b/config/.bot.template @@ -1,2 +1,3 @@ [DISCORD] -TOKEN = \ No newline at end of file +TOKEN = +OWNER_ID = \ No newline at end of file diff --git a/src/cogs/about.py b/src/cogs/about.py new file mode 100644 index 0000000..d5696c8 --- /dev/null +++ b/src/cogs/about.py @@ -0,0 +1,45 @@ +import discord +from discord import app_commands +from discord.ext import commands +from cogs.base_cog import Base_Cog +import logging +from utils.portal import Portal + +class About_Command(Base_Cog): + def __init__(self, bot:commands.Bot): + self.__bot = bot + super().__init__(logging.getLogger("cmds.about")) + + @app_commands.command(name = "about", description = "Provides general information about the bot") + async def about(self, ctx: discord.Interaction): + portal:Portal = Portal.instance() + embed = discord.Embed(title=f"POST IT - `{portal.PROGRAM_VERSION}`", + description="### Übersicht der Befehle\n- `/post` Veröffentlicht einen Post einer anderen Plattform, mit beliebigen Bildern als Anhang\n- `/help` Zeigt diese Übersicht an\n- `/settings` Persönliche Einstellungen für das verhalten des Bots\n- `/stats` Zeigt persönliche und globale Statistiken\n- `/debug` Erweiterte Informationen zum Betriebszustandes des Bots", + colour=0x5a5a5a) + + embed.add_field(name="Status des Bots", + value="Fehlerfrei :green_circle:", + inline=True) + embed.add_field(name="Besitzer der Bot Instanz", + value=f"<@{portal.bot_config['DISCORD']['OWNER_ID']}>", + inline=True) + + await ctx.response.send_message(embed = embed, ephemeral = True) + +async def setup(bot:commands.Bot): + await bot.add_cog(About_Command(bot)) + +# class AddCog(commands.Cog): +# def __init__(self, bot): +# self.bot = bot + +# @app_commands.command(name="add", description="Füge ein neues Wort zur Liste hinzu") +# async def add(self, interaction: discord.Interaction, new_word: str): +# if new_word.lower() not in self.bot.known_words: +# self.bot.known_words.append(new_word.lower()) +# await interaction.response.send_message(f"Wort '{new_word}' wurde zur Liste hinzugefügt.") +# else: +# await interaction.response.send_message(f"Wort '{new_word}' existiert bereits.") + +# async def setup(bot): +# await bot.add_cog(AddCog(bot)) diff --git a/src/cogs/base_cog.py b/src/cogs/base_cog.py new file mode 100644 index 0000000..82fcd96 --- /dev/null +++ b/src/cogs/base_cog.py @@ -0,0 +1,14 @@ +import discord +from discord import app_commands +from discord.ext import commands +import logging + +class Base_Cog(commands.Cog): + def __init__(self, logger:logging.Logger): + self._logger = logger + + async def cog_load(self): + self._logger.debug(f"Cog for the '{self.__class__.__name__}' command got loaded") + + async def cog_unload(self): + self._logger.debug(f"Cog for the '{self.__class__.__name__}' command got unloaded") \ No newline at end of file diff --git a/src/cogs/reload.py b/src/cogs/reload.py new file mode 100644 index 0000000..a44f407 --- /dev/null +++ b/src/cogs/reload.py @@ -0,0 +1,25 @@ +import discord +from discord import app_commands +from discord.ext import commands +from cogs.base_cog import Base_Cog +import logging +from datetime import datetime + +class Reload_Command(Base_Cog): + def __init__(self, bot:commands.Bot): + self.__bot = bot + super().__init__(logging.getLogger("cmds.reload")) + + @app_commands.command(name = "reload_all", description = "Reloads all cogs") + async def about(self, ctx: discord.Interaction): + self._logger.info("Reloading all cogs ...") + task_start = datetime.now().timestamp() + + cog_names = list(self.__bot.extensions.keys()) + for extension_name in cog_names: + await self.__bot.reload_extension(extension_name) + + await ctx.response.send_message("Cogs successfully reloaded.") + +async def setup(bot:commands.Bot): + await bot.add_cog(Reload_Command(bot)) \ No newline at end of file diff --git a/src/main.py b/src/main.py index 6b1db6e..1cf39f9 100644 --- a/src/main.py +++ b/src/main.py @@ -14,6 +14,9 @@ import discord from discord.ext import commands from pathlib import Path import re +import sys +import traceback +from utils.portal import Portal source_path = Path(__file__).resolve() base_path = source_path.parents[1] @@ -26,6 +29,12 @@ class MyBot(commands.Bot): def __init__(self): super().__init__(command_prefix=None, help_command=None, intents=intents) + async def setup_hook(self): + # Register cogs to handle commands + for cog_name in ["about", "reload"]: + await self.load_extension(f"cogs.{cog_name}") + await self.tree.sync() + bot = MyBot() bot_config = Advanced_ConfigParser(Path.joinpath(base_path, "config", "bot.ini")) if re.match(r'[A-Za-z\d]{24}\.[\w-]{6}\.[\w-]{27}', bot_config["DISCORD"]["TOKEN"]): @@ -41,6 +50,16 @@ else: async def on_ready(): app_logger.info(f"Successfully logged in (after {get_elapsed_time_smal(datetime.now().timestamp() - startup)}) as {bot.user}") +@bot.tree.error +async def on_app_command_error(ctx, error): + # Traceback für mehr Informationen zum Fehler + print('Ignoring exception in command {}:'.format(ctx.command), file=sys.stderr) + traceback.print_exception(type(error), error, error.__traceback__) + +# Execute some housekeeping actions +portal = Portal.instance() +portal.bot_config = bot_config + try: bot.run(bot_config["DISCORD"]["TOKEN"], log_handler = None) except discord.errors.LoginFailure: diff --git a/src/utils/logger/custom_logging.py b/src/utils/logger/custom_logging.py index 2979f59..1b6a776 100644 --- a/src/utils/logger/custom_logging.py +++ b/src/utils/logger/custom_logging.py @@ -28,5 +28,10 @@ class Custom_Logger: utils_logger.addHandler(cls.console_handler) utils_logger.setLevel(logging.DEBUG) + commands_logger = logging.getLogger("cmds") + commands_logger.addHandler(cls.console_handler) + commands_logger.setLevel(logging.DEBUG) + + logging.getLogger('discord.app_commands.tree').setLevel(logging.DEBUG) app_logger.debug("Logging successfully initialized") \ No newline at end of file diff --git a/src/utils/portal.py b/src/utils/portal.py new file mode 100644 index 0000000..2fb4044 --- /dev/null +++ b/src/utils/portal.py @@ -0,0 +1,6 @@ +from utils.singleton import Singleton + +@Singleton +class Portal: + PROGRAM_VERSION = "0.1" + bot_config = None \ No newline at end of file diff --git a/src/utils/singleton.py b/src/utils/singleton.py new file mode 100644 index 0000000..5eff5d8 --- /dev/null +++ b/src/utils/singleton.py @@ -0,0 +1,38 @@ +class Singleton: + """ + A non-thread-safe helper class to ease implementing singletons. + This should be used as a decorator -- not a metaclass -- to the + class that should be a singleton. + + The decorated class can define one `__init__` function that + takes only the `self` argument. Also, the decorated class cannot be + inherited from. Other than that, there are no restrictions that apply + to the decorated class. + + To get the singleton instance, use the `instance` method. Trying + to use `__call__` will result in a `TypeError` being raised. + + """ + # Source: https://stackoverflow.com/questions/31875/is-there-a-simple-elegant-way-to-define-singletons + + def __init__(self, decorated): + self._decorated = decorated + + def instance(self): + """ + Returns the singleton instance. Upon its first call, it creates a + new instance of the decorated class and calls its `__init__` method. + On all subsequent calls, the already created instance is returned. + + """ + try: + return self._instance + except AttributeError: + self._instance = self._decorated() + return self._instance + + def __call__(self): + raise TypeError('Singletons must be accessed through `instance()`.') + + def __instancecheck__(self, inst): + return isinstance(inst, self._decorated) \ No newline at end of file