Convert cogs/sentiment.py (1050 lines) into cogs/sentiment/ package: - __init__.py (656 lines): core SentimentCog with new _process_finding() that deduplicates the per-user finding loop from _process_buffered and _run_mention_scan (~90 lines each → single shared method) - actions.py: mute_user, warn_user - topic_drift.py: handle_topic_drift - channel_redirect.py: handle_channel_redirect, build_channel_context - coherence.py: handle_coherence_alert - log_utils.py: log_analysis, log_action, score_color - state.py: save_user_state, flush_dirty_states All extracted modules use plain async functions (not methods) receiving bot/config as parameters. Named log_utils.py to avoid shadowing stdlib logging. Also update CLAUDE.md with comprehensive project documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
112 lines
4.2 KiB
Python
112 lines
4.2 KiB
Python
import asyncio
|
|
import logging
|
|
|
|
import discord
|
|
|
|
from cogs.sentiment.log_utils import log_action
|
|
from cogs.sentiment.state import save_user_state
|
|
|
|
logger = logging.getLogger("bcs.sentiment")
|
|
|
|
|
|
async def handle_topic_drift(
|
|
bot, message: discord.Message, topic_category: str, topic_reasoning: str,
|
|
db_message_id: int | None, dirty_users: set[int],
|
|
):
|
|
config = bot.config.get("topic_drift", {})
|
|
if not config.get("enabled", True):
|
|
return
|
|
|
|
ignored = config.get("ignored_channels", [])
|
|
if message.channel.id in ignored or getattr(message.channel, "name", "") in ignored:
|
|
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", 10)
|
|
|
|
if not tracker.can_topic_remind(user_id, cooldown):
|
|
return
|
|
|
|
count = tracker.record_off_topic(user_id)
|
|
escalation_threshold = config.get("escalation_count", 3)
|
|
messages_config = bot.config.get("messages", {})
|
|
|
|
if count >= escalation_threshold and not tracker.was_owner_notified(user_id):
|
|
tracker.mark_owner_notified(user_id)
|
|
owner = message.guild.owner
|
|
if owner:
|
|
dm_text = messages_config.get(
|
|
"topic_owner_dm",
|
|
"Heads up: {username} keeps going off-topic in #{channel}. Reminded {count} times.",
|
|
).format(
|
|
username=message.author.display_name,
|
|
channel=message.channel.name,
|
|
count=count,
|
|
)
|
|
try:
|
|
await owner.send(dm_text)
|
|
except discord.HTTPException:
|
|
logger.warning("Could not DM server owner about topic drift.")
|
|
|
|
await log_action(
|
|
message.guild,
|
|
f"**TOPIC DRIFT \u2014 OWNER NOTIFIED** | {message.author.mention} | "
|
|
f"Off-topic count: {count} | Category: {topic_category}",
|
|
)
|
|
logger.info("Notified owner about %s topic drift (count %d)", 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="topic_escalation", message_id=db_message_id,
|
|
details=f"off_topic_count={count} category={topic_category}",
|
|
))
|
|
save_user_state(bot, dirty_users, user_id)
|
|
|
|
elif count >= 2:
|
|
nudge_text = messages_config.get(
|
|
"topic_nudge",
|
|
"{username}, let's keep it to gaming talk in here.",
|
|
).format(username=message.author.display_name)
|
|
await message.channel.send(nudge_text)
|
|
await log_action(
|
|
message.guild,
|
|
f"**TOPIC NUDGE** | {message.author.mention} | "
|
|
f"Off-topic count: {count} | Category: {topic_category}",
|
|
)
|
|
logger.info("Topic nudge for %s (count %d)", 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="topic_nudge", message_id=db_message_id,
|
|
details=f"off_topic_count={count} category={topic_category}",
|
|
))
|
|
save_user_state(bot, dirty_users, user_id)
|
|
|
|
else:
|
|
remind_text = messages_config.get(
|
|
"topic_remind",
|
|
"Hey {username}, this is a gaming server \u2014 maybe take the personal stuff to DMs?",
|
|
).format(username=message.author.display_name)
|
|
await message.channel.send(remind_text)
|
|
await log_action(
|
|
message.guild,
|
|
f"**TOPIC REMIND** | {message.author.mention} | "
|
|
f"Category: {topic_category} | {topic_reasoning}",
|
|
)
|
|
logger.info("Topic remind for %s (count %d)", 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="topic_remind", message_id=db_message_id,
|
|
details=f"off_topic_count={count} category={topic_category} reasoning={topic_reasoning}",
|
|
))
|
|
save_user_state(bot, dirty_users, user_id)
|