|
2 | 2 |
|
3 | 3 | import logging |
4 | 4 | import random |
5 | | -import time |
6 | | -from collections import OrderedDict |
| 5 | +from typing import Self |
7 | 6 |
|
8 | 7 | import discord |
9 | 8 | from discord.ext import commands |
10 | 9 |
|
11 | | -from europython_discord.animals.clients import AnimalClient |
12 | | -from europython_discord.animals.config import AnimalKind, AnimalsConfig |
| 10 | +from europython_discord.animals import providers |
| 11 | +from europython_discord.animals.config import AnimalsConfig |
| 12 | +from europython_discord.animals.providers import AnimalImageProvider |
| 13 | +from europython_discord.animals.rate_limiter import RateLimiter |
13 | 14 |
|
14 | 15 | _logger = logging.getLogger(__name__) |
15 | 16 |
|
16 | | -_MAX_COOLDOWN_TRACKING = 100 |
17 | | - |
18 | 17 |
|
19 | 18 | class AnimalsCog(commands.Cog): |
20 | 19 | def __init__( |
21 | 20 | self, |
22 | 21 | bot: commands.Bot, |
23 | | - config: AnimalsConfig, |
24 | | - client: AnimalClient | None = None, |
| 22 | + providers_by_animal: dict[str, list[AnimalImageProvider]], |
| 23 | + rate_limiter: RateLimiter, |
| 24 | + channel_name: str, |
25 | 25 | ) -> None: |
26 | | - self.bot = bot |
27 | | - self.config = config |
28 | | - self._client = client or AnimalClient() |
29 | | - self._last_usage_timestamp_by_user_id: OrderedDict[int, float] = OrderedDict() |
| 26 | + self._bot = bot |
| 27 | + self._channel_name = channel_name |
| 28 | + self._providers = providers_by_animal |
| 29 | + self._rate_limiter = rate_limiter |
| 30 | + |
| 31 | + for animal in self._providers: |
| 32 | + self._bot.add_command(self._create_animal_command(animal)) |
| 33 | + |
30 | 34 | _logger.info("Cog 'Animals' has been initialized") |
31 | 35 |
|
32 | | - async def handle_animal_command(self, ctx: commands.Context, animal: AnimalKind) -> None: |
33 | | - if ctx.channel.name != self.config.channel_name: |
| 36 | + async def post_animal_picture(self, animal: str, ctx: commands.Context) -> None: |
| 37 | + if ctx.channel.name != self._channel_name: |
34 | 38 | return |
35 | | - |
36 | | - if self._is_rate_limited(ctx.author.id): |
| 39 | + if self._rate_limiter.is_rate_limited(ctx.author.id): |
37 | 40 | return |
38 | | - |
39 | | - result = await self._client.fetch_image(animal) |
40 | | - if result is not None: |
41 | | - embed = discord.Embed() |
42 | | - embed.description = f"Behold! A friendly {animal} appeared from {result.source}" |
43 | | - embed.set_image(url=result.url) |
44 | | - self._update_rate_limit_cache(ctx.author.id) |
45 | | - await ctx.send(embed=embed) |
| 41 | + if animal not in self._providers: |
46 | 42 | return |
47 | 43 |
|
48 | | - await ctx.send(self._error_message_for(animal)) |
49 | | - |
50 | | - @commands.hybrid_command(name="dog", description="Get a random dog picture") |
51 | | - async def dog_command(self, ctx: commands.Context) -> None: |
52 | | - await self.handle_animal_command(ctx, AnimalKind.DOG) |
53 | | - |
54 | | - @commands.hybrid_command(name="cat", description="Get a random cat picture") |
55 | | - async def cat_command(self, ctx: commands.Context) -> None: |
56 | | - await self.handle_animal_command(ctx, AnimalKind.CAT) |
57 | | - |
58 | | - @commands.hybrid_command(name="duck", description="Get a random duck picture") |
59 | | - async def duck_command(self, ctx: commands.Context) -> None: |
60 | | - await self.handle_animal_command(ctx, AnimalKind.DUCK) |
61 | | - |
62 | | - @commands.hybrid_command(name="fox", description="Get a random fox picture") |
63 | | - async def fox_command(self, ctx: commands.Context) -> None: |
64 | | - await self.handle_animal_command(ctx, AnimalKind.FOX) |
65 | | - |
66 | | - async def cog_load(self) -> None: |
67 | | - existing = {"dog", "cat", "duck", "fox"} |
68 | | - for animal in AnimalKind: |
69 | | - if animal in existing: |
70 | | - continue |
71 | | - cmd = _make_animality_command(animal, self) |
72 | | - self.bot.add_command(cmd) |
| 44 | + provider = random.choice(self._providers[animal]) # noqa: S311 suspicious-non-cryptographic-random-usage |
| 45 | + try: |
| 46 | + image = await provider.generate_image() |
| 47 | + except Exception: |
| 48 | + _logger.exception("Error while generating %s image", animal) |
| 49 | + await ctx.send(f"Failed to fetch {animal} picture. Internal error, please report it.") |
| 50 | + return |
73 | 51 |
|
74 | | - def _error_message_for(self, animal: AnimalKind) -> str: |
75 | | - if animal_config := self.config.config_by_kind.get(animal): |
76 | | - return random.choice(animal_config.error_messages) # noqa: S311 |
| 52 | + if image is None: |
| 53 | + _logger.error("Failed to fetch %s pictures", animal) |
| 54 | + await ctx.send(f"Failed to fetch {animal} picture.") |
| 55 | + return |
77 | 56 |
|
78 | | - if not self.config.animality_error_messages: |
79 | | - return f"Couldn't find a {animal} picture right now." |
80 | | - plural = f"{animal}s" if animal != "fish" else "fish" |
81 | | - return random.choice(self.config.animality_error_messages).format( # noqa: S311 |
82 | | - animal=animal, plural=plural |
| 57 | + embed = discord.Embed() |
| 58 | + embed.description = f"Behold! A friendly {animal} appeared from {image.source}" |
| 59 | + embed.set_image(url=image.url) |
| 60 | + self._rate_limiter.register_usage(ctx.author.id) |
| 61 | + await ctx.send(embed=embed) |
| 62 | + |
| 63 | + def _create_animal_command(self, animal: str) -> commands.HybridCommand: |
| 64 | + @commands.hybrid_command(name=animal, description=f"Get a random {animal} picture") |
| 65 | + async def callback(ctx: commands.Context) -> None: |
| 66 | + await self.post_animal_picture(animal, ctx) |
| 67 | + |
| 68 | + return callback |
| 69 | + |
| 70 | + @classmethod |
| 71 | + def from_config(cls, bot: commands.Bot, config: AnimalsConfig) -> Self: |
| 72 | + return cls( |
| 73 | + bot=bot, |
| 74 | + providers_by_animal=providers.get_all_providers(), |
| 75 | + rate_limiter=RateLimiter(config.cooldown_seconds), |
| 76 | + channel_name=config.channel_name, |
83 | 77 | ) |
84 | | - |
85 | | - def _is_rate_limited(self, user_id: int) -> bool: |
86 | | - last_usage_timestamp = self._last_usage_timestamp_by_user_id.get(user_id, 0) |
87 | | - return last_usage_timestamp + self.config.cooldown_seconds > time.time() |
88 | | - |
89 | | - def _update_rate_limit_cache(self, user_id: int) -> None: |
90 | | - # update cache |
91 | | - self._last_usage_timestamp_by_user_id[user_id] = time.time() |
92 | | - |
93 | | - # trim cache |
94 | | - self._last_usage_timestamp_by_user_id.move_to_end(user_id) |
95 | | - if len(self._last_usage_timestamp_by_user_id) > _MAX_COOLDOWN_TRACKING: |
96 | | - self._last_usage_timestamp_by_user_id.popitem(last=False) |
97 | | - |
98 | | - |
99 | | -def _make_animality_command(name: AnimalKind, cog: AnimalsCog) -> commands.HybridCommand: |
100 | | - @commands.hybrid_command(name=name, description=f"Get a random {name} picture") |
101 | | - async def callback(ctx: commands.Context) -> None: |
102 | | - await cog.handle_animal_command(ctx, name) |
103 | | - |
104 | | - return callback |
0 commit comments