diff --git a/bot/bot.py b/bot/bot.py index dc59698..75f3534 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -23,6 +23,7 @@ COGS = [ "bot.cogs.youtube_watcher", "bot.cogs.blog_watcher", + "bot.cogs.digest", ] diff --git a/bot/cogs/blog_watcher.py b/bot/cogs/blog_watcher.py index cdf4fde..9e1e4d1 100644 --- a/bot/cogs/blog_watcher.py +++ b/bot/cogs/blog_watcher.py @@ -172,15 +172,8 @@ async def before_weekly_digest(self) -> None: # Slash command – manual trigger # ------------------------------------------------------------------ - @app_commands.command( - name="blogdigest", - description="Manually run the GitHub Blog weekly digest right now.", - ) - @app_commands.default_permissions(manage_guild=True) - async def blogdigest(self, interaction: discord.Interaction) -> None: - """Slash command to trigger the blog digest immediately.""" - await interaction.response.defer(ephemeral=True) - + async def trigger_manual_digest(self, user: discord.abc.User | discord.Member) -> tuple[bool, str]: + """Helper to trigger the blog digest immediately (called by DigestCommand).""" try: now = datetime.now(tz=timezone.utc) since = now - timedelta(days=7) @@ -196,50 +189,22 @@ async def blogdigest(self, interaction: discord.Interaction) -> None: try: channel = await self.bot.fetch_channel(self.discord_channel_id) except discord.NotFound: - await interaction.followup.send( - f"❌ Channel ID `{self.discord_channel_id}` not found — check `config.yaml`.", - ephemeral=True, - ) - return + return False, f"❌ Blog Channel ID `{self.discord_channel_id}` not found — check `config.yaml`." except discord.Forbidden: - await interaction.followup.send( - f"❌ Channel ID `{self.discord_channel_id}` is not accessible — check bot permissions.", - ephemeral=True, - ) - return + return False, f"❌ Blog Channel ID `{self.discord_channel_id}` is not accessible — check bot permissions." except discord.HTTPException as exc: - await interaction.followup.send( - f"❌ Failed to fetch channel: {exc}", - ephemeral=True, - ) - return + return False, f"❌ Failed to fetch Blog channel: {exc}" if not posts: - await interaction.followup.send( - "⚠️ No matching blog posts found for the past 7 days.", - ephemeral=True, - ) - return + return True, "⚠️ No matching blog posts found for the past 7 days." embed = _build_digest_embed(posts, self.keywords, since, now) await channel.send(embed=embed) - logger.info("Manual blog digest posted by %s: %d post(s).", interaction.user, len(posts)) - await interaction.followup.send( - f"✅ Blog digest posted to <#{self.discord_channel_id}> ({len(posts)} post(s)).", - ephemeral=True, - ) + logger.info("Manual blog digest posted by %s: %d post(s).", user, len(posts)) + return True, f"✅ Blog digest posted to <#{self.discord_channel_id}> ({len(posts)} post(s))." except Exception: - logger.exception("Manual blog digest failed for %s.", interaction.user) - try: - await interaction.followup.send( - "❌ Failed to run the blog digest. Please try again later and check the bot logs.", - ephemeral=True, - ) - except discord.HTTPException: - logger.exception( - "Failed to send manual blog digest error response to %s.", - interaction.user, - ) + logger.exception("Manual blog digest failed for %s.", user) + return False, "❌ Failed to run the blog digest due to an unexpected error." # ────────────────────────────────────────────────────────────────────────────── diff --git a/bot/cogs/digest.py b/bot/cogs/digest.py new file mode 100644 index 0000000..775111b --- /dev/null +++ b/bot/cogs/digest.py @@ -0,0 +1,61 @@ +import logging +import discord +from discord import app_commands +from discord.ext import commands + +logger = logging.getLogger(__name__) + +class DigestCommand(commands.Cog): + """Cog containing the unified /digest manual trigger command.""" + + def __init__(self, bot: commands.Bot) -> None: + self.bot = bot + + @app_commands.command( + name="digest", + description="Manually run the content digests right now.", + ) + @app_commands.describe( + source="Which digest to run (all, youtube, blog)" + ) + @app_commands.choices(source=[ + app_commands.Choice(name="All Sources", value="all"), + app_commands.Choice(name="YouTube Only", value="youtube"), + app_commands.Choice(name="Blog Only", value="blog"), + ]) + @app_commands.default_permissions(manage_guild=True) + async def digest(self, interaction: discord.Interaction, source: app_commands.Choice[str] = None) -> None: + """Slash command to trigger the digests immediately.""" + choice = source.value if source else "all" + + youtube_cog = self.bot.get_cog("YouTubeWatcher") + blog_cog = self.bot.get_cog("BlogWatcher") + + await interaction.response.defer(ephemeral=True) + + responses = [] + + if choice in ("all", "youtube"): + if youtube_cog: + _, msg = await youtube_cog.trigger_manual_digest(interaction.user) + responses.append(msg) + else: + responses.append("❌ YouTubeWatcher cog is not loaded.") + + if choice in ("all", "blog"): + if blog_cog: + _, msg = await blog_cog.trigger_manual_digest(interaction.user) + responses.append(msg) + else: + responses.append("❌ BlogWatcher cog is not loaded.") + + if not responses: + await interaction.followup.send("No actions were taken.", ephemeral=True) + return + + final_msg = "\n".join(responses) + await interaction.followup.send(final_msg, ephemeral=True) + +# Required by discord.py to load the cog. +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(DigestCommand(bot)) diff --git a/bot/cogs/youtube_watcher.py b/bot/cogs/youtube_watcher.py index 87bb846..8faddc7 100644 --- a/bot/cogs/youtube_watcher.py +++ b/bot/cogs/youtube_watcher.py @@ -165,15 +165,8 @@ async def before_weekly_digest(self) -> None: # Slash command – manual trigger # ------------------------------------------------------------------ - @app_commands.command( - name="youtubedigest", - description="Manually run the GitHub YouTube weekly digest right now.", - ) - @app_commands.default_permissions(manage_guild=True) - async def youtubedigest(self, interaction: discord.Interaction) -> None: - """Slash command to trigger the YouTube digest immediately.""" - await interaction.response.defer(ephemeral=True) - + async def trigger_manual_digest(self, user: discord.abc.User | discord.Member) -> tuple[bool, str]: + """Helper to trigger the YouTube digest immediately (called by DigestCommand).""" try: now = datetime.now(tz=timezone.utc) today_midnight = now.replace(hour=0, minute=0, second=0, microsecond=0) @@ -190,51 +183,25 @@ async def youtubedigest(self, interaction: discord.Interaction) -> None: try: channel = await self.bot.fetch_channel(self.discord_channel_id) except discord.NotFound: - await interaction.followup.send( - f"❌ Channel ID `{self.discord_channel_id}` not found — check `config.yaml`.", - ephemeral=True, - ) - return + return False, f"❌ YouTube Channel ID `{self.discord_channel_id}` not found — check `config.yaml`." except discord.Forbidden: - await interaction.followup.send( - f"❌ Channel ID `{self.discord_channel_id}` is not accessible — check bot permissions.", - ephemeral=True, - ) - return + return False, f"❌ YouTube Channel ID `{self.discord_channel_id}` is not accessible — check bot permissions." except discord.HTTPException as exc: - await interaction.followup.send( - f"❌ Failed to fetch channel: {exc}", - ephemeral=True, - ) - return + return False, f"❌ Failed to fetch YouTube channel: {exc}" if not videos: - await interaction.followup.send( - "⚠️ No recent YouTube videos found for the past 7 days.", - ephemeral=True, - ) - return + return True, "⚠️ No recent YouTube videos found for the past 7 days." embed = _build_digest_embed(videos, since, now) await channel.send(embed=embed) - logger.info("Manual YouTube digest posted by %s: %d video(s).", interaction.user, len(videos)) - await interaction.followup.send( - f"✅ YouTube digest posted to <#{self.discord_channel_id}> ({len(videos)} video(s)).", - ephemeral=True, - ) + logger.info("Manual YouTube digest posted by %s: %d video(s).", user, len(videos)) + return True, f"✅ YouTube digest posted to <#{self.discord_channel_id}> ({len(videos)} video(s))." except HttpError as exc: - logger.error("YouTube API error during manual digest for %s: %s", interaction.user, exc) - await interaction.followup.send( - f"❌ YouTube API error (status {exc.status_code}): {exc.reason}\n" - "This is usually a quota issue or invalid API key — check the logs and your Google Cloud console.", - ephemeral=True, - ) + logger.error("YouTube API error during manual digest for %s: %s", user, exc) + return False, f"❌ YouTube API error (status {exc.status_code}): {exc.reason}\nThis is usually a quota issue or invalid API key." except Exception: - logger.exception("Manual YouTube digest failed for %s.", interaction.user) - await interaction.followup.send( - "❌ Failed to run the YouTube digest due to an unexpected error. Please check the logs and try again.", - ephemeral=True, - ) + logger.exception("Manual YouTube digest failed for %s.", user) + return False, "❌ Failed to run the YouTube digest due to an unexpected error." # ──────────────────────────────────────────────────────────────────────────────