From 3a8f7140ab66d86c7dbf068a1a59ee04a3b7d416 Mon Sep 17 00:00:00 2001 From: Cromatin Date: Fri, 17 Jan 2025 17:17:02 +0100 Subject: [PATCH 01/16] Prepared the post command for release --- src/cogs/post.py | 81 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 59 insertions(+), 22 deletions(-) diff --git a/src/cogs/post.py b/src/cogs/post.py index 28fddca..6a127d2 100644 --- a/src/cogs/post.py +++ b/src/cogs/post.py @@ -8,6 +8,11 @@ import logging from urllib.parse import urlparse from praw.models import Submission, Subreddit 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 Post_Command(Base_Cog): def __init__(self, bot:commands.Bot): @@ -16,12 +21,14 @@ class Post_Command(Base_Cog): @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): + async def post(self, ctx:discord.Interaction, url:str, custom_note:str = None): domain_info = urlparse(url) portal = Portal.instance() toplevel_domain = '.'.join(domain_info.netloc.split('.')[-2:]) match toplevel_domain: case "reddit.com": + await ctx.response.defer() + subm:Submission = portal.reddit_adapter.fetch(url) embeds = [] @@ -35,30 +42,60 @@ class Post_Command(Base_Cog): image_urls.append(subm.url) self._logger.debug(f"Found {len(image_urls)} 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) + # 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() # Fehlerbehandlung für HTTP-Status + image_data = await response.read() # Bilddaten im RAM laden - 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) + original_image = Image.open(BytesIO(image_data)) # Originalbild laden + webp_buffer = BytesIO() # Puffer für WebP-Bild + original_image.save(webp_buffer, format="WEBP", lossless=True) # WebP speichern + webp_buffer.seek(0) + image_file = discord.File(webp_buffer, filename = f"image_{index}.webp") + image_files.append(image_file) + + index += 1 + + self._logger.debug(f"Downloaded and converted {len(image_files)} images in {get_elapsed_time_milliseconds(datetime.now().timestamp() - begin_conversion)}") - for image_url in image_urls: - embed = discord.Embed(url = "https://discord.com/humans.txt") - embed.set_image(url = image_url) - embeds.append(embed) + author = subm.author.name if subm.author else "Author not found" + content = f":copyright: [{author}]({url})" + if custom_note: + content += f"\n> {custom_note}" + + await ctx.followup.send( + content = content, + suppress_embeds = True, + files = image_files + ) - await ctx.response.send_message(embeds = embeds) + # 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) + + # 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) + + # 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 _: -- 2.52.0 From 7c5d841399cb058c518184deba8d3f8637d68852 Mon Sep 17 00:00:00 2001 From: Cromatin Date: Fri, 17 Jan 2025 17:19:21 +0100 Subject: [PATCH 02/16] Removed the maintenance check --- src/cogs/about.py | 1 - src/cogs/debug.py | 2 -- src/cogs/maintenance.py | 61 ----------------------------------------- src/cogs/post.py | 2 -- src/cogs/reload.py | 3 -- 5 files changed, 69 deletions(-) delete mode 100644 src/cogs/maintenance.py diff --git a/src/cogs/about.py b/src/cogs/about.py index c67035e..030dc6e 100644 --- a/src/cogs/about.py +++ b/src/cogs/about.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 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 6a127d2..8c324fd 100644 --- a/src/cogs/post.py +++ b/src/cogs/post.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 urllib.parse import urlparse @@ -20,7 +19,6 @@ 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, custom_note:str = None): domain_info = urlparse(url) portal = Portal.instance() diff --git a/src/cogs/reload.py b/src/cogs/reload.py index 9d660f2..60f9420 100644 --- a/src/cogs/reload.py +++ b/src/cogs/reload.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 datetime import datetime @@ -39,7 +38,6 @@ class Reload_Command(Base_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) @@ -79,7 +77,6 @@ class Reload_Command(Base_Cog): @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: -- 2.52.0 From da18c164c7c198259325eb0b49d70a4fb1af66f9 Mon Sep 17 00:00:00 2001 From: Cromatin Date: Fri, 17 Jan 2025 17:20:33 +0100 Subject: [PATCH 03/16] Removed the about command Removed leftover of the maintenance command --- src/cogs/about.py | 32 -------------------------------- src/main.py | 7 +------ 2 files changed, 1 insertion(+), 38 deletions(-) delete mode 100644 src/cogs/about.py diff --git a/src/cogs/about.py b/src/cogs/about.py deleted file mode 100644 index 030dc6e..0000000 --- a/src/cogs/about.py +++ /dev/null @@ -1,32 +0,0 @@ -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") - @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/main.py b/src/main.py index a82f126..9bec778 100644 --- a/src/main.py +++ b/src/main.py @@ -32,7 +32,6 @@ from utils.portal import Portal import asyncio from typing import Union from platforms.reddit import Reddit_Adapter -from cogs.maintenance import Maintenance_Command source_path = Path(__file__).resolve() base_path = source_path.parents[1] @@ -52,7 +51,7 @@ 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 ["about", "debug", "reload", "post"]: await self.load_extension(f"cogs.{cog_name}") await self.tree.sync() @@ -79,8 +78,6 @@ 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 ...") routine_begin = datetime.now().timestamp() await self.change_presence(status = discord.Status.dnd, activity = discord.CustomActivity("Executing pre startup routine")) @@ -100,8 +97,6 @@ class MyBot(commands.Bot): 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") -- 2.52.0 From eef7343aa7b34a0651629e923ca27ff9af9822cd Mon Sep 17 00:00:00 2001 From: Cromatin Date: Fri, 17 Jan 2025 17:21:27 +0100 Subject: [PATCH 04/16] Removed the reload(-all) command --- src/cogs/reload.py | 113 --------------------------------------------- 1 file changed, 113 deletions(-) delete mode 100644 src/cogs/reload.py diff --git a/src/cogs/reload.py b/src/cogs/reload.py deleted file mode 100644 index 60f9420..0000000 --- a/src/cogs/reload.py +++ /dev/null @@ -1,113 +0,0 @@ -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 -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") - 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.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 -- 2.52.0 From f98a70f330da38ab60f4ce40efb44f890afb472e Mon Sep 17 00:00:00 2001 From: Cromatin Date: Fri, 17 Jan 2025 17:22:53 +0100 Subject: [PATCH 05/16] Removed deleted cogs from the setup_hook --- src/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.py b/src/main.py index 9bec778..d22616d 100644 --- a/src/main.py +++ b/src/main.py @@ -51,7 +51,7 @@ class MyBot(commands.Bot): async def setup_hook(self): # Register cogs to handle commands - for cog_name in ["about", "debug", "reload", "post"]: + for cog_name in ["debug", "post"]: await self.load_extension(f"cogs.{cog_name}") await self.tree.sync() -- 2.52.0 From fdacccc23b46acaf6114ea34f2059b82d3b66055 Mon Sep 17 00:00:00 2001 From: Cromatin Date: Fri, 17 Jan 2025 18:11:31 +0100 Subject: [PATCH 06/16] Polished the post command and the response --- src/cogs/post.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/cogs/post.py b/src/cogs/post.py index 8c324fd..88ab8e9 100644 --- a/src/cogs/post.py +++ b/src/cogs/post.py @@ -25,11 +25,10 @@ class Post_Command(Base_Cog): toplevel_domain = '.'.join(domain_info.netloc.split('.')[-2:]) match toplevel_domain: case "reddit.com": - await ctx.response.defer() + self._logger.debug(f"Recieved command by {ctx.user} ({ctx.user.id}) for reddit ({url})") + begin_process = datetime.now().timestamp() subm:Submission = portal.reddit_adapter.fetch(url) - embeds = [] - image_urls = [] # Check if submission has a gallery if hasattr(subm, "media_metadata"): @@ -38,7 +37,13 @@ class Post_Command(Base_Cog): 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") + image_count = len(image_urls) + loaded_count = 0 + self._logger.debug(f"Found {image_count} image urls for the post") + + 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) # Download and convert each image image_files:list[discord.File] = [] @@ -56,8 +61,10 @@ class Post_Command(Base_Cog): 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)}") @@ -66,12 +73,15 @@ class Post_Command(Base_Cog): if custom_note: content += f"\n> {custom_note}" - await ctx.followup.send( + 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})") + # 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) @@ -97,12 +107,15 @@ class Post_Command(Base_Cog): # 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\nSee an list of supported platforms with the `/platforms` command", + 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(embed = embed) + await ctx.response.send_message(embed = embed, ephemeral = True) async def setup(bot:commands.Bot): -- 2.52.0 From c8e9ba1d9e486051a5733b5716bd9f2ff8c2931b Mon Sep 17 00:00:00 2001 From: Cromatin Date: Fri, 17 Jan 2025 18:11:39 +0100 Subject: [PATCH 07/16] Updated the requirements --- requirements.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index f296ac7..847acb4 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 +praw==7.8.1 +colorama==0.4.6 +aiohttp==3.11.11 +pillow==11.1.0 \ No newline at end of file -- 2.52.0 From 3a9608d863e048ae8e43f2c297db69beaf7809f8 Mon Sep 17 00:00:00 2001 From: Cromatin Date: Thu, 6 Feb 2025 12:36:57 +0100 Subject: [PATCH 08/16] Added descriptions to the command parameters, granular error handling and new parameters --- src/cogs/post.py | 185 +++++++++++++++++++++++++---------------------- 1 file changed, 97 insertions(+), 88 deletions(-) diff --git a/src/cogs/post.py b/src/cogs/post.py index 88ab8e9..5e10063 100644 --- a/src/cogs/post.py +++ b/src/cogs/post.py @@ -19,103 +19,112 @@ 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") - async def post(self, ctx:discord.Interaction, url:str, custom_note:str = None): - domain_info = urlparse(url) - portal = Portal.instance() - toplevel_domain = '.'.join(domain_info.netloc.split('.')[-2:]) - match toplevel_domain: - case "reddit.com": - self._logger.debug(f"Recieved command by {ctx.user} ({ctx.user.id}) for reddit ({url})") - begin_process = datetime.now().timestamp() + @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:]) + match toplevel_domain: + case "reddit.com": + self._logger.debug(f"Recieved command by {ctx.user} ({ctx.user.id}) for reddit ({url})") + begin_process = datetime.now().timestamp() - subm:Submission = 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] - image_urls.append(f"https://i.redd.it/{media_id}.{file_extension}") - else: - image_urls.append(subm.url) - image_count = len(image_urls) - loaded_count = 0 - self._logger.debug(f"Found {image_count} image urls for the post") + subm:Submission = 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] + image_urls.append(f"https://i.redd.it/{media_id}.{file_extension}") + else: + image_urls.append(subm.url) + image_count = len(image_urls) + loaded_count = 0 + self._logger.debug(f"Found {image_count} image urls for the post") - 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) + 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) - # 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() # Fehlerbehandlung für HTTP-Status - image_data = await response.read() # Bilddaten im RAM laden + if isinstance(quality, app_commands.Choice): + quality_value = quality.value + else: + quality_value = quality - original_image = Image.open(BytesIO(image_data)) # Originalbild laden - webp_buffer = BytesIO() # Puffer für WebP-Bild - original_image.save(webp_buffer, format="WEBP", lossless=True) # WebP speichern - webp_buffer.seek(0) - image_file = discord.File(webp_buffer, filename = f"image_{index}.webp") - image_files.append(image_file) - index += 1 + # 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() - progress_temp = progress_title + f"\n`{index}` of `{image_count}` have already been loaded" - await ctx.edit_original_response(content = progress_temp) + 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)}") - 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 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 - ) + author = subm.author.name if subm.author else "Author not found" + content = f":copyright: [{author}]({url})" + if use_title: + content += f"\n# {subm.title}" - 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})") - - # 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) - - # 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) - - # 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" + 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})") + + # 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(embed = embed, ephemeral = True) + 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) + + await ctx.delete_original_response() + await ctx.followup.send( 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(embed = embed, ephemeral = True) + title = "Error while processing", + description = f"While we processed your request, the following exception occured: `{error}`", + color = 0xED4337 + ), + ephemeral = True + ) async def setup(bot:commands.Bot): -- 2.52.0 From 96b001522fb1663696570a9c051f7f4d24741cff Mon Sep 17 00:00:00 2001 From: Cromatin Date: Thu, 6 Feb 2025 18:24:22 +0100 Subject: [PATCH 09/16] Replaced praw with the asyncpraw libary --- requirements.txt | 2 +- src/platforms/reddit.py | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/requirements.txt b/requirements.txt index 847acb4..cc47caa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ discord.py==2.4.0 -praw==7.8.1 +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/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 -- 2.52.0 From 435a2486a82a96573912635f26506368832b05a0 Mon Sep 17 00:00:00 2001 From: offical-Cromatin Date: Sat, 22 Feb 2025 10:32:38 +0100 Subject: [PATCH 10/16] Refined the error report to be more legible --- src/cogs/post.py | 50 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/src/cogs/post.py b/src/cogs/post.py index 5e10063..4e3e69f 100644 --- a/src/cogs/post.py +++ b/src/cogs/post.py @@ -13,6 +13,9 @@ 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): self.__bot = bot @@ -34,22 +37,25 @@ class Post_Command(Base_Cog): 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})") - begin_process = datetime.now().timestamp() - subm:Submission = portal.reddit_adapter.fetch(url) + 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) - loaded_count = 0 + if image_count == 0: + raise NoMediaFound self._logger.debug(f"Found {image_count} image urls for the post") progress_title = f"`{image_count}` images are going to be converted, it may take a while." @@ -87,7 +93,7 @@ class Post_Command(Base_Cog): author = subm.author.name if subm.author else "Author not found" content = f":copyright: [{author}]({url})" if use_title: - content += f"\n# {subm.title}" + content += f"\n## {subm.title}" if custom_note: content += f"\n> {custom_note}" @@ -112,19 +118,39 @@ class Post_Command(Base_Cog): 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 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) - await ctx.delete_original_response() - await ctx.followup.send( - embed = discord.Embed( - title = "Error while processing", - description = f"While we processed your request, the following exception occured: `{error}`", - color = 0xED4337 - ), - ephemeral = 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 ) + await ctx.followup.send(embed = embed, ephemeral = True) async def setup(bot:commands.Bot): -- 2.52.0 From 01d90e6625f2ede0dd8b4c2c866ef24067f10cca Mon Sep 17 00:00:00 2001 From: official-Cromatin Date: Tue, 16 Sep 2025 14:10:04 +0200 Subject: [PATCH 11/16] Move imports to the top of the document - Remove unnecessary prints - Remove unnecessary f-strings --- src/main.py | 44 ++++++++++++++++++++------------------------ 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/src/main.py b/src/main.py index d22616d..2e011c2 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 @@ -33,6 +14,23 @@ import asyncio from typing import Union from platforms.reddit import Reddit_Adapter +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") +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] app_logger.info(f"Using the following path as entrypoint: '{base_path}'") @@ -58,13 +56,11 @@ class MyBot(commands.Bot): 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") @@ -78,20 +74,20 @@ 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: - 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)}") -- 2.52.0 From 4dd578c9a831e123f47aa5ce5e1109760b4db58b Mon Sep 17 00:00:00 2001 From: official-Cromatin Date: Tue, 16 Sep 2025 14:13:02 +0200 Subject: [PATCH 12/16] Refine '/post'-command error handling when file size exceeds upload limit --- src/cogs/post.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/cogs/post.py b/src/cogs/post.py index 4e3e69f..1e53e72 100644 --- a/src/cogs/post.py +++ b/src/cogs/post.py @@ -5,7 +5,7 @@ from cogs.base_cog import Base_Cog 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 @@ -93,7 +93,7 @@ class Post_Command(Base_Cog): author = subm.author.name if subm.author else "Author not found" content = f":copyright: [{author}]({url})" if use_title: - content += f"\n## {subm.title}" + content += f"\n# {subm.title}" if custom_note: content += f"\n> {custom_note}" @@ -134,6 +134,25 @@ class Post_Command(Base_Cog): ) 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) @@ -150,7 +169,10 @@ class Post_Command(Base_Cog): description = f"While we processed your request, the following exception occured: `{error}`", color = 0xED4337 ) - await ctx.followup.send(embed = embed, ephemeral = True) + if ctx.response.is_done(): + await ctx.followup.send(embed = embed, ephemeral = True) + else: + await ctx.response.send_message(embed = embed) async def setup(bot:commands.Bot): -- 2.52.0 From f427d06e6180a0fb4caa67adb645674960163a39 Mon Sep 17 00:00:00 2001 From: official-Cromatin Date: Tue, 16 Sep 2025 16:07:07 +0200 Subject: [PATCH 13/16] Create Dockerfile and .dockerignore for upcoming official docker support - Update .gitignore --- .dockerignore | 27 +++++++++++++++++++++++++++ .gitignore | 1 - Dockerfile | 22 ++++++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 .dockerignore create mode 100644 Dockerfile 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/.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"] -- 2.52.0 From f56636c1ff8c933616f16f950e57f98314848375 Mon Sep 17 00:00:00 2001 From: official-Cromatin Date: Tue, 16 Sep 2025 17:30:08 +0200 Subject: [PATCH 14/16] Create workflow to build and release docker image on ghcr --- .github/workflows/release-docker.yml | 35 ++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/release-docker.yml 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 -- 2.52.0 From 26e3a4bb8e9285bb09e2b071a3af3f0203a019b7 Mon Sep 17 00:00:00 2001 From: official-Cromatin Date: Tue, 16 Sep 2025 18:13:56 +0200 Subject: [PATCH 15/16] Update LICENSE --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index f0ce113..3646557 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ 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 of this software and associated documentation files (the "Software"), to deal -- 2.52.0 From bfd497898e506acea455a627b8fe610c1aa4dd96 Mon Sep 17 00:00:00 2001 From: official-Cromatin Date: Tue, 16 Sep 2025 18:30:07 +0200 Subject: [PATCH 16/16] Update copyright year --- src/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.py b/src/main.py index 2e011c2..16f2d96 100644 --- a/src/main.py +++ b/src/main.py @@ -19,7 +19,7 @@ print(" / __ \/ __ \/ ___/_ __/ / _/_ __/") print(" / /_/ / / / /\__ \ / /_____ / / / / ") print(" / ____/ /_/ /___/ // /_____// / / / ") print(" /_/ \____//____//_/ /___/ /_/ ") -print(" Copyright (c) 2024 Lars Winzer") +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") -- 2.52.0