Skip to content

Commit e29c5c7

Browse files
committed
📦 Switch image API + bug fixes
1 parent 06b3e02 commit e29c5c7

File tree

6 files changed

+191
-87
lines changed

6 files changed

+191
-87
lines changed

killua/cogs/actions.py

+157-65
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import asyncio, os, random
44
from pathlib import Path
55
from logging import info, warning
6-
from typing import List, Union, Optional, cast, Tuple, Dict
6+
from typing import List, Union, Optional, cast, Tuple, Dict, TypedDict
77

88
from killua.bot import BaseBot
99
from killua.utils.checks import check
@@ -23,6 +23,34 @@ def __init__(self, message: str):
2323
super().__init__(message)
2424

2525

26+
class AnimeAsset(TypedDict):
27+
url: str
28+
anime_name: str
29+
30+
@classmethod
31+
def is_anime_asset(cls, data: dict) -> bool:
32+
return "anime_name" in data
33+
34+
35+
class Artist(TypedDict):
36+
name: str
37+
link: str
38+
39+
@classmethod
40+
def from_api(cls, data: dict) -> "Artist":
41+
return cls(name=data["artist_name"], link=data["artist_href"])
42+
43+
44+
class ArtistAsset(TypedDict):
45+
url: str
46+
artist: Optional[Artist]
47+
featured: bool
48+
49+
@classmethod
50+
def is_artist_asset(cls, data: dict) -> bool:
51+
return "artist" in data
52+
53+
2654
class SettingsSelect(discord.ui.Select):
2755
"""Creates a select menu to change action settings"""
2856

@@ -62,9 +90,11 @@ def __init__(self, client: BaseBot):
6290
async def cog_load(self):
6391
# Get number of files in assets/hugs
6492
DIR = Path(__file__).parent.parent.parent.joinpath("assets/hugs")
65-
93+
6694
files = [
67-
name for name in os.listdir(DIR) if os.path.isfile(os.path.join(DIR, name)) and not name.startswith(".")
95+
name
96+
for name in os.listdir(DIR)
97+
if os.path.isfile(os.path.join(DIR, name)) and not name.startswith(".")
6898
]
6999
number_of_hug_imgs = len(files)
70100

@@ -75,7 +105,7 @@ async def cog_load(self):
75105
)
76106
self.limited_hugs = True
77107
return
78-
108+
79109
# Constant will be sorted, the files variable will not
80110
files.sort(key=lambda f: int(cast(str, f).split(".")[0]))
81111
for filename, expected_filename in zip(files, ACTIONS["hug"]["images"]):
@@ -91,26 +121,53 @@ async def cog_load(self):
91121
f"{PrintColors.OKGREEN}{number_of_hug_imgs} hugs loaded.{PrintColors.ENDC}"
92122
)
93123

94-
async def request_action(self, endpoint: str) -> Union[dict, str]:
124+
async def request_action(self, endpoint: str) -> Union[AnimeAsset, ArtistAsset]:
95125
"""
96126
Fetch an image from the API for the action commands
97127
98128
Raises:
99129
APIException: If the API returns an error
100130
"""
101131

102-
r = await self.session.get(f"https://purrbot.site/api/img/sfw/{endpoint}/gif")
132+
r = await self.session.get(f"https://nekos.best/api/v2/{endpoint}")
103133
if r.status == 200:
104134
res = await r.json()
105135

106-
if res["error"]:
136+
if "message" in res:
107137
raise APIException(res["message"])
108138

109-
return res
139+
raw_asset = res["results"][0]
140+
if "anime_name" in raw_asset:
141+
return AnimeAsset(
142+
url=raw_asset["url"], anime_name=raw_asset["anime_name"]
143+
)
144+
else:
145+
return ArtistAsset(
146+
# No asset is currently returned in this format but the API has
147+
# endpoints that return this format so it's here for future use
148+
url=raw_asset["url"],
149+
artist=Artist.from_api(raw_asset),
150+
featured=False,
151+
)
110152
else:
111-
raise APIException(await r.text())
153+
json = await r.json()
154+
raise APIException(json["message"] if "message" in json else await r.text())
112155

