ignore-channels: fnmatch patterns + silent before classifier gate (beh-09) — no emoji leak into ignored channels
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import fnmatch
|
||||
import logging
|
||||
import random
|
||||
import re
|
||||
@@ -481,7 +482,7 @@ class FjerkroaBot(commands.Bot):
|
||||
return fallback_channel
|
||||
if channel_name.startswith("#"):
|
||||
channel_name = channel_name[1:]
|
||||
if not no_ignore and channel_name in self.config.get("ignore-channels", []):
|
||||
if not no_ignore and self.channel_ignored(channel_name):
|
||||
return fallback_channel
|
||||
for guild in self.guilds:
|
||||
channel = discord.utils.get(guild.channels, name=channel_name)
|
||||
@@ -494,8 +495,12 @@ class FjerkroaBot(commands.Bot):
|
||||
return str(channel.recipient.name)
|
||||
return str(channel.id) if isinstance(channel, DMChannel) else str(channel.name)
|
||||
|
||||
def channel_ignored(self, channel_name) -> bool:
|
||||
"""fnmatch patterns; plain names match exactly as before (BEH-09)."""
|
||||
return any(fnmatch.fnmatchcase(str(channel_name), pattern) for pattern in self.config.get("ignore-channels", []))
|
||||
|
||||
def ignore_message(self, channel_name, message):
|
||||
return channel_name in self.config.get("ignore-channels", []) and not message.direct
|
||||
return self.channel_ignored(channel_name) and not message.direct
|
||||
|
||||
def log_message_action(self, action, message, channel_name):
|
||||
logging.info(f"{action} message {repr(message)} for channel {channel_name}")
|
||||
@@ -521,6 +526,11 @@ class FjerkroaBot(commands.Bot):
|
||||
|
||||
async def handle_message_through_responder(self, message):
|
||||
"""Handle a message through the AI responder"""
|
||||
# Ignored channels are fully silent — before the classifier gate,
|
||||
# so no emoji reaction leaks either (BEH-09). DMs are never ignored.
|
||||
if not isinstance(message.channel, DMChannel) and self.channel_ignored(self.get_channel_name(message.channel)):
|
||||
self.log_message_action("ignore", message, self.get_channel_name(message.channel))
|
||||
return
|
||||
message_content = str(message.content).strip()
|
||||
if message.reference and message.reference.resolved and isinstance(message.reference.resolved.content, str):
|
||||
reference_content = str(message.reference.resolved.content).replace("\n", "> \n")
|
||||
|
||||
@@ -61,3 +61,15 @@ Within `quiet-hours = "HH:MM-HH:MM"` (host-local, may wrap midnight)
|
||||
`bot_initiated_allowed()` is false: no boreness, later no scheduler
|
||||
posts. Replies to users stay unaffected — a guest asking at 23:30
|
||||
still gets an answer.
|
||||
|
||||
### BEH-09 — Ignored channels are fully silent (coverage: test)
|
||||
|
||||
Channels matching `ignore-channels` get neither replies nor
|
||||
classifier emoji reactions: the message handler returns before the
|
||||
classifier gate, so no model call, no reaction, no history entry.
|
||||
Entries are fnmatch patterns (`todo*` matches `todo`, `todo-lists`);
|
||||
plain names keep matching exactly as before. DMs are never ignored.
|
||||
`channel_by_name` resolution honors the same patterns. (Previously
|
||||
the ignore check sat only in `respond()`, after the classifier —
|
||||
emoji reactions leaked into ignored channels, and matching was
|
||||
exact-name only.)
|
||||
|
||||
@@ -65,6 +65,55 @@ class TestClassifierGate(ClassifierGateBase):
|
||||
self.bot.respond.assert_not_awaited()
|
||||
|
||||
|
||||
class TestIgnoredChannels(ClassifierGateBase):
|
||||
def ignored_msg(self, channel_name):
|
||||
message = self.public_msg("hello there")
|
||||
message.channel.name = channel_name
|
||||
message.add_reaction = AsyncMock()
|
||||
return message
|
||||
|
||||
async def test_pattern_match_suppresses_reaction_and_reply(self):
|
||||
"""BEH-09: fnmatch pattern hit -> no classifier call, no emoji, no reply."""
|
||||
self.gate_setup({"reply": False, "factual": False, "emoji": "👍"})
|
||||
self.bot.config["ignore-channels"] = ["todo*"]
|
||||
message = self.ignored_msg("todo-lists")
|
||||
await self.bot.on_message(message)
|
||||
self.bot.airesponder.classify.assert_not_awaited()
|
||||
message.add_reaction.assert_not_awaited()
|
||||
self.bot.respond.assert_not_awaited()
|
||||
|
||||
async def test_exact_name_still_matches(self):
|
||||
"""BEH-09: plain names keep working as exact matches."""
|
||||
self.gate_setup({"reply": True, "factual": False, "emoji": None})
|
||||
self.bot.config["ignore-channels"] = ["blengon"]
|
||||
await self.bot.on_message(self.ignored_msg("blengon"))
|
||||
self.bot.respond.assert_not_awaited()
|
||||
|
||||
async def test_non_matching_channel_passes(self):
|
||||
"""BEH-09: unmatched channels reach the responder as before."""
|
||||
self.gate_setup({"reply": True, "factual": False, "emoji": None})
|
||||
self.bot.config["ignore-channels"] = ["todo*"]
|
||||
await self.bot.on_message(self.ignored_msg("chat"))
|
||||
self.bot.respond.assert_awaited_once()
|
||||
|
||||
async def test_dm_never_ignored(self):
|
||||
"""BEH-09: a DM whose recipient name matches a pattern is still answered."""
|
||||
self.gate_setup({"reply": True, "factual": False, "emoji": None})
|
||||
self.bot.config["ignore-channels"] = ["todo*"]
|
||||
message = self.public_msg("hei bot")
|
||||
message.channel = MagicMock(spec=DMChannel)
|
||||
message.channel.recipient = MagicMock()
|
||||
message.channel.recipient.name = "todo-fan"
|
||||
await self.bot.on_message(message)
|
||||
self.bot.respond.assert_awaited_once()
|
||||
|
||||
def test_channel_by_name_honors_patterns(self):
|
||||
"""BEH-09: channel_by_name resolution skips pattern-ignored channels."""
|
||||
self.bot.config["ignore-channels"] = ["todo*"]
|
||||
fallback = MagicMock(spec=TextChannel)
|
||||
self.assertIs(self.bot.channel_by_name("todo-lists", fallback), fallback)
|
||||
|
||||
|
||||
class TestTypingPacing(OpsBase):
|
||||
async def send_with(self, answer, factual, cps=30):
|
||||
if cps is not None:
|
||||
|
||||
Reference in New Issue
Block a user