From 6325b2534d7487ba67c30b36d378f6bf09efa969 Mon Sep 17 00:00:00 2001 From: Alia Meek Date: Thu, 28 Aug 2025 16:39:55 +0100 Subject: [PATCH 1/4] Added XKCD search by keyword feature --- cogs/commands/xkcd.py | 58 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/cogs/commands/xkcd.py b/cogs/commands/xkcd.py index b47e9c3..01fab03 100644 --- a/cogs/commands/xkcd.py +++ b/cogs/commands/xkcd.py @@ -1,7 +1,10 @@ +import datetime import json import random +import re -from discord.ext import commands +import requests +from discord.ext import commands, tasks from discord.ext.commands import Bot, Context import utils @@ -10,6 +13,7 @@ For all your xkcd needs Use /xkcd to gets the image of a comic with a specific ID. +Use /xkcd_search to search for a comic by title. Or just use /xkcd to get a random comic. If an invalid arguement is made a random comic is returned """ @@ -20,6 +24,7 @@ class XKCD(commands.Cog): def __init__(self, bot: Bot): self.bot = bot + self.comics = None @commands.hybrid_command(help=LONG_HELP_TEXT, brief=SHORT_HELP_TEXT) async def xkcd(self, ctx: Context, comic_id: int | None = None): @@ -50,6 +55,25 @@ async def xkcd(self, ctx: Context, comic_id: int | None = None): comic_title = comic_json["safe_title"] msg = f"**{comic_title}**, available at " await ctx.reply(msg, file=comic_img) + + @commands.hybrid_command(help=LONG_HELP_TEXT, brief=SHORT_HELP_TEXT) + async def xkcd_search(self, ctx: Context, query: str): + """searches for a comic by title""" + + if not self.comics: + self.comics = await self.get_all_comics() + if not self.comics: + return await ctx.reply("Error: could not get comics list", ephemeral=True) + + results = [f"{title} ({comic_id})" for comic_id, title in self.comics.items() if query.lower() in title.lower()] + + if not results: + return await ctx.reply(f"No comics found with title containing '{query}'", ephemeral=True) + + ret_str = f"Found {len(results)} comics with title containing '{query}':\n" + "\n".join(results) + + return await ctx.reply(ret_str, ephemeral=True) + async def get_recent_comic(self) -> int | None: """gets the most recent comic id""" @@ -57,6 +81,38 @@ async def get_recent_comic(self) -> int | None: if xkcd_response: return xkcd_response["num"] return None + + async def get_all_comics(self) -> dict[int, str] | None: + """gets a dictionary of all comic ids and their titles""" + + pattern = re.compile(r']*>(.*?)') + + https_response = requests.get("https://xkcd.com/archive/") + if https_response.status_code != 200: + raise SystemExit + + html_text = https_response.text + lines = [line for line in html_text.splitlines() if line != ''] + results = [pattern.findall(item) for item in lines] + + # flatten results since findall returns list of tuples + results = [match for sub in results for match in sub] + + comics = {int(comic_id): title for comic_id, title in results} + + return comics + + @tasks.loop(time=datetime.time(hour=4, minute=0, tzinfo=datetime.timezone.utc)) + async def update_comics(self): + """updates the comics dictionary daily""" + + xkcd_response = await utils.get_json_from_url("https://xkcd.com/info.0.json") + if not xkcd_response: + return None + max_comic_id = sorted(self.comics.keys())[-1] + + if max_comic_id < xkcd_response["num"]: + self.comics[xkcd_response["num"]] = xkcd_response["safe_title"] async def setup(bot: Bot): From f26697a21e2865ef32e3379c5332ec0ee273d00f Mon Sep 17 00:00:00 2001 From: Alia Meek Date: Thu, 28 Aug 2025 16:46:49 +0100 Subject: [PATCH 2/4] Replace the SystemExit, added comments --- cogs/commands/xkcd.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cogs/commands/xkcd.py b/cogs/commands/xkcd.py index 01fab03..ede3488 100644 --- a/cogs/commands/xkcd.py +++ b/cogs/commands/xkcd.py @@ -60,13 +60,17 @@ async def xkcd(self, ctx: Context, comic_id: int | None = None): async def xkcd_search(self, ctx: Context, query: str): """searches for a comic by title""" + # Load comics if not already loaded if not self.comics: self.comics = await self.get_all_comics() + # Okay something went wrong if not self.comics: return await ctx.reply("Error: could not get comics list", ephemeral=True) + # Search for query in titles results = [f"{title} ({comic_id})" for comic_id, title in self.comics.items() if query.lower() in title.lower()] + # Return results if not results: return await ctx.reply(f"No comics found with title containing '{query}'", ephemeral=True) @@ -85,11 +89,12 @@ async def get_recent_comic(self) -> int | None: async def get_all_comics(self) -> dict[int, str] | None: """gets a dictionary of all comic ids and their titles""" + # Pattern to match lines giving comic id and title pattern = re.compile(r']*>(.*?)') https_response = requests.get("https://xkcd.com/archive/") if https_response.status_code != 200: - raise SystemExit + return None html_text = https_response.text lines = [line for line in html_text.splitlines() if line != ''] @@ -98,6 +103,7 @@ async def get_all_comics(self) -> dict[int, str] | None: # flatten results since findall returns list of tuples results = [match for sub in results for match in sub] + # Create dictionary from list of tuples comics = {int(comic_id): title for comic_id, title in results} return comics From b79ae2453feb7034c21aa5004076a6239efa77cc Mon Sep 17 00:00:00 2001 From: Alia Meek Date: Thu, 28 Aug 2025 16:54:45 +0100 Subject: [PATCH 3/4] Made search a subcommand --- cogs/commands/xkcd.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cogs/commands/xkcd.py b/cogs/commands/xkcd.py index ede3488..e3c479f 100644 --- a/cogs/commands/xkcd.py +++ b/cogs/commands/xkcd.py @@ -13,7 +13,7 @@ For all your xkcd needs Use /xkcd to gets the image of a comic with a specific ID. -Use /xkcd_search to search for a comic by title. +Use /xkcd search to search for a comic by title. Or just use /xkcd to get a random comic. If an invalid arguement is made a random comic is returned """ @@ -26,7 +26,7 @@ def __init__(self, bot: Bot): self.bot = bot self.comics = None - @commands.hybrid_command(help=LONG_HELP_TEXT, brief=SHORT_HELP_TEXT) + @commands.hybrid_group(help=LONG_HELP_TEXT, brief=SHORT_HELP_TEXT) async def xkcd(self, ctx: Context, comic_id: int | None = None): """gets either a random comic or a specific one""" max_comic_id = await self.get_recent_comic() # gets the most recent comic's id @@ -56,8 +56,8 @@ async def xkcd(self, ctx: Context, comic_id: int | None = None): msg = f"**{comic_title}**, available at " await ctx.reply(msg, file=comic_img) - @commands.hybrid_command(help=LONG_HELP_TEXT, brief=SHORT_HELP_TEXT) - async def xkcd_search(self, ctx: Context, query: str): + @xkcd.command(help=LONG_HELP_TEXT, brief=SHORT_HELP_TEXT) + async def search(self, ctx: Context, query: str): """searches for a comic by title""" # Load comics if not already loaded From dae640af37a12ce5c89edc6a6220196234a2ca77 Mon Sep 17 00:00:00 2001 From: Alia Meek Date: Thu, 28 Aug 2025 17:09:28 +0100 Subject: [PATCH 4/4] Made requested changes --- cogs/commands/xkcd.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/cogs/commands/xkcd.py b/cogs/commands/xkcd.py index e3c479f..ca889ec 100644 --- a/cogs/commands/xkcd.py +++ b/cogs/commands/xkcd.py @@ -65,18 +65,18 @@ async def search(self, ctx: Context, query: str): self.comics = await self.get_all_comics() # Okay something went wrong if not self.comics: - return await ctx.reply("Error: could not get comics list", ephemeral=True) + return await ctx.reply("Error: could not get comics list") # Search for query in titles results = [f"{title} ({comic_id})" for comic_id, title in self.comics.items() if query.lower() in title.lower()] # Return results if not results: - return await ctx.reply(f"No comics found with title containing '{query}'", ephemeral=True) + return await ctx.reply(f"No comics found with title containing '{query}'") ret_str = f"Found {len(results)} comics with title containing '{query}':\n" + "\n".join(results) - return await ctx.reply(ret_str, ephemeral=True) + return await ctx.reply(ret_str) async def get_recent_comic(self) -> int | None: @@ -117,8 +117,16 @@ async def update_comics(self): return None max_comic_id = sorted(self.comics.keys())[-1] - if max_comic_id < xkcd_response["num"]: - self.comics[xkcd_response["num"]] = xkcd_response["safe_title"] + # No new comics + if xkcd_response["num"] == max_comic_id: + return + + # Add any new comics since last update + self.comics[xkcd_response["num"]] = xkcd_response["safe_title"] + for comic_id in range(max_comic_id + 1, xkcd_response["num"]): + comic_response = await utils.get_json_from_url(f"https://xkcd.com/{comic_id}/info.0.json") + if comic_response: + self.comics[comic_id] = comic_response["safe_title"] async def setup(bot: Bot):