113-
async def get_image(
156+
def add_credit(
157+
self, embed: discord.Embed, asset: Union[ArtistAsset, AnimeAsset]
158+
) -> discord.Embed:
159+
"""
160+
Adds the artist credit to the embed
161+
"""
162+
if ArtistAsset.is_artist_asset(asset) and asset["artist"] is not None:
163+
embed.description = (
164+
f"-# Art by [{asset['artist']['name']}]({asset['artist']['link']})"
165+
)
166+
elif AnimeAsset.is_anime_asset(asset):
167+
embed.description = f"-# GIF from anime `{asset['anime_name']}`"
168+
return embed
169+
170+
async def _get_image(
114171
self, ctx: commands.Context
115172
) -> (
116173
discord.Message
@@ -122,10 +179,11 @@ async def get_image(
122179
embed = discord.Embed.from_dict(
123180
{
124181
"title": "",
125-
"image": {"url": image["link"]},
126-
"color": await self.client.find_dominant_color(image["link"]),
182+
"image": {"url": image["url"]},
183+
"color": await self.client.find_dominant_color(image["url"]),
127184
}
128185
)
186+
embed = self.add_credit(embed, image)
129187
return await ctx.send(embed=embed)
130188

131189
async def save_stat(
@@ -171,39 +229,29 @@ def generate_users(self, users: List[discord.User], title: str) -> str:
171229
userlist = userlist + f", {user.display_name}"
172230
return userlist
173231

174-
async def _get_image_url(
175-
self, endpoint: str
176-
) -> Tuple[str, Optional[Dict[str, str]], bool]:
232+
async def _get_image_url(self, endpoint: str) -> Union[AnimeAsset, ArtistAsset]:
177233
"""
178-
Gets a tuple object with all image infos.
179-
First element is the image URL, second is the artist info.
180-
181-
Either as
182-
(https://example.com/image.jpg, None)
183-
if returned from the API or
184-
185-
(https://example.com/image.jpg, {"name": "artist", "link": "https://example.com/artist"})
186-
if the image is from the hug command
234+
Gets an image URL and extra info from the API for the action commands
235+
or, for the hug command, returns a random image from the list of images
187236
188237
Raises:
189238
APIException: If the API returns an error
190239
"""
191240
if endpoint == "hug":
192-
chosen = LIMITED_HUGS_ENDPOINT if self.limited_hugs else random.choice(ACTIONS[endpoint]["images"])
241+
chosen = (
242+
LIMITED_HUGS_ENDPOINT
243+
if self.limited_hugs
244+
else random.choice(ACTIONS[endpoint]["images"])
245+
)
193246

194247
if not cast(str, chosen["url"]).startswith("http"):
195248
# This could have already been done (Python mutability my beloved)
196249
chosen["url"] = (
197250
self.client.api_url(to_fetch=self.client.is_dev) + chosen["url"]
198251
)
199-
return (
200-
chosen["url"],
201-
chosen["artist"],
202-
chosen["featured"]
203-
) # This might eventually be deprecated for copyright reasons
252+
return ArtistAsset(chosen)
204253
else:
205-
image = await self.request_action(endpoint)
206-
return image["link"], None, False
254+
return await self.request_action(endpoint)
207255

208256
async def action_embed(
209257
self,
@@ -219,7 +267,7 @@ async def action_embed(
219267
if disabled == len(users):
220268
return "All members targeted have disabled this action.", None
221269

222-
image_url, artist, featured = await self._get_image_url(endpoint)
270+
asset = await self._get_image_url(endpoint)
223271

224272
text: str = random.choice(ACTIONS[endpoint]["text"])
225273
text = text.format(
@@ -231,39 +279,33 @@ async def action_embed(
231279
+ "**",
232280
)
233281

234-
embed = discord.Embed.from_dict(
235-
{
236-
"title": text,
237-
"description": (
238-
f"Art by [{artist['name']}]({artist['link']})" if artist else None
239-
),
240-
}
241-
)
282+
embed = discord.Embed(title=text, description="")
242283

243-
if featured is True:
284+
if ArtistAsset.is_artist_asset(asset) and asset["featured"] is True:
244285
embed.set_footer(
245286
text="\U000024d8 This artwork has been created specifically for this bot"
246287
)
247288

248289
file = None
249290
if endpoint == "hug":
250-
image_path = image_url.split("image/")[1]
291+
image_path = asset["url"].split("image/")[1]
251292
token, expiry = self.client.sha256_for_api(
252293
image_path, expires_in_seconds=60 * 60 * 24 * 7
253294
)
254-
image_url += f"?token={token}&expiry={expiry}"
295+
asset["url"] += f"?token={token}&expiry={expiry}"
255296
embed, file = await self.client.make_embed_from_api(
256-
image_url, embed, no_token=True
297+
asset["url"], embed, no_token=True
257298
)
258299
else:
259300
# Does not need to be fetched under any conditions
260-
embed.set_image(url=image_url)
301+
embed.set_image(url=asset["url"])
261302

262-
embed.color = await self.client.find_dominant_color(image_url)
303+
embed.color = await self.client.find_dominant_color(asset["url"])
263304
if disabled > 0:
264305
embed.set_footer(
265306
text=f"{disabled} user{'s' if disabled > 1 else ''} disabled being targetted with this action"
266307
)
308+
embed = self.add_credit(embed, asset)
267309
return embed, file
268310

269311
async def no_argument(
@@ -364,6 +406,31 @@ async def _do_action(
364406
): # May be None from no_argument, in which case we don't want to send a message
365407
await self.client.send_message(ctx, embed=embed, file=file)
366408

409+
async def _handle_error(self, ctx: commands.Context, error: Exception) -> None:
410+
"""
411+
Handles any exceptions raised during the execution of an action command
412+
"""
413+
if isinstance(error, ActionException):
414+
embed = discord.Embed.from_dict(
415+
{
416+
"title": "An error occurred",
417+
"description": ":x: " + type(error).__name__ + ": " + error.message,
418+
"color": int(discord.Colour.red()),
419+
}
420+
)
421+
await ctx.send(embed=embed)
422+
else:
423+
raise error
424+
425+
async def get_image(self, ctx: commands.Context) -> discord.Message:
426+
"""
427+
Wrapper for _get_image to catch any exceptions raised
428+
"""
429+
try:
430+
return await self._get_image(ctx)
431+
except APIException as e:
432+
await self._handle_error(ctx, e)
433+
367434
async def do_action(
368435
self, ctx: commands.Context, users: List[discord.User] = None
369436
) -> None:
@@ -373,14 +440,7 @@ async def do_action(
373440
try:
374441
await self._do_action(ctx, users)
375442
except ActionException as e:
376-
embed = discord.Embed.from_dict(
377-
{
378-
"title": "An error occurred",
379-
"description": ":x: " + type(e).__name__ + ": " + e.message,
380-
"color": discord.Colour.red(),
381-
}
382-
)
383-
await ctx.send(embed=embed)
443+
await self._handle_error(ctx, e)
384444

385445
@check()
386446
@commands.hybrid_command(
@@ -456,13 +516,13 @@ async def dance(self, ctx: commands.Context):
456516
"""Show off your dance moves!"""
457517
return await self.get_image(ctx)
458518

459-
@check()
460-
@commands.hybrid_command(
461-
extras={"category": Category.ACTIONS, "id": 8}, usage="neko"
462-
)
463-
async def neko(self, ctx: commands.Context):
464-
"""uwu"""
465-
return await self.get_image(ctx)
519+
# @check()
520+
# @commands.hybrid_command(
521+
# extras={"category": Category.ACTIONS, "id": 8}, usage="neko"
522+
# )
523+
# async def neko(self, ctx: commands.Context):
524+
# """uwu"""
525+
# return await self.get_image(ctx)
466526

467527
@check()
468528
@commands.hybrid_command(
@@ -480,12 +540,44 @@ async def blush(self, ctx: commands.Context):
480540
"""O-Oh! T-thank you for t-the compliment... You have beautiful fingernails too!"""
481541
return await self.get_image(ctx)
482542

543+
# @check()
544+
# @commands.hybrid_command(
545+
# extras={"category": Category.ACTIONS, "id": 11}, usage="tail"
546+
# )
547+
# async def tail(self, ctx: commands.Context):
548+
# """Wag your tail when you're happy!"""
549+
# return await self.get_image(ctx)
550+
551+
@check()
552+
@commands.hybrid_command(
553+
extras={"category": Category.ACTIONS, "id": 120}, usage="cry"
554+
)
555+
async def cry(self, ctx: commands.Context):
556+
"""They thought Ant Man was gonna do WHAT to Thanos???"""
557+
return await self.get_image(ctx)
558+
559+
@check()
560+
@commands.hybrid_command(
561+
extras={"category": Category.ACTIONS, "id": 121}, usage="smug"
562+
)
563+
async def smug(self, ctx: commands.Context):
564+
"""They don't know I use Discord light mode..."""
565+
return await self.get_image(ctx)
566+
567+
@check()
568+
@commands.hybrid_command(
569+
extras={"category": Category.ACTIONS, "id": 122}, usage="yawn"
570+
)
571+
async def yawn(self, ctx: commands.Context):
572+
"""Don't worry I'll go to bed just 5 more Tik Toks!"""
573+
return await self.get_image(ctx)
574+
483575
@check()
484576
@commands.hybrid_command(
485-
extras={"category": Category.ACTIONS, "id": 11}, usage="tail"
577+
extras={"category": Category.ACTIONS, "id": 123}, usage="nope"
486578
)
487-
async def tail(self, ctx: commands.Context):
488-
"""Wag your tail when you're happy!"""
579+
async def nope(self, ctx: commands.Context):
580+
"""No I don't want to buy your new cryptocurrency Shabloink Coin™️!"""
489581
return await self.get_image(ctx)
490582

491583
def _get_view(self, id: int, current: dict) -> View:

killua/cogs/dev.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ async def api_stats(self, ctx: commands.Context):
327327
headers={"Authorization": self.client.secret_api_key},
328328
)
329329
if data.status != 200:
330-
return await ctx.send("An error occured while fetching the data")
330+
return await ctx.send("An error occurred while fetching the data")
331331

332332
json = await data.json()
333333
response_time = data.headers.get("X-Response-Time")

killua/cogs/economy.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,9 @@ async def _getmember(
149149
"inline": False,
150150
},
151151
],
152-
"thumbnail": {"url": str(user.display_avatar.url) if user.avatar else None},
152+
"thumbnail": {
153+
"url": str(user.display_avatar.url) if user.avatar else None
154+
},
153155
"image": {"url": user.banner.url if user.banner else None},
154156
"color": 0x3E4A78,
155157
}
@@ -479,7 +481,7 @@ async def boxinfo(self, ctx: commands.Context, box: str):
479481
cast(str, data["image"]).format(
480482
self.client.api_url(to_fetch=self.client.is_dev)
481483
),
482-
embed
484+
embed,
483485
)
484486
await ctx.send(embed=embed, file=file)
485487

0 commit comments

Comments
 (0)