feat: move user aliases from config to DB with /bcs-alias command
Aliases now stored in UserState table instead of config.yaml. Adds Aliases column (NVARCHAR 500), loads on startup, persists via flush. New /bcs-alias slash command (view/set/clear) for managing nicknames. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -250,6 +250,7 @@ class CommandsCog(commands.Cog):
|
|||||||
off_topic_count=user_data.off_topic_count,
|
off_topic_count=user_data.off_topic_count,
|
||||||
baseline_coherence=user_data.baseline_coherence,
|
baseline_coherence=user_data.baseline_coherence,
|
||||||
user_notes=user_data.notes or None,
|
user_notes=user_data.notes or None,
|
||||||
|
aliases=",".join(user_data.aliases) if user_data.aliases else None,
|
||||||
))
|
))
|
||||||
status = "now immune" if is_immune else "no longer immune"
|
status = "now immune" if is_immune else "no longer immune"
|
||||||
await interaction.response.send_message(
|
await interaction.response.send_message(
|
||||||
@@ -501,6 +502,7 @@ class CommandsCog(commands.Cog):
|
|||||||
off_topic_count=user_data.off_topic_count,
|
off_topic_count=user_data.off_topic_count,
|
||||||
baseline_coherence=user_data.baseline_coherence,
|
baseline_coherence=user_data.baseline_coherence,
|
||||||
user_notes=user_data.notes or None,
|
user_notes=user_data.notes or None,
|
||||||
|
aliases=",".join(user_data.aliases) if user_data.aliases else None,
|
||||||
))
|
))
|
||||||
await interaction.response.send_message(
|
await interaction.response.send_message(
|
||||||
f"Note added for {user.display_name}.", ephemeral=True
|
f"Note added for {user.display_name}.", ephemeral=True
|
||||||
@@ -516,11 +518,86 @@ class CommandsCog(commands.Cog):
|
|||||||
off_topic_count=user_data.off_topic_count,
|
off_topic_count=user_data.off_topic_count,
|
||||||
baseline_coherence=user_data.baseline_coherence,
|
baseline_coherence=user_data.baseline_coherence,
|
||||||
user_notes=None,
|
user_notes=None,
|
||||||
|
aliases=",".join(user_data.aliases) if user_data.aliases else None,
|
||||||
))
|
))
|
||||||
await interaction.response.send_message(
|
await interaction.response.send_message(
|
||||||
f"Notes cleared for {user.display_name}.", ephemeral=True
|
f"Notes cleared for {user.display_name}.", ephemeral=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@app_commands.command(
|
||||||
|
name="bcs-alias",
|
||||||
|
description="Manage nicknames/aliases for a user. (Admin only)",
|
||||||
|
)
|
||||||
|
@app_commands.default_permissions(administrator=True)
|
||||||
|
@app_commands.describe(
|
||||||
|
action="What to do with aliases",
|
||||||
|
user="The user whose aliases to manage",
|
||||||
|
text="Comma-separated aliases (only used with 'set')",
|
||||||
|
)
|
||||||
|
@app_commands.choices(action=[
|
||||||
|
app_commands.Choice(name="view", value="view"),
|
||||||
|
app_commands.Choice(name="set", value="set"),
|
||||||
|
app_commands.Choice(name="clear", value="clear"),
|
||||||
|
])
|
||||||
|
async def bcs_alias(
|
||||||
|
self,
|
||||||
|
interaction: discord.Interaction,
|
||||||
|
action: app_commands.Choice[str],
|
||||||
|
user: discord.Member,
|
||||||
|
text: str | None = None,
|
||||||
|
):
|
||||||
|
if not self._is_admin(interaction):
|
||||||
|
await interaction.response.send_message("Admin only.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
if action.value == "view":
|
||||||
|
aliases = self.bot.drama_tracker.get_user_aliases(user.id)
|
||||||
|
desc = ", ".join(aliases) if aliases else "_No aliases set._"
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=f"Aliases: {user.display_name}",
|
||||||
|
description=desc,
|
||||||
|
color=discord.Color.blue(),
|
||||||
|
)
|
||||||
|
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||||
|
|
||||||
|
elif action.value == "set":
|
||||||
|
if not text:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"Provide `text` with comma-separated aliases (e.g. `Glam, G`).", ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
aliases = [a.strip() for a in text.split(",") if a.strip()]
|
||||||
|
self.bot.drama_tracker.set_user_aliases(user.id, aliases)
|
||||||
|
user_data = self.bot.drama_tracker.get_user(user.id)
|
||||||
|
asyncio.create_task(self.bot.db.save_user_state(
|
||||||
|
user_id=user.id,
|
||||||
|
offense_count=user_data.offense_count,
|
||||||
|
immune=user_data.immune,
|
||||||
|
off_topic_count=user_data.off_topic_count,
|
||||||
|
baseline_coherence=user_data.baseline_coherence,
|
||||||
|
user_notes=user_data.notes or None,
|
||||||
|
aliases=",".join(aliases),
|
||||||
|
))
|
||||||
|
await interaction.response.send_message(
|
||||||
|
f"Aliases for {user.display_name} set to: {', '.join(aliases)}", ephemeral=True
|
||||||
|
)
|
||||||
|
|
||||||
|
elif action.value == "clear":
|
||||||
|
self.bot.drama_tracker.set_user_aliases(user.id, [])
|
||||||
|
user_data = self.bot.drama_tracker.get_user(user.id)
|
||||||
|
asyncio.create_task(self.bot.db.save_user_state(
|
||||||
|
user_id=user.id,
|
||||||
|
offense_count=user_data.offense_count,
|
||||||
|
immune=user_data.immune,
|
||||||
|
off_topic_count=user_data.off_topic_count,
|
||||||
|
baseline_coherence=user_data.baseline_coherence,
|
||||||
|
user_notes=user_data.notes or None,
|
||||||
|
aliases=None,
|
||||||
|
))
|
||||||
|
await interaction.response.send_message(
|
||||||
|
f"Aliases cleared for {user.display_name}.", ephemeral=True
|
||||||
|
)
|
||||||
|
|
||||||
@app_commands.command(
|
@app_commands.command(
|
||||||
name="bcs-mode",
|
name="bcs-mode",
|
||||||
description="Switch the bot's personality mode.",
|
description="Switch the bot's personality mode.",
|
||||||
|
|||||||
@@ -253,18 +253,18 @@ class SentimentCog(commands.Cog):
|
|||||||
"""Replace display name keys with anonymous keys in user notes map."""
|
"""Replace display name keys with anonymous keys in user notes map."""
|
||||||
return {anon_map.get(name, name): notes for name, notes in user_notes_map.items()}
|
return {anon_map.get(name, name): notes for name, notes in user_notes_map.items()}
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _build_alias_context(
|
def _build_alias_context(
|
||||||
|
self,
|
||||||
messages: list[discord.Message],
|
messages: list[discord.Message],
|
||||||
anon_map: dict[str, str],
|
anon_map: dict[str, str],
|
||||||
alias_config: dict,
|
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Build anonymized alias context string for the LLM.
|
"""Build anonymized alias context string for the LLM.
|
||||||
|
|
||||||
Maps user IDs from messages to their known nicknames using the
|
Maps user IDs from messages to their known nicknames from
|
||||||
config, then replaces display names with anonymous keys.
|
DramaTracker, then replaces display names with anonymous keys.
|
||||||
"""
|
"""
|
||||||
if not alias_config:
|
all_aliases = self.bot.drama_tracker.get_all_aliases()
|
||||||
|
if not all_aliases:
|
||||||
return ""
|
return ""
|
||||||
lines = []
|
lines = []
|
||||||
seen_ids: set[int] = set()
|
seen_ids: set[int] = set()
|
||||||
@@ -273,14 +273,13 @@ class SentimentCog(commands.Cog):
|
|||||||
if uid in seen_ids:
|
if uid in seen_ids:
|
||||||
continue
|
continue
|
||||||
seen_ids.add(uid)
|
seen_ids.add(uid)
|
||||||
aliases = alias_config.get(uid)
|
aliases = all_aliases.get(uid)
|
||||||
if aliases:
|
if aliases:
|
||||||
anon_key = anon_map.get(msg.author.display_name, msg.author.display_name)
|
anon_key = anon_map.get(msg.author.display_name, msg.author.display_name)
|
||||||
lines.append(f" {anon_key} is also known as: {', '.join(aliases)}")
|
lines.append(f" {anon_key} is also known as: {', '.join(aliases)}")
|
||||||
# Also include aliases for members NOT in the conversation (so the LLM
|
# Also include aliases for members NOT in the conversation (so the LLM
|
||||||
# can recognize name-drops of absent members)
|
# can recognize name-drops of absent members)
|
||||||
for uid, aliases in alias_config.items():
|
for uid, aliases in all_aliases.items():
|
||||||
uid = int(uid) if isinstance(uid, str) else uid
|
|
||||||
if uid not in seen_ids:
|
if uid not in seen_ids:
|
||||||
lines.append(f" (not in chat) also known as: {', '.join(aliases)}")
|
lines.append(f" (not in chat) also known as: {', '.join(aliases)}")
|
||||||
return "\n".join(lines) if lines else ""
|
return "\n".join(lines) if lines else ""
|
||||||
@@ -498,8 +497,7 @@ class SentimentCog(commands.Cog):
|
|||||||
anon_conversation = self._anonymize_conversation(conversation, anon_map)
|
anon_conversation = self._anonymize_conversation(conversation, anon_map)
|
||||||
anon_notes = self._anonymize_notes(user_notes_map, anon_map) if user_notes_map else user_notes_map
|
anon_notes = self._anonymize_notes(user_notes_map, anon_map) if user_notes_map else user_notes_map
|
||||||
|
|
||||||
alias_config = config.get("user_aliases", {})
|
alias_context = self._build_alias_context(all_messages, anon_map)
|
||||||
alias_context = self._build_alias_context(all_messages, anon_map, alias_config)
|
|
||||||
|
|
||||||
channel_context = build_channel_context(ref_message, game_channels)
|
channel_context = build_channel_context(ref_message, game_channels)
|
||||||
|
|
||||||
@@ -675,8 +673,7 @@ class SentimentCog(commands.Cog):
|
|||||||
anon_conversation = self._anonymize_conversation(conversation, anon_map)
|
anon_conversation = self._anonymize_conversation(conversation, anon_map)
|
||||||
anon_notes = self._anonymize_notes(user_notes_map, anon_map) if user_notes_map else user_notes_map
|
anon_notes = self._anonymize_notes(user_notes_map, anon_map) if user_notes_map else user_notes_map
|
||||||
|
|
||||||
alias_config = config.get("user_aliases", {})
|
alias_context = self._build_alias_context(raw_messages, anon_map)
|
||||||
alias_context = self._build_alias_context(raw_messages, anon_map, alias_config)
|
|
||||||
|
|
||||||
channel_context = build_channel_context(raw_messages[0], game_channels)
|
channel_context = build_channel_context(raw_messages[0], game_channels)
|
||||||
mention_context = (
|
mention_context = (
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ import logging
|
|||||||
logger = logging.getLogger("bcs.sentiment")
|
logger = logging.getLogger("bcs.sentiment")
|
||||||
|
|
||||||
|
|
||||||
|
def _aliases_csv(user_data) -> str | None:
|
||||||
|
"""Convert aliases list to comma-separated string for DB storage."""
|
||||||
|
return ",".join(user_data.aliases) if user_data.aliases else None
|
||||||
|
|
||||||
|
|
||||||
def save_user_state(bot, dirty_users: set[int], user_id: int) -> None:
|
def save_user_state(bot, dirty_users: set[int], user_id: int) -> None:
|
||||||
"""Fire-and-forget save of a user's current state to DB."""
|
"""Fire-and-forget save of a user's current state to DB."""
|
||||||
user_data = bot.drama_tracker.get_user(user_id)
|
user_data = bot.drama_tracker.get_user(user_id)
|
||||||
@@ -16,6 +21,7 @@ def save_user_state(bot, dirty_users: set[int], user_id: int) -> None:
|
|||||||
user_notes=user_data.notes or None,
|
user_notes=user_data.notes or None,
|
||||||
warned=user_data.warned_since_reset,
|
warned=user_data.warned_since_reset,
|
||||||
last_offense_at=user_data.last_offense_time or None,
|
last_offense_at=user_data.last_offense_time or None,
|
||||||
|
aliases=_aliases_csv(user_data),
|
||||||
))
|
))
|
||||||
dirty_users.discard(user_id)
|
dirty_users.discard(user_id)
|
||||||
|
|
||||||
@@ -37,5 +43,6 @@ async def flush_dirty_states(bot, dirty_users: set[int]) -> None:
|
|||||||
user_notes=user_data.notes or None,
|
user_notes=user_data.notes or None,
|
||||||
warned=user_data.warned_since_reset,
|
warned=user_data.warned_since_reset,
|
||||||
last_offense_at=user_data.last_offense_time or None,
|
last_offense_at=user_data.last_offense_time or None,
|
||||||
|
aliases=_aliases_csv(user_data),
|
||||||
)
|
)
|
||||||
logger.info("Flushed %d dirty user states to DB.", len(dirty))
|
logger.info("Flushed %d dirty user states to DB.", len(dirty))
|
||||||
|
|||||||
@@ -21,13 +21,6 @@ sentiment:
|
|||||||
escalation_threshold: 0.25 # Triage toxicity score that triggers re-analysis with heavy model
|
escalation_threshold: 0.25 # Triage toxicity score that triggers re-analysis with heavy model
|
||||||
escalation_boost: 0.04 # Per-message drama boost after warning (sustained toxicity ramps toward mute)
|
escalation_boost: 0.04 # Per-message drama boost after warning (sustained toxicity ramps toward mute)
|
||||||
|
|
||||||
# Nicknames/aliases for server members. Used by the LLM to recognize
|
|
||||||
# when someone references another member by name in chat.
|
|
||||||
user_aliases:
|
|
||||||
684222822272598058: ["Mark", "Limit"] # thelimitations
|
|
||||||
1113144994790903908: ["Glam", "G"] # Glamgirlxx
|
|
||||||
1195191381929508964: ["Bree"] # QueenBree10
|
|
||||||
|
|
||||||
game_channels:
|
game_channels:
|
||||||
gta-online: "GTA Online"
|
gta-online: "GTA Online"
|
||||||
battlefield: "Battlefield"
|
battlefield: "Battlefield"
|
||||||
|
|||||||
@@ -138,6 +138,12 @@ class Database:
|
|||||||
ALTER TABLE UserState ADD LastOffenseAt FLOAT NULL
|
ALTER TABLE UserState ADD LastOffenseAt FLOAT NULL
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
# --- Schema migration for user aliases/nicknames ---
|
||||||
|
cursor.execute("""
|
||||||
|
IF COL_LENGTH('UserState', 'Aliases') IS NULL
|
||||||
|
ALTER TABLE UserState ADD Aliases NVARCHAR(500) NULL
|
||||||
|
""")
|
||||||
|
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'BotSettings')
|
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'BotSettings')
|
||||||
CREATE TABLE BotSettings (
|
CREATE TABLE BotSettings (
|
||||||
@@ -314,19 +320,20 @@ class Database:
|
|||||||
user_notes: str | None = None,
|
user_notes: str | None = None,
|
||||||
warned: bool = False,
|
warned: bool = False,
|
||||||
last_offense_at: float | None = None,
|
last_offense_at: float | None = None,
|
||||||
|
aliases: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Upsert user state (offense count, immunity, off-topic count, coherence baseline, notes, warned, last offense time)."""
|
"""Upsert user state (offense count, immunity, off-topic count, coherence baseline, notes, warned, last offense time, aliases)."""
|
||||||
if not self._available:
|
if not self._available:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
await asyncio.to_thread(
|
await asyncio.to_thread(
|
||||||
self._save_user_state_sync,
|
self._save_user_state_sync,
|
||||||
user_id, offense_count, immune, off_topic_count, baseline_coherence, user_notes, warned, last_offense_at,
|
user_id, offense_count, immune, off_topic_count, baseline_coherence, user_notes, warned, last_offense_at, aliases,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Failed to save user state")
|
logger.exception("Failed to save user state")
|
||||||
|
|
||||||
def _save_user_state_sync(self, user_id, offense_count, immune, off_topic_count, baseline_coherence, user_notes, warned, last_offense_at):
|
def _save_user_state_sync(self, user_id, offense_count, immune, off_topic_count, baseline_coherence, user_notes, warned, last_offense_at, aliases):
|
||||||
conn = self._connect()
|
conn = self._connect()
|
||||||
try:
|
try:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
@@ -337,14 +344,14 @@ class Database:
|
|||||||
WHEN MATCHED THEN
|
WHEN MATCHED THEN
|
||||||
UPDATE SET OffenseCount = ?, Immune = ?, OffTopicCount = ?,
|
UPDATE SET OffenseCount = ?, Immune = ?, OffTopicCount = ?,
|
||||||
BaselineCoherence = ?, UserNotes = ?, Warned = ?,
|
BaselineCoherence = ?, UserNotes = ?, Warned = ?,
|
||||||
LastOffenseAt = ?,
|
LastOffenseAt = ?, Aliases = ?,
|
||||||
UpdatedAt = SYSUTCDATETIME()
|
UpdatedAt = SYSUTCDATETIME()
|
||||||
WHEN NOT MATCHED THEN
|
WHEN NOT MATCHED THEN
|
||||||
INSERT (UserId, OffenseCount, Immune, OffTopicCount, BaselineCoherence, UserNotes, Warned, LastOffenseAt)
|
INSERT (UserId, OffenseCount, Immune, OffTopicCount, BaselineCoherence, UserNotes, Warned, LastOffenseAt, Aliases)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?);""",
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);""",
|
||||||
user_id,
|
user_id,
|
||||||
offense_count, 1 if immune else 0, off_topic_count, baseline_coherence, user_notes, 1 if warned else 0, last_offense_at,
|
offense_count, 1 if immune else 0, off_topic_count, baseline_coherence, user_notes, 1 if warned else 0, last_offense_at, aliases,
|
||||||
user_id, offense_count, 1 if immune else 0, off_topic_count, baseline_coherence, user_notes, 1 if warned else 0, last_offense_at,
|
user_id, offense_count, 1 if immune else 0, off_topic_count, baseline_coherence, user_notes, 1 if warned else 0, last_offense_at, aliases,
|
||||||
)
|
)
|
||||||
cursor.close()
|
cursor.close()
|
||||||
finally:
|
finally:
|
||||||
@@ -387,7 +394,7 @@ class Database:
|
|||||||
try:
|
try:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"SELECT UserId, OffenseCount, Immune, OffTopicCount, BaselineCoherence, UserNotes, Warned, LastOffenseAt FROM UserState"
|
"SELECT UserId, OffenseCount, Immune, OffTopicCount, BaselineCoherence, UserNotes, Warned, LastOffenseAt, Aliases FROM UserState"
|
||||||
)
|
)
|
||||||
rows = cursor.fetchall()
|
rows = cursor.fetchall()
|
||||||
cursor.close()
|
cursor.close()
|
||||||
@@ -401,6 +408,7 @@ class Database:
|
|||||||
"user_notes": row[5] or "",
|
"user_notes": row[5] or "",
|
||||||
"warned": bool(row[6]),
|
"warned": bool(row[6]),
|
||||||
"last_offense_at": float(row[7]) if row[7] is not None else 0.0,
|
"last_offense_at": float(row[7]) if row[7] is not None else 0.0,
|
||||||
|
"aliases": row[8] or "",
|
||||||
}
|
}
|
||||||
for row in rows
|
for row in rows
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ class UserDrama:
|
|||||||
last_coherence_alert_time: float = 0.0
|
last_coherence_alert_time: float = 0.0
|
||||||
# Per-user LLM notes
|
# Per-user LLM notes
|
||||||
notes: str = ""
|
notes: str = ""
|
||||||
|
# Known aliases/nicknames
|
||||||
|
aliases: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
class DramaTracker:
|
class DramaTracker:
|
||||||
@@ -217,6 +219,16 @@ class DramaTracker:
|
|||||||
def clear_user_notes(self, user_id: int) -> None:
|
def clear_user_notes(self, user_id: int) -> None:
|
||||||
self.get_user(user_id).notes = ""
|
self.get_user(user_id).notes = ""
|
||||||
|
|
||||||
|
def get_user_aliases(self, user_id: int) -> list[str]:
|
||||||
|
return self.get_user(user_id).aliases
|
||||||
|
|
||||||
|
def set_user_aliases(self, user_id: int, aliases: list[str]) -> None:
|
||||||
|
self.get_user(user_id).aliases = aliases
|
||||||
|
|
||||||
|
def get_all_aliases(self) -> dict[int, list[str]]:
|
||||||
|
"""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}
|
||||||
|
|
||||||
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
|
||||||
@@ -298,6 +310,8 @@ class DramaTracker:
|
|||||||
user.offense_count = 0
|
user.offense_count = 0
|
||||||
user.warned_since_reset = False
|
user.warned_since_reset = False
|
||||||
user.last_offense_time = 0.0
|
user.last_offense_time = 0.0
|
||||||
|
if state.get("aliases"):
|
||||||
|
user.aliases = [a.strip() for a in state["aliases"].split(",") if a.strip()]
|
||||||
count += 1
|
count += 1
|
||||||
return count
|
return count
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user