11import random
2+ from html .parser import HTMLParser
23from typing import Any , TypedDict
34
45import disnake
56from disnake .ext import commands , tasks
7+ from rapidfuzz import process
68from typing_extensions import NotRequired
79
810from monty .bot import Monty
@@ -29,6 +31,40 @@ class XkcdDict(TypedDict):
2931 safe_title : str
3032 extra_parts : NotRequired [dict [str , Any ]]
3133
34+ # I'm sure there is a better way to do any of this, but this worked
35+ class ComicParser (HTMLParser ):
36+ def __init__ (self ):
37+ super ().__init__ ()
38+ self .comic = False
39+ self .list_of_comics = False
40+ self .comics : dict [str , str ] = {}
41+ self .number = None
42+
43+ def handle_starttag (self , tag , attrs ):
44+ if tag == "a" and self .list_of_comics :
45+ self .comic = True
46+ self .number = None
47+ for attr in attrs :
48+ if attr [0 ] == "href" :
49+ self .number = attr [1 ].strip ("/" )
50+ if (
51+ tag == "div"
52+ and len (attrs ) > 0
53+ and attrs [0 ][0 ] == "id"
54+ and attrs [0 ][1 ] == "middleContainer"
55+ ):
56+ self .list_of_comics = True
57+
58+ def handle_data (self , data ):
59+ if not self .comic :
60+ return
61+ self .comics [data .strip ()] = self .number
62+
63+ def handle_endtag (self , tag ):
64+ if self .comic and tag == "a" :
65+ self .comic = False
66+ if self .list_of_comics and tag == "div" :
67+ self .list_of_comics = False
3268
3369class XKCD (
3470 commands .Cog ,
@@ -42,6 +78,7 @@ class XKCD(
4278 def __init__ (self , bot : Monty ) -> None :
4379 self .bot = bot
4480 self .latest_comic_info : XkcdDict | None = None
81+ self .comics : dict [str , str ] | None = None
4582 self .get_latest_comic_info .start ()
4683
4784 def cog_unload (self ) -> None :
@@ -56,6 +93,13 @@ async def get_latest_comic_info(self) -> None:
5693 self .latest_comic_info = await resp .json ()
5794 else :
5895 log .debug (f"Failed to get latest XKCD comic information. Status code { resp .status } " )
96+ async with self .bot .http_session .get (f"{ BASE_URL } /archive" ) as resp :
97+ if resp .status == 200 :
98+ parser = ComicParser ()
99+ parser .feed (resp .text ) # parse /archive for all comic titles and comic number
100+ self .comics = parser .comics
101+ else :
102+ log .debug (f"Failed to get latest list of XKCD comics. Status code { resp .status } " )
59103
60104 @commands .slash_command (name = "xkcd" )
61105 async def xkcd (self , _ : disnake .ApplicationCommandInteraction ) -> None :
@@ -142,6 +186,17 @@ async def number(self, inter: disnake.ApplicationCommandInteraction, comic: int)
142186
143187 await self .send_xkcd (inter , info )
144188
189+ @number .autocomplete ("comic" )
190+ async def number_autocomplete (self , _ : disnake .CommandInteraction , query : str ) -> list [disnake .OptionChoice ]:
191+ """Autocomplete names of XKCD comics when searching for number."""
192+ searches = process .extract (query , self .comics .keys (), limit = 5 )
193+ return [
194+ # Probably shouldn't be returning value as a str, but I am.
195+ disnake .OptionChoice (name = comic , value = self .comics [comic ])
196+ for comic
197+ in [res [0 ] for res in searches ]
198+ ]
199+
145200 @xkcd .sub_command ()
146201 async def random (self , inter : disnake .ApplicationCommandInteraction ) -> None :
147202 """View a random xkcd comic."""
0 commit comments