3
3
import asyncio , os , random
4
4
from pathlib import Path
5
5
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
7
7
8
8
from killua .bot import BaseBot
9
9
from killua .utils .checks import check
@@ -23,6 +23,34 @@ def __init__(self, message: str):
23
23
super ().__init__ (message )
24
24
25
25
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
+
26
54
class SettingsSelect (discord .ui .Select ):
27
55
"""Creates a select menu to change action settings"""
28
56
@@ -62,9 +90,11 @@ def __init__(self, client: BaseBot):
62
90
async def cog_load (self ):
63
91
# Get number of files in assets/hugs
64
92
DIR = Path (__file__ ).parent .parent .parent .joinpath ("assets/hugs" )
65
-
93
+
66
94
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 ("." )
68
98
]
69
99
number_of_hug_imgs = len (files )
70
100
@@ -75,7 +105,7 @@ async def cog_load(self):
75
105
)
76
106
self .limited_hugs = True
77
107
return
78
-
108
+
79
109
# Constant will be sorted, the files variable will not
80
110
files .sort (key = lambda f : int (cast (str , f ).split ("." )[0 ]))
81
111
for filename , expected_filename in zip (files , ACTIONS ["hug" ]["images" ]):
@@ -91,26 +121,53 @@ async def cog_load(self):
91
121
f"{ PrintColors .OKGREEN } { number_of_hug_imgs } hugs loaded.{ PrintColors .ENDC } "
92
122
)
93
123
94
- async def request_action (self , endpoint : str ) -> Union [dict , str ]:
124
+ async def request_action (self , endpoint : str ) -> Union [AnimeAsset , ArtistAsset ]:
95
125
"""
96
126
Fetch an image from the API for the action commands
97
127
98
128
Raises:
99
129
APIException: If the API returns an error
100
130
"""
101
131
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 } " )
103
133
if r .status == 200 :
104
134
res = await r .json ()
105
135
106
- if res [ "error" ] :
136
+ if "message" in res :
107
137
raise APIException (res ["message" ])
108
138
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
+ )
110
152
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 ())
112
155
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 (
114
171
self , ctx : commands .Context
115
172
) -> (
116
173
discord .Message
@@ -122,10 +179,11 @@ async def get_image(
122
179
embed = discord .Embed .from_dict (
123
180
{
124
181
"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 " ]),
127
184
}
128
185
)
186
+ embed = self .add_credit (embed , image )
129
187
return await ctx .send (embed = embed )
130
188
131
189
async def save_stat (
@@ -171,39 +229,29 @@ def generate_users(self, users: List[discord.User], title: str) -> str:
171
229
userlist = userlist + f", { user .display_name } "
172
230
return userlist
173
231
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 ]:
177
233
"""
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
187
236
188
237
Raises:
189
238
APIException: If the API returns an error
190
239
"""
191
240
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
+ )
193
246
194
247
if not cast (str , chosen ["url" ]).startswith ("http" ):
195
248
# This could have already been done (Python mutability my beloved)
196
249
chosen ["url" ] = (
197
250
self .client .api_url (to_fetch = self .client .is_dev ) + chosen ["url" ]
198
251
)
199
- return (
200
- chosen ["url" ],
201
- chosen ["artist" ],
202
- chosen ["featured" ]
203
- ) # This might eventually be deprecated for copyright reasons
252
+ return ArtistAsset (chosen )
204
253
else :
205
- image = await self .request_action (endpoint )
206
- return image ["link" ], None , False
254
+ return await self .request_action (endpoint )
207
255
208
256
async def action_embed (
209
257
self ,
@@ -219,7 +267,7 @@ async def action_embed(
219
267
if disabled == len (users ):
220
268
return "All members targeted have disabled this action." , None
221
269
222
- image_url , artist , featured = await self ._get_image_url (endpoint )
270
+ asset = await self ._get_image_url (endpoint )
223
271
224
272
text : str = random .choice (ACTIONS [endpoint ]["text" ])
225
273
text = text .format (
@@ -231,39 +279,33 @@ async def action_embed(
231
279
+ "**" ,
232
280
)
233
281
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 = "" )
242
283
243
- if featured is True :
284
+ if ArtistAsset . is_artist_asset ( asset ) and asset [ " featured" ] is True :
244
285
embed .set_footer (
245
286
text = "\U000024d8 This artwork has been created specifically for this bot"
246
287
)
247
288
248
289
file = None
249
290
if endpoint == "hug" :
250
- image_path = image_url .split ("image/" )[1 ]
291
+ image_path = asset [ "url" ] .split ("image/" )[1 ]
251
292
token , expiry = self .client .sha256_for_api (
252
293
image_path , expires_in_seconds = 60 * 60 * 24 * 7
253
294
)
254
- image_url += f"?token={ token } &expiry={ expiry } "
295
+ asset [ "url" ] += f"?token={ token } &expiry={ expiry } "
255
296
embed , file = await self .client .make_embed_from_api (
256
- image_url , embed , no_token = True
297
+ asset [ "url" ] , embed , no_token = True
257
298
)
258
299
else :
259
300
# Does not need to be fetched under any conditions
260
- embed .set_image (url = image_url )
301
+ embed .set_image (url = asset [ "url" ] )
261
302
262
- embed .color = await self .client .find_dominant_color (image_url )
303
+ embed .color = await self .client .find_dominant_color (asset [ "url" ] )
263
304
if disabled > 0 :
264
305
embed .set_footer (
265
306
text = f"{ disabled } user{ 's' if disabled > 1 else '' } disabled being targetted with this action"
266
307
)
308
+ embed = self .add_credit (embed , asset )
267
309
return embed , file
268
310
269
311
async def no_argument (
@@ -364,6 +406,31 @@ async def _do_action(
364
406
): # May be None from no_argument, in which case we don't want to send a message
365
407
await self .client .send_message (ctx , embed = embed , file = file )
366
408
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
+
367
434
async def do_action (
368
435
self , ctx : commands .Context , users : List [discord .User ] = None
369
436
) -> None :
@@ -373,14 +440,7 @@ async def do_action(
373
440
try :
374
441
await self ._do_action (ctx , users )
375
442
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 )
384
444
385
445
@check ()
386
446
@commands .hybrid_command (
@@ -456,13 +516,13 @@ async def dance(self, ctx: commands.Context):
456
516
"""Show off your dance moves!"""
457
517
return await self .get_image (ctx )
458
518
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)
466
526
467
527
@check ()
468
528
@commands .hybrid_command (
@@ -480,12 +540,44 @@ async def blush(self, ctx: commands.Context):
480
540
"""O-Oh! T-thank you for t-the compliment... You have beautiful fingernails too!"""
481
541
return await self .get_image (ctx )
482
542
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
+
483
575
@check ()
484
576
@commands .hybrid_command (
485
- extras = {"category" : Category .ACTIONS , "id" : 11 }, usage = "tail "
577
+ extras = {"category" : Category .ACTIONS , "id" : 123 }, usage = "nope "
486
578
)
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™️ !"""
489
581
return await self .get_image (ctx )
490
582
491
583
def _get_view (self , id : int , current : dict ) -> View :
0 commit comments