Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion LOCAL_SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**.

Expand Down
6 changes: 6 additions & 0 deletions bot/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ async def setup_hook(self) -> None:
logger.exception("Failed to load required cog %s during startup", cog_path)
raise

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."""
logger.info(
Expand Down
74 changes: 74 additions & 0 deletions bot/cogs/blog_watcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -167,6 +168,79 @@ 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:
Comment thread
MetzinAround marked this conversation as resolved.
"""Slash command to trigger the blog digest immediately."""
await interaction.response.defer(ephemeral=True)

try:
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,
)
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,
)


# ──────────────────────────────────────────────────────────────────────────────
# Helpers
Expand Down
70 changes: 70 additions & 0 deletions bot/cogs/youtube_watcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -174,6 +175,75 @@ 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:
Comment thread
MetzinAround marked this conversation as resolved.
"""Slash command to trigger the YouTube digest immediately."""
await interaction.response.defer(ephemeral=True)

try:
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,
)
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,
)


# ──────────────────────────────────────────────────────────────────────────────
# Helpers
Expand Down
Loading