Skip to content
Open
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
1 change: 1 addition & 0 deletions bot/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
COGS = [
"bot.cogs.youtube_watcher",
"bot.cogs.blog_watcher",
"bot.cogs.digest",
]


Expand Down
55 changes: 10 additions & 45 deletions bot/cogs/blog_watcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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."


# ──────────────────────────────────────────────────────────────────────────────
Expand Down
61 changes: 61 additions & 0 deletions bot/cogs/digest.py
Original file line number Diff line number Diff line change
@@ -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))
57 changes: 12 additions & 45 deletions bot/cogs/youtube_watcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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."


# ──────────────────────────────────────────────────────────────────────────────
Expand Down