From 8e0df4e3e14ce84f586ef5dcf4c2e7e1b0bfc7ab Mon Sep 17 00:00:00 2001 From: Evanroby <107794516+Evanroby@users.noreply.github.com> Date: Wed, 18 Mar 2026 18:06:30 +0100 Subject: [PATCH 1/2] [Autopublisher]: Improvements. --- autopublisher/autopublisher.py | 36 +++++++++++++++--------------- autopublisher/info.json | 3 ++- autopublisher/utils.py | 40 +++++++++++----------------------- autopublisher/view.py | 28 ++++++++++++------------ 4 files changed, 47 insertions(+), 60 deletions(-) diff --git a/autopublisher/autopublisher.py b/autopublisher/autopublisher.py index 3cc8a05a..6c7ade22 100644 --- a/autopublisher/autopublisher.py +++ b/autopublisher/autopublisher.py @@ -35,11 +35,8 @@ from .dashboard_integration import DashboardIntegration from .utils import ( - get_next_reset_times, - get_owner_timezone, increment_published_count, initialize_scheduler, - reset_count, schedule_resets, ) from .view import IgnoredNewsChannelsView, StatsView @@ -72,17 +69,15 @@ def __init__(self, bot: Red) -> None: self.config.register_guild(**default_guild) self.config.register_global(**default_global) self.scheduler = None - self.bot.loop.create_task(self._initialize_scheduler()) - async def _initialize_scheduler(self) -> None: - """Initialize the scheduler after cog is loaded.""" + async def cog_load(self) -> None: + """Initialize the scheduler when the cog is loaded.""" self.scheduler = await initialize_scheduler(self) await schedule_resets(self) def cog_unload(self) -> None: """Clean up scheduler on cog unload.""" if self.scheduler and self.scheduler.running: - self.scheduler.remove_all_jobs() self.scheduler.shutdown() logger.debug("Scheduler shut down") @@ -129,8 +124,22 @@ async def on_message_without_command(self, message: discord.Message) -> None: await asyncio.sleep(0.5) await asyncio.wait_for(message.publish(), timeout=60) await increment_published_count(self.config) - except (discord.HTTPException, discord.Forbidden, asyncio.TimeoutError) as e: - logger.error(f"Failed to publish message in {message.channel.id}: {e}", exc_info=True) + except asyncio.TimeoutError: + logger.error(f"Timed out publishing message in {message.channel.id}") + except discord.Forbidden as e: + logger.error( + f"Missing permissions to publish in {message.channel.id}: {e}", exc_info=True + ) + except discord.HTTPException as e: + if e.status == 429: + logger.warning( + f"Rate limited when publishing in {message.channel.id}. " + "Discord limits 10 publishes/hour per channel." + ) + else: + logger.error( + f"Failed to publish message in {message.channel.id}: {e}", exc_info=True + ) @commands.guild_only() @commands.admin_or_permissions(manage_guild=True) @@ -283,15 +292,6 @@ async def resetcount(self, ctx: commands.Context) -> None: await view.wait() if view.result: await self.config.clear_all_globals() - # Re-register defaults - await self.config.register_global( - published_count=0, - published_weekly_count=0, - published_monthly_count=0, - published_yearly_count=0, - last_count_time=None, - timezone="UTC", - ) await ctx.send("Counts reset.") else: await ctx.send("Reset cancelled.") diff --git a/autopublisher/info.json b/autopublisher/info.json index b9a55983..31db335a 100644 --- a/autopublisher/info.json +++ b/autopublisher/info.json @@ -11,7 +11,8 @@ "tags": [ "news", "autopublish", - "autopublisher" + "autopublisher", + "Dashboard Integrated" ], "permissions": [ "manage_messages", diff --git a/autopublisher/utils.py b/autopublisher/utils.py index dc492e45..958f900a 100644 --- a/autopublisher/utils.py +++ b/autopublisher/utils.py @@ -46,12 +46,13 @@ async def get_owner_timezone(config: Config) -> pytz.timezone: async def initialize_scheduler(cog: commands.Cog) -> AsyncIOScheduler: """Initialize the scheduler.""" - scheduler = AsyncIOScheduler() try: + scheduler = AsyncIOScheduler() logger.info("Scheduler initialized successfully.") + return scheduler except Exception as e: logger.error(f"Failed to initialize scheduler: {e}", exc_info=True) - return scheduler + raise async def schedule_resets(cog: commands.Cog) -> None: @@ -133,9 +134,9 @@ def get_next_reset_times(owner_tz: pytz.timezone) -> tuple[int, int, int]: # Weekly reset: Next Sunday days_until_sunday = (6 - now.weekday()) % 7 - if days_until_sunday == 0 and now.hour >= 0: + if days_until_sunday == 0: days_until_sunday = 7 - next_weekly = datetime( + next_weekly_naive = datetime( year=now.year, month=now.month, day=now.day, @@ -144,35 +145,20 @@ def get_next_reset_times(owner_tz: pytz.timezone) -> tuple[int, int, int]: second=0, microsecond=0, ) + timedelta(days=days_until_sunday) - next_weekly = owner_tz.localize(next_weekly) + next_weekly = owner_tz.normalize(owner_tz.localize(next_weekly_naive, is_dst=False)) next_weekly_ts = int(next_weekly.timestamp()) # Monthly reset: First day of next month - next_month = datetime( - year=now.year, - month=now.month, - day=1, - hour=0, - minute=0, - second=0, - microsecond=0, - ) + timedelta(days=32) - next_month = next_month.replace(day=1) - next_month = owner_tz.localize(next_month) + if now.month == 12: + next_month_naive = datetime(year=now.year + 1, month=1, day=1) + else: + next_month_naive = datetime(year=now.year, month=now.month + 1, day=1) + next_month = owner_tz.normalize(owner_tz.localize(next_month_naive, is_dst=False)) next_monthly_ts = int(next_month.timestamp()) # Yearly reset: January 1st of next year - next_year = now.year + 1 if now.month > 1 or (now.month == 1 and now.day > 1) else now.year - next_yearly = datetime( - year=next_year, - month=1, - day=1, - hour=0, - minute=0, - second=0, - microsecond=0, - ) - next_yearly = owner_tz.localize(next_yearly) + next_yearly_naive = datetime(year=now.year + 1, month=1, day=1) + next_yearly = owner_tz.normalize(owner_tz.localize(next_yearly_naive, is_dst=False)) next_yearly_ts = int(next_yearly.timestamp()) return next_weekly_ts, next_monthly_ts, next_yearly_ts diff --git a/autopublisher/view.py b/autopublisher/view.py index 81e00164..41c602cd 100644 --- a/autopublisher/view.py +++ b/autopublisher/view.py @@ -60,6 +60,7 @@ def __init__(self, cog: commands.Cog) -> None: channel_types=[discord.ChannelType.news], placeholder="Select the news channels to ignore.", min_values=0, + max_values=25, ) self.select.callback = self.select_callback self.container.add_item(discord.ui.ActionRow(self.select)) @@ -128,6 +129,10 @@ async def save_callback(self, interaction: discord.Interaction) -> None: ) await self.cog.config.guild(self.ctx.guild).ignored_channels.set(new_ignored_channels) + self.select.default_values = [ + discord.SelectDefaultValue(type="channel", id=channel.id) + for channel in self.ignored_channels + ] await interaction.response.send_message( ":white_check_mark: Ignored discord news channel(s) saved!", ephemeral=True ) @@ -142,11 +147,15 @@ async def unignore_callback(self, interaction: discord.Interaction) -> None: ) await self.cog.config.guild(self.ctx.guild).ignored_channels.set([]) + self.ignored_channels = [] + self.select.default_values = [] await interaction.response.send_message( ":white_check_mark: Ignored discord news channel(s) removed!", ephemeral=True ) - self.ignored_channels = [] - self.select.default_values = [] + try: + await self.message.edit(view=self) + except discord.HTTPException as e: + log.error(f"Failed to update view after unignore: {e}") class StatsView(discord.ui.LayoutView): @@ -166,18 +175,7 @@ def __init__(self, cog: commands.Cog) -> None: label="Close", style=discord.ButtonStyle.red, emoji="✖️" ) self.close_button.callback = self.close_callback - self._build_container("Never") - - def _build_container(self, last_published: str) -> None: - """Build or rebuild the container with current components.""" - self.container = discord.ui.Container(accent_color=discord.Color.blurple()) - self.container.add_item(discord.ui.TextDisplay("AutoPublisher Statistics")) - self.container.add_item(discord.ui.Separator()) - self.container.add_item(discord.ui.TextDisplay(last_published)) - self.container.add_item(discord.ui.Separator()) - self.container.add_item(discord.ui.ActionRow(self.refresh_button, self.close_button)) - self.clear_items() - self.add_item(self.container) + self.container: discord.ui.Container | None = None async def start(self, ctx: commands.Context) -> None: """Initialize the view with stats data.""" @@ -241,6 +239,8 @@ async def on_timeout(self) -> None: """Handle view timeout.""" self.refresh_button.disabled = True self.close_button.disabled = True + if self.message is None: + return try: await self.message.edit(view=self) except discord.HTTPException as e: From 680c9c5cfeebe50fd603534d4be169b71b8eec60 Mon Sep 17 00:00:00 2001 From: Evanroby Date: Wed, 18 Mar 2026 22:15:03 +0100 Subject: [PATCH 2/2] Update view.py --- autopublisher/view.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/autopublisher/view.py b/autopublisher/view.py index 41c602cd..039779d9 100644 --- a/autopublisher/view.py +++ b/autopublisher/view.py @@ -175,7 +175,18 @@ def __init__(self, cog: commands.Cog) -> None: label="Close", style=discord.ButtonStyle.red, emoji="✖️" ) self.close_button.callback = self.close_callback - self.container: discord.ui.Container | None = None + self._build_container("Never") + + def _build_container(self, last_published: str) -> None: + """Build or rebuild the container with current components.""" + self.container = discord.ui.Container(accent_color=discord.Color.blurple()) + self.container.add_item(discord.ui.TextDisplay("AutoPublisher Statistics")) + self.container.add_item(discord.ui.Separator()) + self.container.add_item(discord.ui.TextDisplay(last_published)) + self.container.add_item(discord.ui.Separator()) + self.container.add_item(discord.ui.ActionRow(self.refresh_button, self.close_button)) + self.clear_items() + self.add_item(self.container) async def start(self, ctx: commands.Context) -> None: """Initialize the view with stats data."""