Compare commits
4 Commits
9872c36b97
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| f79de0ea04 | |||
| 733b86b947 | |||
| f7dfb7931a | |||
| a836584940 |
@@ -161,6 +161,31 @@ class CommandsCog(commands.Cog):
|
|||||||
|
|
||||||
await interaction.response.send_message(embed=embed, ephemeral=True)
|
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||||
|
|
||||||
|
@app_commands.command(
|
||||||
|
name="bcs-pause",
|
||||||
|
description="Pause or resume bot monitoring. (Admin only)",
|
||||||
|
)
|
||||||
|
@app_commands.default_permissions(administrator=True)
|
||||||
|
async def bcs_pause(self, interaction: discord.Interaction):
|
||||||
|
if not self._is_admin(interaction):
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"Admin only.", ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
monitoring = self.bot.config.setdefault("monitoring", {})
|
||||||
|
currently_enabled = monitoring.get("enabled", True)
|
||||||
|
monitoring["enabled"] = not currently_enabled
|
||||||
|
|
||||||
|
if monitoring["enabled"]:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"Monitoring **resumed**.", ephemeral=True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"Monitoring **paused**.", ephemeral=True
|
||||||
|
)
|
||||||
|
|
||||||
@app_commands.command(
|
@app_commands.command(
|
||||||
name="bcs-threshold",
|
name="bcs-threshold",
|
||||||
description="Adjust warning and mute thresholds. (Admin only)",
|
description="Adjust warning and mute thresholds. (Admin only)",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from cogs.sentiment.coherence import handle_coherence_alert
|
|||||||
from cogs.sentiment.log_utils import log_analysis
|
from cogs.sentiment.log_utils import log_analysis
|
||||||
from cogs.sentiment.state import flush_dirty_states
|
from cogs.sentiment.state import flush_dirty_states
|
||||||
from cogs.sentiment.topic_drift import handle_topic_drift
|
from cogs.sentiment.topic_drift import handle_topic_drift
|
||||||
|
from cogs.sentiment.unblock_nag import handle_unblock_nag, matches_unblock_nag
|
||||||
|
|
||||||
logger = logging.getLogger("bcs.sentiment")
|
logger = logging.getLogger("bcs.sentiment")
|
||||||
|
|
||||||
@@ -153,6 +154,12 @@ class SentimentCog(commands.Cog):
|
|||||||
if not message.content or not message.content.strip():
|
if not message.content or not message.content.strip():
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Check for unblock nagging (keyword-based, no LLM needed for detection)
|
||||||
|
if matches_unblock_nag(message.content):
|
||||||
|
asyncio.create_task(handle_unblock_nag(
|
||||||
|
self.bot, message, self._dirty_users,
|
||||||
|
))
|
||||||
|
|
||||||
# Buffer the message and start/reset debounce timer (per-channel)
|
# Buffer the message and start/reset debounce timer (per-channel)
|
||||||
channel_id = message.channel.id
|
channel_id = message.channel.id
|
||||||
if channel_id not in self._message_buffer:
|
if channel_id not in self._message_buffer:
|
||||||
@@ -443,8 +450,7 @@ class SentimentCog(commands.Cog):
|
|||||||
db_message_id, self._dirty_users,
|
db_message_id, self._dirty_users,
|
||||||
)
|
)
|
||||||
|
|
||||||
detected_game = finding.get("detected_game")
|
elif (detected_game := finding.get("detected_game")) and game_channels and not dry_run:
|
||||||
if detected_game and game_channels and not dry_run:
|
|
||||||
await handle_channel_redirect(
|
await handle_channel_redirect(
|
||||||
self.bot, user_ref_msg, detected_game, game_channels,
|
self.bot, user_ref_msg, detected_game, game_channels,
|
||||||
db_message_id, self._redirect_cooldowns,
|
db_message_id, self._redirect_cooldowns,
|
||||||
|
|||||||
@@ -16,20 +16,20 @@ _PROMPTS_DIR = Path(__file__).resolve().parent.parent.parent / "prompts"
|
|||||||
_TOPIC_REDIRECT_PROMPT = (_PROMPTS_DIR / "topic_redirect.txt").read_text(encoding="utf-8")
|
_TOPIC_REDIRECT_PROMPT = (_PROMPTS_DIR / "topic_redirect.txt").read_text(encoding="utf-8")
|
||||||
|
|
||||||
DEFAULT_TOPIC_REMINDS = [
|
DEFAULT_TOPIC_REMINDS = [
|
||||||
"Hey {username}, this is a gaming server 🎮 — maybe take the personal stuff to DMs?",
|
"Hey {username}, this is a gaming server 🎮 — take the personal stuff to {channel}.",
|
||||||
"{username}, sir this is a gaming channel.",
|
"{username}, sir this is a gaming channel. {channel} is right there.",
|
||||||
"Hey {username}, I don't remember this being a therapy session. Gaming talk, please. 🎮",
|
"Hey {username}, I don't remember this being a therapy session. Take it to {channel}. 🎮",
|
||||||
"{username}, I'm gonna need you to take that energy to DMs. This channel has a vibe to protect.",
|
"{username}, I'm gonna need you to take that energy to {channel}. This channel has a vibe to protect.",
|
||||||
"Not to be dramatic {username}, but this is wildly off-topic. Back to gaming? 🎮",
|
"Not to be dramatic {username}, but this is wildly off-topic. {channel} exists for a reason. 🎮",
|
||||||
]
|
]
|
||||||
|
|
||||||
DEFAULT_TOPIC_NUDGES = [
|
DEFAULT_TOPIC_NUDGES = [
|
||||||
"{username}, we've been over this. Gaming. Channel. Please. 🎮",
|
"{username}, we've been over this. Gaming. Channel. {channel} for the rest. 🎮",
|
||||||
"{username}, you keep drifting off-topic like it's a speedrun category. Reel it in.",
|
"{username}, you keep drifting off-topic like it's a speedrun category. {channel}. Now.",
|
||||||
"Babe. {username}. The gaming channel. We talked about this. 😭",
|
"Babe. {username}. The gaming channel. We talked about this. Go to {channel}. 😭",
|
||||||
"{username}, I will not ask again (I will definitely ask again). Stay on topic. 🎮",
|
"{username}, I will not ask again (I will definitely ask again). {channel} for off-topic. 🎮",
|
||||||
"{username}, at this point I'm keeping score. That's off-topic strike {count}. Gaming talk only!",
|
"{username}, at this point I'm keeping score. That's off-topic strike {count}. {channel} is waiting.",
|
||||||
"Look, {username}, I love the enthusiasm but this ain't the channel for it. Back to games. 🎮",
|
"Look, {username}, I love the enthusiasm but this ain't the channel for it. {channel}. 🎮",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Per-channel deque of recent LLM-generated redirect messages (for variety)
|
# Per-channel deque of recent LLM-generated redirect messages (for variety)
|
||||||
@@ -57,7 +57,7 @@ def _strip_brackets(text: str) -> str:
|
|||||||
|
|
||||||
async def _generate_llm_redirect(
|
async def _generate_llm_redirect(
|
||||||
bot, message: discord.Message, topic_category: str,
|
bot, message: discord.Message, topic_category: str,
|
||||||
topic_reasoning: str, count: int,
|
topic_reasoning: str, count: int, redirect_mention: str = "",
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
"""Ask the LLM chat model to generate a topic redirect message."""
|
"""Ask the LLM chat model to generate a topic redirect message."""
|
||||||
recent = _get_recent_redirects(message.channel.id)
|
recent = _get_recent_redirects(message.channel.id)
|
||||||
@@ -70,6 +70,8 @@ async def _generate_llm_redirect(
|
|||||||
f"Off-topic strike count: {count}\n"
|
f"Off-topic strike count: {count}\n"
|
||||||
f"What they said: {message.content[:300]}"
|
f"What they said: {message.content[:300]}"
|
||||||
)
|
)
|
||||||
|
if redirect_mention:
|
||||||
|
user_prompt += f"\nRedirect channel: {redirect_mention}"
|
||||||
|
|
||||||
messages = [{"role": "user", "content": user_prompt}]
|
messages = [{"role": "user", "content": user_prompt}]
|
||||||
|
|
||||||
@@ -96,7 +98,7 @@ async def _generate_llm_redirect(
|
|||||||
return response if response else None
|
return response if response else None
|
||||||
|
|
||||||
|
|
||||||
def _static_fallback(bot, message: discord.Message, count: int) -> str:
|
def _static_fallback(bot, message: discord.Message, count: int, redirect_mention: str = "") -> str:
|
||||||
"""Pick a static template message as fallback."""
|
"""Pick a static template message as fallback."""
|
||||||
messages_config = bot.config.get("messages", {})
|
messages_config = bot.config.get("messages", {})
|
||||||
if count >= 2:
|
if count >= 2:
|
||||||
@@ -109,6 +111,7 @@ def _static_fallback(bot, message: discord.Message, count: int) -> str:
|
|||||||
pool = [pool]
|
pool = [pool]
|
||||||
return random.choice(pool).format(
|
return random.choice(pool).format(
|
||||||
username=message.author.display_name, count=count,
|
username=message.author.display_name, count=count,
|
||||||
|
channel=redirect_mention or "the right channel",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -138,18 +141,26 @@ async def handle_topic_drift(
|
|||||||
count = tracker.record_off_topic(user_id)
|
count = tracker.record_off_topic(user_id)
|
||||||
action_type = "topic_nudge" if count >= 2 else "topic_remind"
|
action_type = "topic_nudge" if count >= 2 else "topic_remind"
|
||||||
|
|
||||||
|
# Resolve redirect channel mention
|
||||||
|
redirect_mention = ""
|
||||||
|
redirect_name = config.get("redirect_channel")
|
||||||
|
if redirect_name and message.guild:
|
||||||
|
ch = discord.utils.get(message.guild.text_channels, name=redirect_name)
|
||||||
|
if ch:
|
||||||
|
redirect_mention = ch.mention
|
||||||
|
|
||||||
# Generate the redirect message
|
# Generate the redirect message
|
||||||
use_llm = config.get("use_llm", False)
|
use_llm = config.get("use_llm", False)
|
||||||
redirect_text = None
|
redirect_text = None
|
||||||
if use_llm:
|
if use_llm:
|
||||||
redirect_text = await _generate_llm_redirect(
|
redirect_text = await _generate_llm_redirect(
|
||||||
bot, message, topic_category, topic_reasoning, count,
|
bot, message, topic_category, topic_reasoning, count, redirect_mention,
|
||||||
)
|
)
|
||||||
|
|
||||||
if redirect_text:
|
if redirect_text:
|
||||||
_record_redirect(message.channel.id, redirect_text)
|
_record_redirect(message.channel.id, redirect_text)
|
||||||
else:
|
else:
|
||||||
redirect_text = _static_fallback(bot, message, count)
|
redirect_text = _static_fallback(bot, message, count, redirect_mention)
|
||||||
|
|
||||||
await message.channel.send(redirect_text)
|
await message.channel.send(redirect_text)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
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)
|
||||||
+18
-12
@@ -30,11 +30,17 @@ game_channels:
|
|||||||
topic_drift:
|
topic_drift:
|
||||||
enabled: true
|
enabled: true
|
||||||
use_llm: true # Generate redirect messages via LLM instead of static templates
|
use_llm: true # Generate redirect messages via LLM instead of static templates
|
||||||
|
redirect_channel: "general" # Channel to suggest for off-topic chat
|
||||||
ignored_channels: ["general"] # Channel names or IDs to skip topic drift monitoring
|
ignored_channels: ["general"] # Channel names or IDs to skip topic drift monitoring
|
||||||
remind_cooldown_minutes: 10 # Don't remind same user more than once per this window
|
remind_cooldown_minutes: 10 # Don't remind same user more than once per this window
|
||||||
escalation_count: 3 # After this many reminds, DM the server owner
|
escalation_count: 3 # After this many reminds, DM the server owner
|
||||||
reset_minutes: 60 # Reset off-topic count after this much on-topic behavior
|
reset_minutes: 60 # Reset off-topic count after this much on-topic behavior
|
||||||
|
|
||||||
|
unblock_nag:
|
||||||
|
enabled: true
|
||||||
|
use_llm: true # Generate redirect messages via LLM instead of static templates
|
||||||
|
remind_cooldown_minutes: 30 # Don't remind same user more than once per this window
|
||||||
|
|
||||||
mention_scan:
|
mention_scan:
|
||||||
enabled: true
|
enabled: true
|
||||||
scan_messages: 30 # Messages to scan per mention trigger
|
scan_messages: 30 # Messages to scan per mention trigger
|
||||||
@@ -51,18 +57,18 @@ messages:
|
|||||||
mute_title: "\U0001F6A8 BREEHAVIOR ALERT \U0001F6A8"
|
mute_title: "\U0001F6A8 BREEHAVIOR ALERT \U0001F6A8"
|
||||||
mute_description: "{username} has been placed in timeout for {duration}.\n\nReason: Sustained elevated drama levels detected.\nDrama Score: {score}/1.0\nCategories: {categories}\n\nCool down and come back when you've resolved your skill issues."
|
mute_description: "{username} has been placed in timeout for {duration}.\n\nReason: Sustained elevated drama levels detected.\nDrama Score: {score}/1.0\nCategories: {categories}\n\nCool down and come back when you've resolved your skill issues."
|
||||||
topic_reminds:
|
topic_reminds:
|
||||||
- "Hey {username}, this is a gaming server 🎮 — maybe take the personal stuff to DMs?"
|
- "Hey {username}, this is a gaming server 🎮 — take the personal stuff to {channel}."
|
||||||
- "{username}, sir this is a gaming channel."
|
- "{username}, sir this is a gaming channel. {channel} is right there."
|
||||||
- "Hey {username}, I don't remember this being a therapy session. Gaming talk, please. 🎮"
|
- "Hey {username}, I don't remember this being a therapy session. Take it to {channel}. 🎮"
|
||||||
- "{username}, I'm gonna need you to take that energy to DMs. This channel has a vibe to protect."
|
- "{username}, I'm gonna need you to take that energy to {channel}. This channel has a vibe to protect."
|
||||||
- "Not to be dramatic {username}, but this is wildly off-topic. Back to gaming? 🎮"
|
- "Not to be dramatic {username}, but this is wildly off-topic. {channel} exists for a reason. 🎮"
|
||||||
topic_nudges:
|
topic_nudges:
|
||||||
- "{username}, we've been over this. Gaming. Channel. Please. 🎮"
|
- "{username}, we've been over this. Gaming. Channel. {channel} for the rest. 🎮"
|
||||||
- "{username}, you keep drifting off-topic like it's a speedrun category. Reel it in."
|
- "{username}, you keep drifting off-topic like it's a speedrun category. {channel}. Now."
|
||||||
- "Babe. {username}. The gaming channel. We talked about this. 😭"
|
- "Babe. {username}. The gaming channel. We talked about this. Go to {channel}. 😭"
|
||||||
- "{username}, I will not ask again (I will definitely ask again). Stay on topic. 🎮"
|
- "{username}, I will not ask again (I will definitely ask again). {channel} for off-topic. 🎮"
|
||||||
- "{username}, at this point I'm keeping score. That's off-topic strike {count}. Gaming talk only!"
|
- "{username}, at this point I'm keeping score. That's off-topic strike {count}. {channel} is waiting."
|
||||||
- "Look, {username}, I love the enthusiasm but this ain't the channel for it. Back to games. 🎮"
|
- "Look, {username}, I love the enthusiasm but this ain't the channel for it. {channel}. 🎮"
|
||||||
topic_owner_dm: "Heads up: {username} keeps going off-topic with personal drama in #{channel}. They've been reminded {count} times. Might need a word."
|
topic_owner_dm: "Heads up: {username} keeps going off-topic with personal drama in #{channel}. They've been reminded {count} times. Might need a word."
|
||||||
channel_redirect: "Hey {username}, that sounds like {game} talk — head over to {channel} for that!"
|
channel_redirect: "Hey {username}, that sounds like {game} talk — head over to {channel} for that!"
|
||||||
|
|
||||||
@@ -176,7 +182,7 @@ coherence:
|
|||||||
default: "You okay there, {username}? That message was... something."
|
default: "You okay there, {username}? That message was... something."
|
||||||
|
|
||||||
reactions:
|
reactions:
|
||||||
enabled: true
|
enabled: false
|
||||||
chance: 0.15 # Probability of evaluating a message for reaction
|
chance: 0.15 # Probability of evaluating a message for reaction
|
||||||
cooldown_seconds: 45 # Per-channel cooldown between reactions
|
cooldown_seconds: 45 # Per-channel cooldown between reactions
|
||||||
excluded_channels: [] # Channel names or IDs to skip reactions in
|
excluded_channels: [] # Channel names or IDs to skip reactions in
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ You're the hall monitor of "Skill Issue Support Group" (gaming Discord). Someone
|
|||||||
|
|
||||||
- Snarky and playful, not mean. Reference what they actually said — don't be vague.
|
- Snarky and playful, not mean. Reference what they actually said — don't be vague.
|
||||||
- Casual, like a friend ribbing them. If strike count 2+, escalate the sass.
|
- Casual, like a friend ribbing them. If strike count 2+, escalate the sass.
|
||||||
|
- If a redirect channel is provided, tell them to take it there. Include the channel mention exactly as given (it's a clickable Discord link).
|
||||||
- Max 1 emoji. No hashtags, brackets, metadata, or AI references.
|
- Max 1 emoji. No hashtags, brackets, metadata, or AI references.
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
You're the hall monitor of "Skill Issue Support Group" (gaming Discord). Someone is asking to be unblocked — again.
|
||||||
|
Write 1-2 sentences shutting it down. The message should make it clear that begging in chat won't help.
|
||||||
|
|
||||||
|
- Snarky and playful, not cruel. Reference what they actually said — don't be vague.
|
||||||
|
- Casual, like a friend telling them to knock it off. If nag count is 2+, escalate the sass.
|
||||||
|
- The core message: block/unblock decisions are between them and the person who blocked them (or admins). Bringing it up in chat repeatedly is not going to change anything.
|
||||||
|
- Max 1 emoji. No hashtags, brackets, metadata, or AI references.
|
||||||
@@ -29,6 +29,9 @@ class UserDrama:
|
|||||||
coherence_scores: list[float] = field(default_factory=list)
|
coherence_scores: list[float] = field(default_factory=list)
|
||||||
baseline_coherence: float = 0.85
|
baseline_coherence: float = 0.85
|
||||||
last_coherence_alert_time: float = 0.0
|
last_coherence_alert_time: float = 0.0
|
||||||
|
# Unblock nagging tracking
|
||||||
|
unblock_nag_count: int = 0
|
||||||
|
last_unblock_nag_time: float = 0.0
|
||||||
# Per-user LLM notes
|
# Per-user LLM notes
|
||||||
notes: str = ""
|
notes: str = ""
|
||||||
# Known aliases/nicknames
|
# Known aliases/nicknames
|
||||||
@@ -256,6 +259,21 @@ class DramaTracker:
|
|||||||
"""Return {user_id: [aliases]} for all users that have aliases set."""
|
"""Return {user_id: [aliases]} for all users that have aliases set."""
|
||||||
return {uid: user.aliases for uid, user in self._users.items() if user.aliases}
|
return {uid: user.aliases for uid, user in self._users.items() if user.aliases}
|
||||||
|
|
||||||
|
def record_unblock_nag(self, user_id: int) -> int:
|
||||||
|
user = self.get_user(user_id)
|
||||||
|
user.unblock_nag_count += 1
|
||||||
|
user.last_unblock_nag_time = time.time()
|
||||||
|
return user.unblock_nag_count
|
||||||
|
|
||||||
|
def can_unblock_remind(self, user_id: int, cooldown_minutes: int) -> bool:
|
||||||
|
user = self.get_user(user_id)
|
||||||
|
if user.last_unblock_nag_time == 0.0:
|
||||||
|
return True
|
||||||
|
return time.time() - user.last_unblock_nag_time > cooldown_minutes * 60
|
||||||
|
|
||||||
|
def get_unblock_nag_count(self, user_id: int) -> int:
|
||||||
|
return self.get_user(user_id).unblock_nag_count
|
||||||
|
|
||||||
def reset_off_topic(self, user_id: int) -> None:
|
def reset_off_topic(self, user_id: int) -> None:
|
||||||
user = self.get_user(user_id)
|
user = self.get_user(user_id)
|
||||||
user.off_topic_count = 0
|
user.off_topic_count = 0
|
||||||
|
|||||||
Reference in New Issue
Block a user