Skip to content

Commit a4004b3

Browse files
committed
Fix action dedup key to catch duplicate bans across events and channels
1 parent c1c51e4 commit a4004b3

2 files changed

Lines changed: 56 additions & 8 deletions

File tree

bot/exts/filtering/filtering.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,17 +1092,19 @@ def _should_run_actions(self, ctx: FilterContext, actions: ActionSettings) -> bo
10921092
break
10931093
del recent[oldest_key]
10941094

1095-
key = (
1096-
ctx.event.name,
1095+
# base_key ignores additional_actions so a events for the same user are caught
1096+
# even when antispam skips adding the deletion handler the second time.
1097+
base_key = (
10971098
getattr(ctx.author, "id", None),
1098-
getattr(ctx.channel, "id", None),
10991099
json.dumps(to_serializable(actions), sort_keys=True, default=str),
1100-
# So that each callable in additional_actions also forms the cache key
1100+
)
1101+
full_key = base_key + (
11011102
tuple(sorted(getattr(a, "__qualname__", repr(a)) for a in ctx.additional_actions)),
11021103
)
1103-
if key in recent:
1104+
if base_key in recent or full_key in recent:
11041105
return False
1105-
recent[key] = now
1106+
recent[base_key] = now
1107+
recent[full_key] = now
11061108
return True
11071109

11081110
def _increment_stats(self, triggered_filters: dict[AtomicList, list[Filter]]) -> None:

tests/bot/exts/filtering/test_action_deduplication.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ def _make_actions(send_alert: bool) -> ActionSettings:
1818
return actions
1919

2020
@staticmethod
21-
def _make_context(*, author_id: int = 1, channel_id: int = 10) -> FilterContext:
21+
def _make_context(*, author_id: int = 1, channel_id: int = 10, event: Event = Event.MESSAGE) -> FilterContext:
2222
return FilterContext(
23-
event=Event.MESSAGE,
23+
event=event,
2424
author=MockMember(id=author_id),
2525
channel=MockTextChannel(id=channel_id),
2626
content="hello",
@@ -42,6 +42,52 @@ def test_different_action_values_are_not_suppressed(self):
4242
self.assertTrue(self.cog._should_run_actions(ctx, first_actions))
4343
self.assertTrue(self.cog._should_run_actions(ctx, second_actions))
4444

45+
def test_same_action_payload_for_different_events_is_suppressed(self):
46+
"""MESSAGE followed by MESSAGE_EDIT for the same user/channel/action should not double-ban."""
47+
msg_ctx = FilterContext(
48+
event=Event.MESSAGE,
49+
author=MockMember(id=1),
50+
channel=MockTextChannel(id=10),
51+
content="hello",
52+
message=None,
53+
)
54+
edit_ctx = FilterContext(
55+
event=Event.MESSAGE_EDIT,
56+
author=MockMember(id=1),
57+
channel=MockTextChannel(id=10),
58+
content="hello",
59+
message=None,
60+
)
61+
actions = self._make_actions(send_alert=True)
62+
63+
self.assertTrue(self.cog._should_run_actions(msg_ctx, actions))
64+
self.assertFalse(self.cog._should_run_actions(edit_ctx, actions))
65+
66+
def test_differing_additional_actions_are_still_suppressed(self):
67+
"""Second event with no additional_actions (antispam already queued) should still be deduped."""
68+
69+
async def fake_handler(ctx: FilterContext) -> None:
70+
pass
71+
72+
first_ctx = self._make_context()
73+
first_ctx.additional_actions.append(fake_handler)
74+
75+
second_ctx = self._make_context() # no additional_actions
76+
77+
actions = self._make_actions(send_alert=True)
78+
79+
self.assertTrue(self.cog._should_run_actions(first_ctx, actions))
80+
self.assertFalse(self.cog._should_run_actions(second_ctx, actions))
81+
82+
def test_same_action_payload_across_channels_is_suppressed(self):
83+
"""Antispam is cross-channel; same user/action in a different channel should not double-ban."""
84+
first_ctx = self._make_context(channel_id=10)
85+
second_ctx = self._make_context(channel_id=20)
86+
actions = self._make_actions(send_alert=True)
87+
88+
self.assertTrue(self.cog._should_run_actions(first_ctx, actions))
89+
self.assertFalse(self.cog._should_run_actions(second_ctx, actions))
90+
4591
def test_same_action_payload_for_different_authors_is_not_suppressed(self):
4692
first_ctx = self._make_context(author_id=1)
4793
second_ctx = self._make_context(author_id=2)

0 commit comments

Comments
 (0)