Skip to content

Commit 3dc9bf9

Browse files
Fix action dedup key (#3534)
* Fix action dedup key to catch duplicate bans across events and channels * Add info logs to action dedup cache hits and misses
1 parent c1c51e4 commit 3dc9bf9

2 files changed

Lines changed: 58 additions & 8 deletions

File tree

bot/exts/filtering/filtering.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,17 +1092,21 @@ 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:
1105+
log.info(f"Cache hit, not running actions {ctx.author} (event={ctx.event.name}): {actions}")
11041106
return False
1105-
recent[key] = now
1107+
log.info(f"Cache miss, running actions on {ctx.author} (event={ctx.event.name}): {actions}")
1108+
recent[base_key] = now
1109+
recent[full_key] = now
11061110
return True
11071111

11081112
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)