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>
124 lines
3.9 KiB
Python
124 lines
3.9 KiB
Python
import asyncio
|
|
import logging
|
|
from datetime import timedelta
|
|
|
|
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 mute_user(
|
|
bot, message: discord.Message, score: float,
|
|
categories: list[str], db_message_id: int | None, dirty_users: set[int],
|
|
):
|
|
member = message.author
|
|
if not isinstance(member, discord.Member):
|
|
return
|
|
|
|
if not message.guild.me.guild_permissions.moderate_members:
|
|
logger.warning("Missing moderate_members permission, cannot mute.")
|
|
return
|
|
|
|
offense_num = bot.drama_tracker.record_offense(member.id)
|
|
timeout_config = bot.config.get("timeouts", {})
|
|
escalation = timeout_config.get("escalation_minutes", [5, 15, 30, 60])
|
|
idx = min(offense_num - 1, len(escalation) - 1)
|
|
duration_minutes = escalation[idx]
|
|
|
|
try:
|
|
await member.timeout(
|
|
timedelta(minutes=duration_minutes),
|
|
reason=f"BCS auto-mute: drama score {score:.2f}",
|
|
)
|
|
except discord.Forbidden:
|
|
logger.warning("Cannot timeout %s — role hierarchy issue.", member)
|
|
return
|
|
except discord.HTTPException as e:
|
|
logger.error("Failed to timeout %s: %s", member, e)
|
|
return
|
|
|
|
messages_config = bot.config.get("messages", {})
|
|
cat_str = ", ".join(c for c in categories if c != "none") or "general negativity"
|
|
|
|
embed = discord.Embed(
|
|
title=messages_config.get("mute_title", "BREEHAVIOR ALERT"),
|
|
description=messages_config.get("mute_description", "").format(
|
|
username=member.display_name,
|
|
duration=f"{duration_minutes} minutes",
|
|
score=f"{score:.2f}",
|
|
categories=cat_str,
|
|
),
|
|
color=discord.Color.red(),
|
|
)
|
|
embed.set_footer(
|
|
text=f"Offense #{offense_num} | Timeout: {duration_minutes}m"
|
|
)
|
|
|
|
await message.channel.send(embed=embed)
|
|
await log_action(
|
|
message.guild,
|
|
f"**MUTE** | {member.mention} | Score: {score:.2f} | "
|
|
f"Duration: {duration_minutes}m | Offense #{offense_num} | "
|
|
f"Categories: {cat_str}",
|
|
)
|
|
|
|
logger.info(
|
|
"Muted %s for %d minutes (offense #%d, score %.2f)",
|
|
member, duration_minutes, offense_num, score,
|
|
)
|
|
|
|
asyncio.create_task(bot.db.save_action(
|
|
guild_id=message.guild.id,
|
|
user_id=member.id,
|
|
username=member.display_name,
|
|
action_type="mute",
|
|
message_id=db_message_id,
|
|
details=f"duration={duration_minutes}m offense={offense_num} score={score:.2f} categories={cat_str}",
|
|
))
|
|
save_user_state(bot, dirty_users, member.id)
|
|
|
|
|
|
async def warn_user(
|
|
bot, message: discord.Message, score: float,
|
|
db_message_id: int | None, dirty_users: set[int],
|
|
):
|
|
timeout_config = bot.config.get("timeouts", {})
|
|
cooldown = timeout_config.get("warning_cooldown_minutes", 5)
|
|
|
|
if not bot.drama_tracker.can_warn(message.author.id, cooldown):
|
|
return
|
|
|
|
bot.drama_tracker.record_warning(message.author.id)
|
|
|
|
try:
|
|
await message.add_reaction("\u26a0\ufe0f")
|
|
except discord.HTTPException:
|
|
pass
|
|
|
|
messages_config = bot.config.get("messages", {})
|
|
warning_text = messages_config.get(
|
|
"warning",
|
|
"Easy there, {username}. The Breehavior Monitor is watching.",
|
|
).format(username=message.author.display_name)
|
|
|
|
await message.channel.send(warning_text)
|
|
await log_action(
|
|
message.guild,
|
|
f"**WARNING** | {message.author.mention} | Score: {score:.2f}",
|
|
)
|
|
|
|
logger.info("Warned %s (score %.2f)", message.author, score)
|
|
|
|
asyncio.create_task(bot.db.save_action(
|
|
guild_id=message.guild.id,
|
|
user_id=message.author.id,
|
|
username=message.author.display_name,
|
|
action_type="warning",
|
|
message_id=db_message_id,
|
|
details=f"score={score:.2f}",
|
|
))
|
|
save_user_state(bot, dirty_users, message.author.id)
|