diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0b1e1e7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,27 @@ +**/__pycache__ +**/.venv +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/bin +**/charts +**/docker-compose* +**/compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md diff --git a/.github/workflows/release-docker.yml b/.github/workflows/release-docker.yml new file mode 100644 index 0000000..e9c48d1 --- /dev/null +++ b/.github/workflows/release-docker.yml @@ -0,0 +1,35 @@ +name: Release Multi-Arch Docker Image + +on: + release: + types: [published] + +jobs: + build-and-push-docker: + runs-on: self-hosted + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push multi-arch image + uses: docker/build-push-action@v5 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64 + tags: | + ghcr.io/${{ github.repository }}:${{ github.event.release.tag_name }} + ghcr.io/${{ github.repository }}:latest diff --git a/.gitignore b/.gitignore index c4d6440..6a67833 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,6 @@ config/*.ini pastebin.txt .DS_Store test*.py -.dockerignore docker-* .vscode diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e24116d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +# Base image (multi-arch via buildx) +FROM python:3.10-slim + +# Prevents pyc files + enables unbuffered logging +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +# Install dependencies +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy only necessary files (via .dockerignore) +COPY . . + +# Create non-root user +RUN adduser --disabled-password --no-create-home --gecos "" appuser \ + && chown -R appuser /app +USER appuser + +# Default command +ENTRYPOINT ["python", "src/main.py"] diff --git a/requirements.txt b/requirements.txt index f296ac7..cc47caa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ discord.py==2.4.0 -praw>=7.8.0 -colorama==0.4.6 \ No newline at end of file +asyncpraw==7.8.1 +colorama==0.4.6 +aiohttp==3.11.11 +pillow==11.1.0 \ No newline at end of file diff --git a/src/cogs/about.py b/src/cogs/about.py deleted file mode 100644 index c67035e..0000000 --- a/src/cogs/about.py +++ /dev/null @@ -1,33 +0,0 @@ -import discord -from discord import app_commands -from discord.ext import commands -from cogs.base_cog import Base_Cog -from cogs.maintenance import Maintenance_Command - -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") - @app_commands.check(Maintenance_Command.handle_check) - 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)) \ No newline at end of file diff --git a/src/cogs/debug.py b/src/cogs/debug.py index dc44426..64f67d1 100644 --- a/src/cogs/debug.py +++ b/src/cogs/debug.py @@ -2,7 +2,6 @@ import discord from discord import app_commands from discord.ext import commands from cogs.base_cog import Base_Cog -from cogs.maintenance import Maintenance_Command import logging from utils.portal import Portal @@ -16,7 +15,6 @@ class Debug_Command(Base_Cog): super().__init__(logging.getLogger("cmds.debug")) @app_commands.command(name = "debug", description = "Provides debug informations about the bot, useful for troubleshooting") - @app_commands.check(Maintenance_Command.handle_check) @log_command_execution async def debug(self, ctx:discord.Interaction): portal:Portal = Portal.instance() diff --git a/src/cogs/maintenance.py b/src/cogs/maintenance.py deleted file mode 100644 index 019d3cc..0000000 --- a/src/cogs/maintenance.py +++ /dev/null @@ -1,61 +0,0 @@ -import discord -from discord.ext import commands -from discord import app_commands -from cogs.base_cog import Base_Cog - -import logging - - -class Maintenance_Command(Base_Cog): - def __init__(self, bot): - self.__bot = bot - self.__maintenance_mode_enabled = False - super().__init__(logging.getLogger("cmds.maintenance")) - - def enable_global_maintenance(self): - self.__maintenance_mode_enabled = True - - def disable_gloabal_maintenance(self): - self.__maintenance_mode_enabled = False - - @staticmethod - async def handle_check(ctx: discord.Integration): - """Check function called for every command""" - instance = ctx.client.get_cog("Maintenance_Command") - if instance and instance.__maintenance_mode_enabled: - await instance.startup(ctx) - return False - return True - - async def startup(self, ctx: discord.Interaction): - """Returns an message, explaining that the bot is still starting up""" - embed = discord.Embed( - title = ":construction: Bot is still starting up! :construction:", - description = "While the bot is starting, all commands are locked and unable to be executed", - color = 0xED4337) - await ctx.response.send_message(embed = embed) - - @commands.Cog.listener() - async def on_command_error(self, ctx, error): - if isinstance(error, app_commands.CheckFailure): - # Suppress the traceback and inform the user about maintenance mode - return # Already handled in the `startup` method - - cmds_group = app_commands.Group(name="maintenance", description="Put the bot into maintenance mode and control its alternative behaviour") - @cmds_group.command(name = "enable", description = "Enables the maintenance mode globally") - async def maintenance_enable(self, ctx: discord.Interaction): - if self.__maintenance_mode_enabled: - await ctx.response.send_message("Maintenance mode was allready enabled", ephemeral = True) - else: - await ctx.response.send_message("Maintenance mode is now enabled", ephemeral = True) - self.__maintenance_mode_enabled = True - - async def maintenance_disable(self, ctx: discord.Interaction): - if self.__maintenance_mode_enabled: - await ctx.response.send_message("Maintenance mode is now disabled", ephemeral = True) - else: - await ctx.response.send_message("Maintenance mode was allready disabled", ephemeral = True) - self.__maintenance_mode_enabled = False - -async def setup(bot:commands.Bot): - await bot.add_cog(Maintenance_Command(bot)) \ No newline at end of file diff --git a/src/cogs/post.py b/src/cogs/post.py index 28fddca..1e53e72 100644 --- a/src/cogs/post.py +++ b/src/cogs/post.py @@ -2,12 +2,19 @@ import discord from discord import app_commands from discord.ext import commands from cogs.base_cog import Base_Cog -from cogs.maintenance import Maintenance_Command import logging from urllib.parse import urlparse -from praw.models import Submission, Subreddit +from asyncpraw.models import Submission from utils.portal import Portal +import aiohttp +from PIL import Image +from io import BytesIO +from datetime import datetime +from utils.datetime_tools import get_elapsed_time_milliseconds + +class NoMediaFound(Exception): + pass class Post_Command(Base_Cog): def __init__(self, bot:commands.Bot): @@ -15,58 +22,156 @@ class Post_Command(Base_Cog): super().__init__(logging.getLogger("cmds.post")) @app_commands.command(name = "post", description = "Post an embed in the Current Channel with a link to the content") - @app_commands.check(Maintenance_Command.handle_check) - async def post(self, ctx:discord.Interaction, url:str): - domain_info = urlparse(url) - portal = Portal.instance() - toplevel_domain = '.'.join(domain_info.netloc.split('.')[-2:]) - match toplevel_domain: - case "reddit.com": - subm:Submission = portal.reddit_adapter.fetch(url) - embeds = [] + @app_commands.describe(url = "URL to the post", custom_note = "Describe the post with your own note", use_title = "Display the title of the post", quality = "Specifies the quality of the converted image, closer to 100 is better") + @app_commands.choices(quality = [ + app_commands.Choice(name = "Poor (60)", value = 60), + app_commands.Choice(name = "Fair (70)", value = 70), + app_commands.Choice(name = "Good (80)", value = 80), + app_commands.Choice(name = "Very Good (85)", value = 85), + app_commands.Choice(name = "Excellent (90)", value = 90), + app_commands.Choice(name = "Superior (95)", value = 95), + app_commands.Choice(name = "Perfect (100)", value = 100) + ]) + async def post(self, ctx:discord.Interaction, url:str, custom_note:str = None, use_title:bool = True, quality:app_commands.Choice[int] = 95): + try: + domain_info = urlparse(url) + portal = Portal.instance() + toplevel_domain = '.'.join(domain_info.netloc.split('.')[-2:]) + begin_process = datetime.now().timestamp() + match toplevel_domain: + case "reddit.com": + self._logger.debug(f"Recieved command by {ctx.user} ({ctx.user.id}) for reddit ({url})") - image_urls = [] - # Check if submission has a gallery - if hasattr(subm, "media_metadata"): - for media_id, media in subm.media_metadata.items(): - file_extension = media["m"].split("/")[1] - image_urls.append(f"https://i.redd.it/{media_id}.{file_extension}") - else: - image_urls.append(subm.url) - self._logger.debug(f"Found {len(image_urls)} image urls for the post") + subm:Submission = await portal.reddit_adapter.fetch(url) + image_urls = [] + # Check if submission has a gallery + if hasattr(subm, "media_metadata"): + for media_id, media in subm.media_metadata.items(): + file_extension = media["m"].split("/")[1] + if file_extension not in ("jpg", "jpeg", "png", "webp", "heic", "heif"): + continue + image_urls.append(f"https://i.redd.it/{media_id}.{file_extension}") + else: + image_urls.append(subm.url) + image_count = len(image_urls) + if image_count == 0: + raise NoMediaFound + self._logger.debug(f"Found {image_count} image urls for the post") - embed = discord.Embed(url = "https://discord.com/humans.txt", color = int(portal.platforms_config["REDDIT"]["embed_color"], 16)) - embed.set_author(name = subm.title) - embeds.append(embed) + progress_title = f"`{image_count}` images are going to be converted, it may take a while." + progress_temp = progress_title + f"\n`0` of `{image_count}` have already been loaded" + await ctx.response.send_message(progress_temp, ephemeral = True) - embed.add_field( - name = "Author", - value = f"[u/{subm.author}](https://www.reddit.com/user/{subm.author})", - inline = True) - embed.add_field( - name = "Subreddit", - value = f"[r/{subm.subreddit.display_name}]({url})", - inline = True) - if subm.selftext: - embed.add_field( - name = "Discription", - value = subm.selftext, - inline = False) + if isinstance(quality, app_commands.Choice): + quality_value = quality.value + else: + quality_value = quality + + # Download and convert each image + image_files:list[discord.File] = [] + begin_conversion = datetime.now().timestamp() + async with aiohttp.ClientSession() as session: + index = 0 + for image_url in image_urls: + async with session.get(image_url) as response: + response.raise_for_status() + image_data = await response.read() + + original_image = Image.open(BytesIO(image_data)) + webp_buffer = BytesIO() + original_image.save(webp_buffer, format = "WEBP", quality = quality_value) + webp_buffer.seek(0) + image_file = discord.File(webp_buffer, filename = f"image_{index}.webp") + image_files.append(image_file) + index += 1 + + progress_temp = progress_title + f"\n`{index}` of `{image_count}` have already been loaded" + await ctx.edit_original_response(content = progress_temp) + + self._logger.debug(f"Downloaded and converted {len(image_files)} images in {get_elapsed_time_milliseconds(datetime.now().timestamp() - begin_conversion)}") + + author = subm.author.name if subm.author else "Author not found" + content = f":copyright: [{author}]({url})" + if use_title: + content += f"\n# {subm.title}" + + if custom_note: + content += f"\n> {custom_note}" + + await ctx.delete_original_response() + message = await ctx.followup.send( + content = content, + suppress_embeds = True, + files = image_files + ) + + self._logger.info(f"Successfully processed the command executed by {ctx.user.name} ({ctx.user.id}) after {get_elapsed_time_milliseconds(datetime.now().timestamp() - begin_process)} (ID of message: {message.id})") - for image_url in image_urls: - embed = discord.Embed(url = "https://discord.com/humans.txt") - embed.set_image(url = image_url) - embeds.append(embed) + # No domain for seperation found + case _: + if toplevel_domain == "": + toplevel_domain = "not_found" + + embed = discord.Embed( + title = "Domain not found", + description = f"The requested domain `{toplevel_domain}` is currently not supported\nOpen [an issue](https://github.com/official-Cromatin/Post-It/issues/new?assignees=&labels=feature-request&projects=&template=feature_request.yml) to request support for it.\n\nCurrently supported plattforms:\n- Reddit", + color = 0xED4337) - await ctx.response.send_message(embeds = embeds) - - # No domain for seperation found - case _: - embed = discord.Embed( - title = "Domain not found", - description = f"The requested domain `{toplevel_domain}` is currently not supported\nOpen [an issue](https://github.com/official-Cromatin/Post-It/issues/new?assignees=&labels=feature-request&projects=&template=feature_request.yml) to request support for it.\n\nSee an list of supported platforms with the `/platforms` command", - color = 0xED4337) + await ctx.response.send_message(embed = embed, ephemeral = True) + except NoMediaFound: + self._logger.error(f"Aborted issued command by {ctx.user.name} ({ctx.user.id}). Post had no media attatched") + + # Delete the original response, if existing + try: + await ctx.delete_original_response() + except discord.NotFound: + pass + embed = discord.Embed( + title = "No media found", + description = "The post had no media attatched, or was in an unsupported format\nVideos are not supported! (yet)", + color = 0xED4337 + ) + embed.set_footer(text = "Supported image formats: jpg, jpeg, png, webp, heic, heif") + + except discord.errors.HTTPException as error: + self._logger.error(f"Could not complete command by {ctx.user.name} ({ctx.user.id})") + self._logger.exception(error, stack_info = True) + + match error.code: + case 40005: + self._logger.error("Failed to upload images, payload too large") + + embed = discord.Embed( + title = "Error while processing", + description = "The attachments exceed the upload limit of Discord,\nchoose a different quality level via the argument `quality`", + color = 0xED4337 + ) + + if ctx.response.is_done(): + await ctx.followup.send(embed = embed, ephemeral = True) + else: + await ctx.response.send_message(embed = embed) + + except Exception as error: + self._logger.error(f"Could not complete command by {ctx.user.name} ({ctx.user.id})") + self._logger.exception(error, stack_info = True) + + # Delete the original response, if existing + try: + await ctx.delete_original_response() + except discord.NotFound: + pass + + # Explain to the user what the error was + embed = discord.Embed( + title = "Error while processing", + description = f"While we processed your request, the following exception occured: `{error}`", + color = 0xED4337 + ) + if ctx.response.is_done(): + await ctx.followup.send(embed = embed, ephemeral = True) + else: await ctx.response.send_message(embed = embed) diff --git a/src/cogs/reload.py b/src/cogs/reload.py deleted file mode 100644 index 9d660f2..0000000 --- a/src/cogs/reload.py +++ /dev/null @@ -1,116 +0,0 @@ -import discord -from discord import app_commands -from discord.ext import commands -from cogs.base_cog import Base_Cog -from cogs.maintenance import Maintenance_Command - -import logging -from datetime import datetime -from utils.datetime_tools import get_elapsed_time_milliseconds -import traceback -from utils.truncate_str import truncate_message_with_notice - -class Reload_Command(Base_Cog): - def __init__(self, bot:commands.Bot): - self.__bot = bot - self.__cached_cog_names:list[str] = None - self.__reload_running = False - super().__init__(logging.getLogger("cmds.reload")) - - async def autocomplete_cog(self, ctx: discord.Interaction, cog_name_stw:str): - """Helpermethod to recommend the right cog name to the user""" - # Cache the cog names if not cached yet - if not self.__cached_cog_names: - self.__cached_cog_names = list(self.__bot.extensions.keys()) - - if cog_name_stw == "": - # Return first 25 cog names, if provided name is empty - matching_cogs = sorted(self.__cached_cog_names) - else: - matching_cogs = [] - for cog_name in self.__cached_cog_names: - if cog_name.startswith(cog_name_stw.lower()): - matching_cogs.append(cog_name) - - choices = [] - for matching_cog in matching_cogs[:25]: - choices.append(app_commands.Choice(name = matching_cog.removeprefix("cogs.").capitalize(), value = matching_cog)) - - return choices - - @app_commands.command(name = "reload_all", description = "Reloads all cogs") - @app_commands.check(Maintenance_Command.handle_check) - async def reload_all(self, ctx: discord.Interaction): - if self.__reload_running: - await ctx.response.send_message("Reload is allready being executed", ephemeral = True) - else: - self.__reload_running = True - task_start = datetime.now().timestamp() - cog_names = list(self.__bot.extensions.keys()) - self._logger.debug(f"Reloading all {len(cog_names)} cogs ...") - try: - embed_cog_stats = "Reloaded the following cogs:\n" - for extension_name in cog_names: - start_reload = datetime.now().timestamp() - await self.__bot.reload_extension(extension_name) - embed_cog_stats += f"- {extension_name.removeprefix('cogs.').capitalize()} (`{get_elapsed_time_milliseconds(datetime.now().timestamp() - start_reload)}`)\n" - - except Exception as error: - embed_cog_stats += f"- {extension_name.removeprefix('cogs.').capitalize()} <--" - traceback_str = truncate_message_with_notice(traceback.format_exc(), 1800, "\nSee console for more details on the full traceback") - - embed = discord.Embed( - title = "Reloading All Cogs", - description = f"{embed_cog_stats} \n\nDuring the reload the following exception occured:\n```{traceback_str}```", - color = 0xED4337) - - raise error - else: - elapsed_time = get_elapsed_time_milliseconds(datetime.now().timestamp() - task_start) - embed = discord.Embed( - title = "Reloading All Cogs", - description = f"{embed_cog_stats}Total time spend: `{elapsed_time}`", - color = 0x4BB543) - - self._logger.info(f"Cogs successfully reloaded after {elapsed_time}") - finally: - await ctx.response.send_message(embed = embed, ephemeral = True) - self.__reload_running = False - - - @app_commands.command(name = "reload", description = "Reload specific cog") - @app_commands.check(Maintenance_Command.handle_check) - @app_commands.autocomplete(cog_name = autocomplete_cog) - async def reload(self, ctx: discord.Interaction, cog_name:str): - if self.__reload_running: - await ctx.response.send_message("Reload is allready being executed", ephemeral = True) - else: - self.__reload_running = True - task_start = datetime.now().timestamp() - self._logger.debug(f"Reloading {cog_name} cog ...") - try: - await self.__bot.reload_extension(cog_name) - except Exception as error: - traceback_str = truncate_message_with_notice(traceback.format_exc(), 1800, "\nSee console for more details on the full traceback") - embed = discord.Embed( - title = f"Reloading `{cog_name.removeprefix('cogs.').capitalize()}` cog", - description = f"Reload failed with the following exception:\n```{traceback_str}```", - color = 0xED4337) - - raise error - else: - embed = discord.Embed( - title = f"Reloading `{cog_name.removeprefix('cogs.').capitalize()}` cog", - description = f"Total time spend: `{get_elapsed_time_milliseconds(datetime.now().timestamp() - task_start)}`", - color = 0x4BB543) - - self._logger.info(f"Cog {cog_name} successfully reloaded after {get_elapsed_time_milliseconds(datetime.now().timestamp() - task_start)}") - finally: - await ctx.response.send_message(embed = embed, ephemeral = True) - self.__reload_running = False - - # Clear the cache - self.__cached_cog_names = None - -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 a82f126..16f2d96 100644 --- a/src/main.py +++ b/src/main.py @@ -1,25 +1,6 @@ -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 @@ -32,7 +13,23 @@ from utils.portal import Portal import asyncio from typing import Union from platforms.reddit import Reddit_Adapter -from cogs.maintenance import Maintenance_Command + +print(" ____ ____ ___________ __________") +print(" / __ \/ __ \/ ___/_ __/ / _/_ __/") +print(" / /_/ / / / /\__ \ / /_____ / / / / ") +print(" / ____/ /_/ /___/ // /_____// / / / ") +print(" /_/ \____//____//_/ /___/ /_/ ") +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") +startup = datetime.now().timestamp() + +# Initialize the logger +Custom_Logger.initialize() +app_logger = logging.getLogger("app") +startup_logger = logging.getLogger("app.startup") source_path = Path(__file__).resolve() base_path = source_path.parents[1] @@ -52,20 +49,18 @@ class MyBot(commands.Bot): async def setup_hook(self): # Register cogs to handle commands - for cog_name in ["maintenance", "about", "debug", "reload", "post"]: + for cog_name in ["debug", "post"]: await self.load_extension(f"cogs.{cog_name}") await self.tree.sync() async def on_app_command_completion(self, interaction: discord.Interaction, command: Union[discord.app_commands.Command, discord.app_commands.ContextMenu]): """Called when a `app_commands.Command` or `app_commands.ContextMenu` has successfully completed without error""" self.__portal.no_succeeded_commands += 1 - print("Command succeeded") async def on_interaction(self, interaction: discord.Interaction): """Called when an interaction happened""" match interaction.type.name: case discord.InteractionType.application_command.name: - print("Interaction with bot", interaction.command.name) self.__portal.no_executed_commands += 1 case discord.InteractionType.ping.name: print("App got pinged by discord") @@ -79,29 +74,25 @@ class MyBot(commands.Bot): async def on_connect(self): """A coroutine to be called to setup the bot, after the bot is logged in but before it has connected to the Websocket""" if not self.__first_on_ready: - instance: Maintenance_Command = self.get_cog("Maintenance_Command") - instance.enable_global_maintenance() - startup_logger.info(f"Beginning startup routine ...") + startup_logger.info("Beginning startup routine ...") 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 task_start = datetime.now().timestamp() - startup_logger.debug(f"Loading platforms config ...") + startup_logger.debug("Loading platforms config ...") 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 task_start = datetime.now().timestamp() - startup_logger.debug(f"Creating reddit adapter ...") + startup_logger.debug("Creating reddit adapter ...") portal.reddit_adapter = Reddit_Adapter(platforms_config["REDDIT"]["CLIENT_ID"], platforms_config["REDDIT"]["CLIENT_SECRET"]) 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) startup_logger.info(f"Startup routine finished after {get_elapsed_time_milliseconds(datetime.now().timestamp() - routine_begin)}") - - instance.disable_gloabal_maintenance() self.__first_on_ready = True else: startup_logger.info("Startup routine allready executed, omitting this execution") diff --git a/src/platforms/reddit.py b/src/platforms/reddit.py index 0f278ce..b764fb2 100644 --- a/src/platforms/reddit.py +++ b/src/platforms/reddit.py @@ -1,13 +1,12 @@ -import praw -import praw.models +import asyncpraw +import asyncpraw.models import logging from utils.event_counter import Event_Counter from utils.datetime_tools import get_elapsed_time_milliseconds -from typing import Union from datetime import datetime -class Reddit_Adapter(praw.Reddit): - """A class that extends and abstracts the functionality of the `praw.Reddit` class by adding +class Reddit_Adapter(asyncpraw.Reddit): + """A class that extends and abstracts the functionality of the `asyncpraw.Reddit` class by adding logging and request tracking capabilities.""" VERSION = "1.0" number_of_instances = 0 @@ -25,11 +24,11 @@ class Reddit_Adapter(praw.Reddit): self.__events = Event_Counter(1000) self.__logger = logging.getLogger(f"pltfm.reddit.{self.__instance_number}") - def fetch(self, post_url:str) -> praw.models.Submission: + async def fetch(self, post_url:str) -> asyncpraw.models.Submission: """Fetches specified submission (post) and returns it""" start_time = datetime.now().timestamp() self.__events.increment() - subm = self.submission(url = post_url) + subm = await self.submission(url = post_url) self.__logger.debug(f"Submission for post (URL: {post_url}), successfully fetched after {get_elapsed_time_milliseconds(datetime.now().timestamp() - start_time)}") return subm