From 461cc080cb2aa1bc518d064e4438cfa6bd683fa7 Mon Sep 17 00:00:00 2001 From: Pj Metz Date: Thu, 14 May 2026 10:57:55 -0700 Subject: [PATCH 1/5] feat: add /blogdigest and /youtubedigest slash commands for manual triggering - Add /blogdigest slash command to BlogWatcher cog - Add /youtubedigest slash command to YouTubeWatcher cog - Both commands require manage_guild permission and respond ephemerally - Sync slash command tree in setup_hook on startup Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bot/bot.py | 3 ++ bot/cogs/blog_watcher.py | 60 +++++++++++++++++++++++++++++++++++ bot/cogs/youtube_watcher.py | 62 +++++++++++++++++++++++++++++++++++++ 3 files changed, 125 insertions(+) diff --git a/bot/bot.py b/bot/bot.py index 83066a8..da6a7e2 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -53,6 +53,9 @@ async def setup_hook(self) -> None: logger.exception("Failed to load required cog %s during startup", cog_path) raise + await self.tree.sync() + logger.info("Slash commands synced.") + async def on_ready(self) -> None: """Called when the bot has successfully connected to Discord.""" logger.info( diff --git a/bot/cogs/blog_watcher.py b/bot/cogs/blog_watcher.py index 32f1a89..03244ad 100644 --- a/bot/cogs/blog_watcher.py +++ b/bot/cogs/blog_watcher.py @@ -32,6 +32,7 @@ from typing import List import discord +from discord import app_commands from discord.ext import commands, tasks from utils.blog_fetcher import BlogFetcher @@ -167,6 +168,65 @@ async def before_weekly_digest(self) -> None: """Wait until the bot is fully connected before the first check.""" await self.bot.wait_until_ready() + # ------------------------------------------------------------------ + # 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) + now = datetime.now(tz=timezone.utc) + since = now - timedelta(days=7) + + posts = await asyncio.to_thread( + self.blog_client.get_posts_since_by_keywords, + since=since, + keywords=self.keywords, + max_results=self.digest_count, + search_pool=self.search_pool, + ) + + 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 + except discord.Forbidden: + await interaction.followup.send( + f"❌ Channel ID `{self.discord_channel_id}` is not accessible — check bot permissions.", + ephemeral=True, + ) + return + except discord.HTTPException as exc: + await interaction.followup.send( + f"❌ Failed to fetch channel: {exc}", + ephemeral=True, + ) + return + + if not posts: + await interaction.followup.send( + "⚠️ No matching blog posts found for the past 7 days.", + ephemeral=True, + ) + return + + 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, + ) + # ────────────────────────────────────────────────────────────────────────────── # Helpers diff --git a/bot/cogs/youtube_watcher.py b/bot/cogs/youtube_watcher.py index 77f9243..16a6c0d 100644 --- a/bot/cogs/youtube_watcher.py +++ b/bot/cogs/youtube_watcher.py @@ -37,6 +37,7 @@ from typing import List import discord +from discord import app_commands from discord.ext import commands, tasks from utils.state import load_state, save_state @@ -174,6 +175,67 @@ async def before_weekly_digest(self) -> None: """Wait until the bot is fully connected before the first check.""" await self.bot.wait_until_ready() + # ------------------------------------------------------------------ + # 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) + now = datetime.now(tz=timezone.utc) + today_midnight = now.replace(hour=0, minute=0, second=0, microsecond=0) + since = today_midnight - timedelta(days=7) + + videos = await asyncio.to_thread( + self.yt_client.get_top_videos_by_keywords, + channel_id=self.yt_channel_id, + keywords=self.keywords, + published_after=since, + top_n=self.digest_count, + search_pool=self.search_pool, + ) + + 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 + except discord.Forbidden: + await interaction.followup.send( + f"❌ Channel ID `{self.discord_channel_id}` is not accessible — check bot permissions.", + ephemeral=True, + ) + return + except discord.HTTPException as exc: + await interaction.followup.send( + f"❌ Failed to fetch channel: {exc}", + ephemeral=True, + ) + return + + if not videos: + await interaction.followup.send( + "⚠️ No matching YouTube videos found for the past 7 days.", + ephemeral=True, + ) + return + + embed = _build_digest_embed(videos, self.keywords, 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, + ) + # ────────────────────────────────────────────────────────────────────────────── # Helpers From 21897ba57c3d658f9991a692001416b7c919639d Mon Sep 17 00:00:00 2001 From: Pj Metz <65838556+MetzinAround@users.noreply.github.com> Date: Thu, 14 May 2026 11:19:04 -0700 Subject: [PATCH 2/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- bot/cogs/youtube_watcher.py | 86 ++++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 39 deletions(-) diff --git a/bot/cogs/youtube_watcher.py b/bot/cogs/youtube_watcher.py index 16a6c0d..5e092ac 100644 --- a/bot/cogs/youtube_watcher.py +++ b/bot/cogs/youtube_watcher.py @@ -187,54 +187,62 @@ async def before_weekly_digest(self) -> None: async def youtubedigest(self, interaction: discord.Interaction) -> None: """Slash command to trigger the YouTube digest immediately.""" await interaction.response.defer(ephemeral=True) - now = datetime.now(tz=timezone.utc) - today_midnight = now.replace(hour=0, minute=0, second=0, microsecond=0) - since = today_midnight - timedelta(days=7) - - videos = await asyncio.to_thread( - self.yt_client.get_top_videos_by_keywords, - channel_id=self.yt_channel_id, - keywords=self.keywords, - published_after=since, - top_n=self.digest_count, - search_pool=self.search_pool, - ) 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 - except discord.Forbidden: - await interaction.followup.send( - f"❌ Channel ID `{self.discord_channel_id}` is not accessible — check bot permissions.", - ephemeral=True, + now = datetime.now(tz=timezone.utc) + today_midnight = now.replace(hour=0, minute=0, second=0, microsecond=0) + since = today_midnight - timedelta(days=7) + + videos = await asyncio.to_thread( + self.yt_client.get_top_videos_by_keywords, + channel_id=self.yt_channel_id, + keywords=self.keywords, + published_after=since, + top_n=self.digest_count, + search_pool=self.search_pool, ) - return - except discord.HTTPException as exc: + + 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 + except discord.Forbidden: + await interaction.followup.send( + f"❌ Channel ID `{self.discord_channel_id}` is not accessible — check bot permissions.", + ephemeral=True, + ) + return + except discord.HTTPException as exc: + await interaction.followup.send( + f"❌ Failed to fetch channel: {exc}", + ephemeral=True, + ) + return + + if not videos: + await interaction.followup.send( + "⚠️ No matching YouTube videos found for the past 7 days.", + ephemeral=True, + ) + return + + embed = _build_digest_embed(videos, self.keywords, 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"❌ Failed to fetch channel: {exc}", + f"✅ YouTube digest posted to <#{self.discord_channel_id}> ({len(videos)} video(s)).", ephemeral=True, ) - return - - if not videos: + except Exception: + logger.exception("Manual YouTube digest failed for %s.", interaction.user) await interaction.followup.send( - "⚠️ No matching YouTube videos found for the past 7 days.", + "❌ Failed to run the YouTube digest due to an unexpected error. Please check the logs and try again.", ephemeral=True, ) - return - - embed = _build_digest_embed(videos, self.keywords, 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, - ) # ────────────────────────────────────────────────────────────────────────────── From 36b8606da0ad7825dfc661419c52328a57235ac7 Mon Sep 17 00:00:00 2001 From: Pj Metz <65838556+MetzinAround@users.noreply.github.com> Date: Thu, 14 May 2026 11:19:34 -0700 Subject: [PATCH 3/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- bot/cogs/blog_watcher.py | 92 +++++++++++++++++++++++----------------- 1 file changed, 53 insertions(+), 39 deletions(-) diff --git a/bot/cogs/blog_watcher.py b/bot/cogs/blog_watcher.py index 03244ad..cdf4fde 100644 --- a/bot/cogs/blog_watcher.py +++ b/bot/cogs/blog_watcher.py @@ -180,52 +180,66 @@ async def before_weekly_digest(self) -> None: async def blogdigest(self, interaction: discord.Interaction) -> None: """Slash command to trigger the blog digest immediately.""" await interaction.response.defer(ephemeral=True) - now = datetime.now(tz=timezone.utc) - since = now - timedelta(days=7) - - posts = await asyncio.to_thread( - self.blog_client.get_posts_since_by_keywords, - since=since, - keywords=self.keywords, - max_results=self.digest_count, - search_pool=self.search_pool, - ) 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 - except discord.Forbidden: - await interaction.followup.send( - f"❌ Channel ID `{self.discord_channel_id}` is not accessible — check bot permissions.", - ephemeral=True, - ) - return - except discord.HTTPException as exc: - await interaction.followup.send( - f"❌ Failed to fetch channel: {exc}", - ephemeral=True, + now = datetime.now(tz=timezone.utc) + since = now - timedelta(days=7) + + posts = await asyncio.to_thread( + self.blog_client.get_posts_since_by_keywords, + since=since, + keywords=self.keywords, + max_results=self.digest_count, + search_pool=self.search_pool, ) - return - if not posts: + 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 + except discord.Forbidden: + await interaction.followup.send( + f"❌ Channel ID `{self.discord_channel_id}` is not accessible — check bot permissions.", + ephemeral=True, + ) + return + except discord.HTTPException as exc: + await interaction.followup.send( + f"❌ Failed to fetch channel: {exc}", + ephemeral=True, + ) + return + + if not posts: + await interaction.followup.send( + "⚠️ No matching blog posts found for the past 7 days.", + ephemeral=True, + ) + return + + 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( - "⚠️ No matching blog posts found for the past 7 days.", + f"✅ Blog digest posted to <#{self.discord_channel_id}> ({len(posts)} post(s)).", ephemeral=True, ) - return - - 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, - ) + 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, + ) # ────────────────────────────────────────────────────────────────────────────── From 3b941abab8f0a531a5a07bfafefe9d21c4d90fcd Mon Sep 17 00:00:00 2001 From: Pj Metz <65838556+MetzinAround@users.noreply.github.com> Date: Thu, 14 May 2026 11:19:46 -0700 Subject: [PATCH 4/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- bot/bot.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bot/bot.py b/bot/bot.py index da6a7e2..dc59698 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -53,8 +53,11 @@ async def setup_hook(self) -> None: logger.exception("Failed to load required cog %s during startup", cog_path) raise - await self.tree.sync() - logger.info("Slash commands synced.") + try: + await self.tree.sync() + logger.info("Slash commands synced.") + except discord.HTTPException: + logger.exception("Failed to sync slash commands during startup; continuing without command sync.") async def on_ready(self) -> None: """Called when the bot has successfully connected to Discord.""" From c4c702d12f42a1d96ff47583a24c52ad8425bfec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 18:24:53 +0000 Subject: [PATCH 5/5] Update setup scopes for slash commands --- LOCAL_SETUP.md | 2 +- SETUP.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LOCAL_SETUP.md b/LOCAL_SETUP.md index 2609279..4abda93 100644 --- a/LOCAL_SETUP.md +++ b/LOCAL_SETUP.md @@ -14,7 +14,7 @@ You need: 4. Under **Token**, generate/copy the token (`DISCORD_TOKEN`). 5. Invite the bot to your server: - Open **OAuth2 → URL Generator** - - Scopes: `bot` + - Scopes: `bot`, `applications.commands` - Bot permissions: `Send Messages`, `Embed Links` - Open the generated URL and authorize the bot for your server. 6. In Discord, enable **Developer Mode**, then copy the IDs for the channels where digests should post. diff --git a/SETUP.md b/SETUP.md index 0c6b702..ee2bdd4 100644 --- a/SETUP.md +++ b/SETUP.md @@ -79,7 +79,7 @@ A Discord bot that watches the **GitHub YouTube channel** and the **GitHub Blog* 6. **Invite the bot to your server:** - In the sidebar click **OAuth2 → URL Generator**. - - Under *Scopes* tick **bot**. + - Under *Scopes* tick **bot** and **applications.commands**. - Under *Bot Permissions* tick **Send Messages** and **Embed Links**. - Copy the generated URL, open it in a browser, select your server, and click **Authorise**.