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:
2026-02-27 10:35:19 -05:00
parent ad1234ec99
commit 33d56f8737
6 changed files with 124 additions and 28 deletions

View File

@@ -250,6 +250,7 @@ class CommandsCog(commands.Cog):
off_topic_count=user_data.off_topic_count,
baseline_coherence=user_data.baseline_coherence,
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"
await interaction.response.send_message(
@@ -501,6 +502,7 @@ class CommandsCog(commands.Cog):
off_topic_count=user_data.off_topic_count,
baseline_coherence=user_data.baseline_coherence,
user_notes=user_data.notes or None,
aliases=",".join(user_data.aliases) if user_data.aliases else None,
))
await interaction.response.send_message(
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,
baseline_coherence=user_data.baseline_coherence,
user_notes=None,
aliases=",".join(user_data.aliases) if user_data.aliases else None,
))
await interaction.response.send_message(
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(
name="bcs-mode",
description="Switch the bot's personality mode.",

View File

@@ -253,18 +253,18 @@ class SentimentCog(commands.Cog):
"""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()}
@staticmethod
def _build_alias_context(
self,
messages: list[discord.Message],
anon_map: dict[str, str],
alias_config: dict,
) -> str:
"""Build anonymized alias context string for the LLM.
Maps user IDs from messages to their known nicknames using the
config, then replaces display names with anonymous keys.
Maps user IDs from messages to their known nicknames from
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 ""
lines = []
seen_ids: set[int] = set()
@@ -273,14 +273,13 @@ class SentimentCog(commands.Cog):
if uid in seen_ids:
continue
seen_ids.add(uid)
aliases = alias_config.get(uid)
aliases = all_aliases.get(uid)
if aliases:
anon_key = anon_map.get(msg.author.display_name, msg.author.display_name)
lines.append(f" {anon_key} is also known as: {', '.join(aliases)}")
# Also include aliases for members NOT in the conversation (so the LLM
# can recognize name-drops of absent members)
for uid, aliases in alias_config.items():
uid = int(uid) if isinstance(uid, str) else uid
for uid, aliases in all_aliases.items():
if uid not in seen_ids:
lines.append(f" (not in chat) also known as: {', '.join(aliases)}")
return "\n".join(lines) if lines else ""
@@ -498,8 +497,7 @@ class SentimentCog(commands.Cog):
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
alias_config = config.get("user_aliases", {})
alias_context = self._build_alias_context(all_messages, anon_map, alias_config)
alias_context = self._build_alias_context(all_messages, anon_map)
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_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_config)
alias_context = self._build_alias_context(raw_messages, anon_map)
channel_context = build_channel_context(raw_messages[0], game_channels)
mention_context = (

View File

@@ -4,6 +4,11 @@ import logging
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:
"""Fire-and-forget save of a user's current state to DB."""
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,
warned=user_data.warned_since_reset,
last_offense_at=user_data.last_offense_time or None,
aliases=_aliases_csv(user_data),
))
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,
warned=user_data.warned_since_reset,
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))