Skip to content

Commit 42379fe

Browse files
committed
Improve animal generation command UX
1 parent 1555d6f commit 42379fe

2 files changed

Lines changed: 55 additions & 15 deletions

File tree

src/europython_discord/animals/cog.py

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import discord
88
from discord.ext import commands
9+
from discord.utils import get as discord_get
910

1011
from europython_discord.animals import providers
1112
from europython_discord.animals.config import AnimalsConfig
@@ -28,17 +29,31 @@ def __init__(
2829
self._providers = providers_by_animal
2930
self._rate_limiter = rate_limiter
3031

31-
for animal in self._providers:
32-
self._bot.add_command(self._create_animal_command(animal))
33-
3432
_logger.info("Cog 'Animals' has been initialized")
3533

34+
@commands.Cog.listener()
35+
async def on_ready(self) -> None:
36+
# (re-) register commands on each connected guild
37+
for guild in self._bot.guilds:
38+
self._bot.tree.clear_commands(guild=guild)
39+
for animal in self._providers:
40+
self._bot.tree.add_command(self._create_animal_command(animal), guild=guild)
41+
synced_commands = await self._bot.tree.sync(guild=guild)
42+
_logger.info(
43+
"Synced %d application command(s) to guild %s", len(synced_commands), guild.id
44+
)
45+
3646
async def post_animal_picture(self, animal: str, ctx: commands.Context) -> None:
47+
# send typing indicator
48+
await ctx.defer(ephemeral=False)
49+
50+
# check if command shall be executed
3751
if ctx.channel.name != self._channel_name:
52+
channel = discord_get(ctx.guild.channels, name=self._channel_name)
53+
await ctx.send(f"This command can only be used in {channel.mention}.")
3854
return
3955
if self._rate_limiter.is_rate_limited(ctx.author.id):
40-
return
41-
if animal not in self._providers:
56+
await ctx.send("You are being rate limited. Please try again later.")
4257
return
4358

4459
provider = random.choice(self._providers[animal]) # noqa: S311 suspicious-non-cryptographic-random-usage
@@ -51,7 +66,13 @@ async def post_animal_picture(self, animal: str, ctx: commands.Context) -> None:
5166

5267
if image is None:
5368
_logger.error("Failed to fetch %s pictures", animal)
54-
await ctx.send(f"Failed to fetch {animal} picture.")
69+
await ctx.send(
70+
(
71+
f"Failed to fetch {animal} picture. "
72+
f"If this happens repeatedly, please report it."
73+
),
74+
ephemeral=True,
75+
)
5576
return
5677

5778
embed = discord.Embed()
@@ -60,9 +81,10 @@ async def post_animal_picture(self, animal: str, ctx: commands.Context) -> None:
6081
self._rate_limiter.register_usage(ctx.author.id)
6182
await ctx.send(embed=embed)
6283

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:
84+
def _create_animal_command(self, animal: str) -> discord.app_commands.Command:
85+
@discord.app_commands.command(name=animal, description=f"Get a random {animal} picture")
86+
async def callback(interaction: discord.Interaction) -> None:
87+
ctx = await self._bot.get_context(interaction)
6688
await self.post_animal_picture(animal, ctx)
6789

6890
return callback

tests/animals/test_cog.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ def create_fake_context(
4949
context.author.id = author_id
5050
context.channel.name = channel_name
5151
context.send = AsyncMock()
52+
context.defer = AsyncMock()
53+
# Provide a synchronous guild.channels list so discord.utils.get works.
54+
guild = MagicMock()
55+
fake_channel = MagicMock()
56+
fake_channel.name = DEFAULT_CHANNEL_NAME
57+
fake_channel.mention = f"#{DEFAULT_CHANNEL_NAME}"
58+
guild.channels = [fake_channel]
59+
context.guild = guild
5260
return context
5361

5462

@@ -58,19 +66,23 @@ async def test_command_success() -> None:
5866

5967
await cog.post_animal_picture(DEFAULT_ANIMAL, ctx=ctx)
6068

69+
ctx.defer.assert_awaited_once()
6170
ctx.send.assert_awaited_once()
6271
embed = ctx.send.call_args.kwargs["embed"]
6372
assert embed.image.url == DEFAULT_IMAGE_URL
6473
assert embed.description == f"Behold! A friendly cat appeared from {DEFAULT_IMAGE_SOURCE}"
6574

6675

6776
async def test_command_wrong_channel() -> None:
68-
cog = create_fake_cog(channel_name="channel-1")
69-
ctx = create_fake_context(channel_name="channel-2")
77+
cog = create_fake_cog(channel_name=DEFAULT_CHANNEL_NAME)
78+
ctx = create_fake_context(channel_name="some-other-channel")
7079

7180
await cog.post_animal_picture(DEFAULT_ANIMAL, ctx=ctx)
7281

73-
ctx.send.assert_not_called()
82+
ctx.defer.assert_awaited_once()
83+
ctx.send.assert_awaited_once()
84+
(message,) = ctx.send.call_args.args
85+
assert message == f"This command can only be used in #{DEFAULT_CHANNEL_NAME}."
7486

7587

7688
async def test_rate_limiting() -> None:
@@ -80,11 +92,14 @@ async def test_rate_limiting() -> None:
8092
ctx_1 = create_fake_context()
8193
await cog.post_animal_picture(DEFAULT_ANIMAL, ctx=ctx_1)
8294
ctx_1.send.assert_awaited_once()
95+
assert "embed" in ctx_1.send.call_args.kwargs
8396

84-
# second call with same user
97+
# second call with same user - rate limited, ephemeral error message
8598
ctx_2 = create_fake_context()
8699
await cog.post_animal_picture(DEFAULT_ANIMAL, ctx=ctx_2)
87-
ctx_2.send.assert_not_awaited()
100+
ctx_2.send.assert_awaited_once()
101+
(message,) = ctx_2.send.call_args.args
102+
assert message == "You are being rate limited. Please try again later."
88103

89104

90105
async def test_rate_limiting_different_user() -> None:
@@ -145,4 +160,7 @@ async def generate_image(self) -> AnimalImage | None:
145160

146161
ctx.send.assert_called_once()
147162
(message,) = ctx.send.call_args.args
148-
assert message == f"Failed to fetch {DEFAULT_ANIMAL} picture."
163+
assert message == (
164+
f"Failed to fetch {DEFAULT_ANIMAL} picture. If this happens repeatedly, please report it."
165+
)
166+
assert ctx.send.call_args.kwargs.get("ephemeral") is True

0 commit comments

Comments
 (0)