import asyncio import logging import random import re from collections import deque from pathlib import Path import discord from cogs.sentiment.log_utils import log_action from cogs.sentiment.state import save_user_state logger = logging.getLogger("bcs.sentiment") _PROMPTS_DIR = Path(__file__).resolve().parent.parent.parent / "prompts" _UNBLOCK_REDIRECT_PROMPT = (_PROMPTS_DIR / "unblock_redirect.txt").read_text(encoding="utf-8") # Regex: matches "unblock" as a whole word, case-insensitive UNBLOCK_PATTERN = re.compile(r"\bunblock(?:ed|ing|s)?\b", re.IGNORECASE) DEFAULT_UNBLOCK_REMINDS = [ "{username}, begging to be unblocked in chat is not the move. Take it up with an admin. 🙄", "{username}, nobody's getting unblocked because you asked nicely in a gaming channel.", "Hey {username}, the unblock button isn't in this chat. Just saying.", "{username}, I admire the persistence but this isn't the unblock hotline.", "{username}, that's between you and whoever blocked you. Chat isn't the appeals court.", ] DEFAULT_UNBLOCK_NUDGES = [ "{username}, we've been over this. No amount of asking here is going to change anything. 🙄", "{username}, I'm starting to think you enjoy being told no. Still not getting unblocked via chat.", "{username}, at this point I could set a reminder for your next unblock request. Take it to an admin.", "Babe. {username}. We've had this conversation {count} times. It's not happening here. 😭", "{username}, I'm keeping a tally and you're at {count}. The answer is still the same.", ] # Per-channel deque of recent LLM-generated messages (for variety) _recent_redirects: dict[int, deque] = {} def _get_recent_redirects(channel_id: int) -> list[str]: if channel_id in _recent_redirects: return list(_recent_redirects[channel_id]) return [] def _record_redirect(channel_id: int, text: str): if channel_id not in _recent_redirects: _recent_redirects[channel_id] = deque(maxlen=5) _recent_redirects[channel_id].append(text) def _strip_brackets(text: str) -> str: """Strip leaked LLM metadata brackets.""" segments = re.split(r"^\s*\[[^\]]*\]\s*$", text, flags=re.MULTILINE) segments = [s.strip() for s in segments if s.strip()] return segments[-1] if segments else "" def matches_unblock_nag(content: str) -> bool: """Check if a message contains unblock-related nagging.""" return bool(UNBLOCK_PATTERN.search(content)) async def _generate_llm_redirect( bot, message: discord.Message, count: int, ) -> str | None: """Ask the LLM chat model to generate an unblock-nag redirect.""" recent = _get_recent_redirects(message.channel.id) user_prompt = ( f"Username: {message.author.display_name}\n" f"Channel: #{getattr(message.channel, 'name', 'unknown')}\n" f"Unblock nag count: {count}\n" f"What they said: {message.content[:300]}" ) messages = [{"role": "user", "content": user_prompt}] effective_prompt = _UNBLOCK_REDIRECT_PROMPT if recent: avoid_block = "\n".join(f"- {r}" for r in recent) effective_prompt += ( "\n\nIMPORTANT — you recently sent these redirects in the same channel. " "Do NOT repeat any of these. Be completely different.\n" + avoid_block ) try: response = await bot.llm_chat.chat(messages, effective_prompt) except Exception: logger.exception("LLM unblock redirect generation failed") return None if response: response = _strip_brackets(response) return response if response else None def _static_fallback(message: discord.Message, count: int) -> str: """Pick a static template message as fallback.""" if count >= 2: pool = DEFAULT_UNBLOCK_NUDGES else: pool = DEFAULT_UNBLOCK_REMINDS return random.choice(pool).format( username=message.author.display_name, count=count, ) async def handle_unblock_nag( bot, message: discord.Message, dirty_users: set[int], ): """Handle a detected unblock-nagging message.""" config = bot.config.get("unblock_nag", {}) if not config.get("enabled", True): return dry_run = bot.config.get("monitoring", {}).get("dry_run", False) if dry_run: return tracker = bot.drama_tracker user_id = message.author.id cooldown = config.get("remind_cooldown_minutes", 30) if not tracker.can_unblock_remind(user_id, cooldown): return count = tracker.record_unblock_nag(user_id) action_type = "unblock_nudge" if count >= 2 else "unblock_remind" # Generate the redirect message use_llm = config.get("use_llm", True) redirect_text = None if use_llm: redirect_text = await _generate_llm_redirect(bot, message, count) if redirect_text: _record_redirect(message.channel.id, redirect_text) else: redirect_text = _static_fallback(message, count) await message.channel.send(redirect_text) await log_action( message.guild, f"**UNBLOCK {'NUDGE' if count >= 2 else 'REMIND'}** | {message.author.mention} | " f"Nag count: {count}", ) logger.info("Unblock %s for %s (count %d)", action_type.replace("unblock_", ""), message.author, count) asyncio.create_task(bot.db.save_action( guild_id=message.guild.id, user_id=user_id, username=message.author.display_name, action_type=action_type, message_id=None, details=f"unblock_nag_count={count}", )) save_user_state(bot, dirty_users, user_id)