Skip to content

Commit 7233db2

Browse files
authored
fix: add permission checks before fetching messages in reaction events (#1754)
* fix: add permission checks before fetching messages in reaction events * refactor: extract permission check logic * fix: add test for refactored reactionevents
1 parent d29f76f commit 7233db2

File tree

2 files changed

+78
-6
lines changed

2 files changed

+78
-6
lines changed

interactions/api/events/processors/reaction_events.py

+45-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import TYPE_CHECKING
22

33
import interactions.api.events as events
4-
from interactions.models import PartialEmoji, Reaction
4+
from interactions.models import PartialEmoji, Reaction, Message, Permissions
55

66
from ._template import EventMixinTemplate, Processor
77

@@ -12,6 +12,29 @@
1212

1313

1414
class ReactionEvents(EventMixinTemplate):
15+
async def _check_message_fetch_permissions(self, channel_id: str, guild_id: str | None) -> bool:
16+
"""
17+
Check if the bot has permissions to fetch a message in the given channel.
18+
19+
Args:
20+
channel_id: The ID of the channel to check
21+
guild_id: The ID of the guild, if any
22+
23+
Returns:
24+
bool: True if the bot has permission to fetch messages, False otherwise
25+
26+
"""
27+
if not guild_id: # DMs always have permission
28+
return True
29+
30+
channel = await self.cache.fetch_channel(channel_id)
31+
if not channel:
32+
return False
33+
34+
bot_member = channel.guild.me
35+
ctx_perms = channel.permissions_for(bot_member)
36+
return Permissions.READ_MESSAGE_HISTORY in ctx_perms
37+
1538
async def _handle_message_reaction_change(self, event: "RawGatewayEvent", add: bool) -> None:
1639
if member := event.data.get("member"):
1740
author = self.cache.place_member_data(event.data.get("guild_id"), member)
@@ -53,11 +76,27 @@ async def _handle_message_reaction_change(self, event: "RawGatewayEvent", add: b
5376
message.reactions.append(reaction)
5477

5578
else:
56-
message = await self.cache.fetch_message(event.data.get("channel_id"), event.data.get("message_id"))
57-
for r in message.reactions:
58-
if r.emoji == emoji:
59-
reaction = r
60-
break
79+
guild_id = event.data.get("guild_id")
80+
channel_id = event.data.get("channel_id")
81+
82+
if await self._check_message_fetch_permissions(channel_id, guild_id):
83+
message = await self.cache.fetch_message(channel_id, event.data.get("message_id"))
84+
for r in message.reactions:
85+
if r.emoji == emoji:
86+
reaction = r
87+
break
88+
89+
if not message: # otherwise construct skeleton message with no reactions
90+
message = Message.from_dict(
91+
{
92+
"id": event.data.get("message_id"),
93+
"channel_id": channel_id,
94+
"guild_id": guild_id,
95+
"reactions": [],
96+
},
97+
self,
98+
)
99+
61100
if add:
62101
self.dispatch(events.MessageReactionAdd(message=message, emoji=emoji, author=author, reaction=reaction))
63102
else:

tests/test_bot.py

+33
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,39 @@ def ensure_attributes(target_object) -> None:
127127
getattr(target_object, attr)
128128

129129

130+
@pytest.mark.asyncio
131+
async def test_reaction_events(bot: Client, guild: Guild) -> None:
132+
"""
133+
Tests reaction event handling on an uncached message.
134+
135+
Requires manual setup:
136+
1. Set TARGET_CHANNEL_ID environment variable to a valid channel ID.
137+
2. A user must add a reaction to the test message within 60 seconds.
138+
"""
139+
# Skip test if target channel not provided
140+
target_channel_id = os.environ.get("BOT_TEST_CHANNEL_ID")
141+
if not target_channel_id:
142+
pytest.skip("Set TARGET_CHANNEL_ID to run this test")
143+
144+
# Get channel and post test message
145+
channel = await bot.fetch_channel(target_channel_id)
146+
test_msg = await channel.send("Reaction Event Test - React with ✅ within 60 seconds")
147+
148+
try:
149+
# simulate uncached state
150+
bot.cache.delete_message(message_id=test_msg.id, channel_id=test_msg.channel.id)
151+
152+
# wait for user to react with checkmark
153+
reaction_event = await bot.wait_for(
154+
"message_reaction_add", timeout=60, checks=lambda e: e.message.id == test_msg.id and str(e.emoji) == "✅"
155+
)
156+
157+
assert reaction_event.message.id == test_msg.id
158+
assert reaction_event.emoji.name == "✅"
159+
finally:
160+
await test_msg.delete()
161+
162+
130163
@pytest.mark.asyncio
131164
async def test_channels(bot: Client, guild: Guild) -> None:
132165
channels = [

0 commit comments

Comments
 (0)