"""Moderation cog implementing the Excommunicado XCOM isolation system for flood prevention. Commands: - /xcom-setup : one-time server setup (creates role + xcom-lounge shared room) - /xcom @user [reason] : excommunicate offending user into the shared room - /xcom-release @user : restore user to normal access - /xcom-list : show currently XCOM users """ from datetime import datetime, timezone from typing import Optional import discord from discord.ext import commands from discord import app_commands from utils.config import ( get_guild_data, update_guild_data, add_isolated_user, remove_isolated_user, get_isolated_users, cast_vote, get_vote_score, get_keyword_points, clear_votes, add_keyword, remove_keyword, get_keywords, ) # --- Community vote configuration --- VOTE_XCOM_THRESHOLD = 10 # net score required to auto-XCOM a user VOTE_ADMIN_WEIGHT = 2 # admins / moderators count double VOTE_USER_WEIGHT = 1 # regular members VOTE_COOLDOWN_HOURS = 24 # one vote per voter per target per 24h VOTE_RESET_DAYS = 7 # tallies reset weekly VOTE_MIN_UNIQUE_VOTERS = 5 # quorum: distinct voters required before XCOM fires async def get_or_create_xcom_role(guild: discord.Guild) -> discord.Role: """Get existing Excommunicado role or create a new one.""" data = get_guild_data(guild.id) role_id = data.get("xcom_role_id") if role_id: role = guild.get_role(role_id) if role: return role # Create role with dark red color (Excommunicado theme) role = await guild.create_role( name="Excommunicado", color=discord.Color.dark_red(), hoist=False, mentionable=False, reason="Excommunicado role for violation containment" ) update_guild_data(guild.id, "xcom_role_id", role.id) return role async def get_or_create_xcom_channel(guild: discord.Guild, role: discord.Role) -> discord.TextChannel: """Get or create the shared 'xcom-lounge' text channel.""" data = get_guild_data(guild.id) ch_id = data.get("xcom_channel_id") if ch_id: channel = guild.get_channel(ch_id) if channel and isinstance(channel, discord.TextChannel): return channel overwrites = { guild.default_role: discord.PermissionOverwrite( view_channel=False, send_messages=False, read_message_history=False ), role: discord.PermissionOverwrite( view_channel=True, send_messages=True, read_message_history=True, embed_links=True ), } channel = await guild.create_text_channel( name="xcom-lounge", overwrites=overwrites, topic="Moderated discussion lounge. Staff are here to help resolve issues.", reason="Creating shared moderated room for Excommunicado", slowmode_delay=5, # light slowmode to further reduce flood ) update_guild_data(guild.id, "xcom_channel_id", channel.id) return channel def _keyword_list_embed(guild: discord.Guild) -> discord.Embed: """Build an embed listing the guild's auto-score keywords.""" keywords = get_keywords(guild.id) embed = discord.Embed( title="🚩 Auto-Score Keywords", description=( "Messages matching these phrases add points to the sender's community score.\n" "Reaching the XCOM threshold auto-isolates them — even when no admin is online.\n" "_Staff and the server owner are immune._" ), color=discord.Color.orange(), ) if not keywords: embed.add_field(name="No keywords yet", value="Use **Add / Edit** to create one.", inline=False) else: lines = [] for _, info in sorted(keywords.items(), key=lambda kv: -int(kv[1].get("weight", 0))): lines.append(f"• `{info.get('display', '?')}` — **+{int(info.get('weight', 1))}**") # Embed field values cap at 1024 chars; chunk if needed chunk = "" idx = 1 for line in lines: if len(chunk) + len(line) + 1 > 1024: embed.add_field(name=f"Phrases ({idx})", value=chunk, inline=False) chunk = "" idx += 1 chunk += line + "\n" if chunk: embed.add_field(name=f"Phrases ({idx})" if idx > 1 else "Phrases", value=chunk, inline=False) embed.set_footer(text="Curate carefully — this is a cybersecurity channel; some terms are legitimate in context.") return embed class KeywordAddModal(discord.ui.Modal, title="Add / Edit Auto-Score Keyword"): phrase = discord.ui.TextInput( label="Keyword or phrase", placeholder="e.g. cracked software, free nitro generator, warez", max_length=200, required=True, ) weight = discord.ui.TextInput( label="Score weight (integer, e.g. 1-10)", placeholder="Higher = closer to instant XCOM. Threshold is 10.", max_length=4, required=True, default="1", ) def __init__(self, parent_view: "KeywordManagerView"): super().__init__() self.parent_view = parent_view async def on_submit(self, interaction: discord.Interaction): try: weight_val = int(str(self.weight.value).strip()) except ValueError: await interaction.response.send_message("Weight must be a whole number.", ephemeral=True) return if weight_val < 1 or weight_val > 100: await interaction.response.send_message("Weight must be between 1 and 100.", ephemeral=True) return entry = add_keyword(interaction.guild.id, str(self.phrase.value), weight_val, interaction.user.id) if entry is None: await interaction.response.send_message("That phrase was empty after normalization.", ephemeral=True) return await interaction.response.edit_message( embed=_keyword_list_embed(interaction.guild), view=self.parent_view ) class KeywordRemoveModal(discord.ui.Modal, title="Remove Auto-Score Keyword"): phrase = discord.ui.TextInput( label="Keyword or phrase to remove", placeholder="Type the phrase exactly as listed", max_length=200, required=True, ) def __init__(self, parent_view: "KeywordManagerView"): super().__init__() self.parent_view = parent_view async def on_submit(self, interaction: discord.Interaction): removed = remove_keyword(interaction.guild.id, str(self.phrase.value)) if not removed: await interaction.response.send_message("No matching keyword was found.", ephemeral=True) return await interaction.response.edit_message( embed=_keyword_list_embed(interaction.guild), view=self.parent_view ) class KeywordManagerView(discord.ui.View): """Interactive admin panel for curating auto-score keywords.""" def __init__(self, author_id: int): super().__init__(timeout=180) self.author_id = author_id async def interaction_check(self, interaction: discord.Interaction) -> bool: # Only the invoking admin (with Manage Server) may operate the panel. if interaction.user.id != self.author_id or not interaction.user.guild_permissions.manage_guild: await interaction.response.send_message( "Only the admin who opened this panel can use it.", ephemeral=True ) return False return True @discord.ui.button(label="Add / Edit", style=discord.ButtonStyle.success, emoji="➕") async def add_edit(self, interaction: discord.Interaction, button: discord.ui.Button): await interaction.response.send_modal(KeywordAddModal(self)) @discord.ui.button(label="Remove", style=discord.ButtonStyle.danger, emoji="🗑️") async def remove(self, interaction: discord.Interaction, button: discord.ui.Button): await interaction.response.send_modal(KeywordRemoveModal(self)) @discord.ui.button(label="Refresh", style=discord.ButtonStyle.secondary, emoji="🔄") async def refresh(self, interaction: discord.Interaction, button: discord.ui.Button): await interaction.response.edit_message(embed=_keyword_list_embed(interaction.guild), view=self) class ModerationCog(commands.Cog): """XCOM commands and logic.""" def __init__(self, bot: commands.Bot): self.bot = bot async def _perform_xcom(self, guild: discord.Guild, user: discord.Member, reason: str, applied_by: str): """Shared XCOM logic used by /xcom and the community vote trigger. Returns (channel, already_isolated). Raises discord.Forbidden if the Excommunicado role cannot be assigned (caller should handle it). """ role = await get_or_create_xcom_role(guild) channel = await get_or_create_xcom_channel(guild, role) if role in user.roles: return channel, True await user.add_roles(role, reason=f"XCOM ({applied_by}): {reason}") # Apply permanent XCOM Mark (nickname prefix) - best effort try: current_nick = user.nick or user.name if not current_nick.startswith("[XCOM]"): new_nick = f"[XCOM] {current_nick}"[:32] await user.edit(nick=new_nick, reason="XCOM permanent mark applied") except discord.Forbidden: pass timestamp = datetime.now(timezone.utc).isoformat() add_isolated_user(guild.id, user.id, reason, timestamp) welcome = discord.Embed( title="🪐 XCOM Lounge", description=( f"**Reason for moderation:** {reason}\n\n" "You are now in the shared XCOM discussion area with staff.\n" "Your messages in other channels are being handled privately here.\n" "This helps keep the main server clean while we resolve the issue.\n\n" "Staff are present in this room to assist you." ), color=discord.Color.dark_red() ) welcome.set_footer(text=f"Isolated at {timestamp[:19]}") try: await channel.send(f"{user.mention}", embed=welcome) except Exception: pass try: await user.send(embed=welcome) except discord.Forbidden: pass return channel, False @app_commands.command(name="xcom-setup", description="One-time setup: create XCOM role and xcom-lounge shared room") @app_commands.checks.has_permissions(manage_guild=True) async def xcom_setup(self, interaction: discord.Interaction): """Run this once per server to initialize the isolation system.""" await interaction.response.defer(ephemeral=True) guild = interaction.guild role = await get_or_create_xcom_role(guild) channel = await get_or_create_xcom_channel(guild, role) embed = discord.Embed( title="🪐 XCOM System Ready", description="The bot can now XCOM users to contain violations without banning them.", color=discord.Color.orange() ) embed.add_field(name="Role Created", value=f"{role.mention} (ID: {role.id})", inline=True) embed.add_field(name="Channel Created", value=f"{channel.mention}", inline=True) embed.add_field( name="⚠️ Stealth Mode Active", value="The **Excommunicado** role is now only a marker + grants access to the shared moderated room.\n\n**Do NOT** deny View/Send on public channels (user must keep seeing the normal server).\nNormal members cannot see xcom-lounge (already configured).\n\nStaff members you want to 'share the room' with the isolated user can be given the Excommunicado role manually.", inline=False ) embed.set_footer(text="After setup, use /xcom @user to excommunicate offenders. They will not realize they have been moved.") await interaction.followup.send(embed=embed, ephemeral=True) @app_commands.command(name="xcom", description="Excommunicate a user into the shared moderated room (anti-violation)") @app_commands.checks.has_permissions(manage_roles=True) @app_commands.checks.cooldown(5, 15.0, key=lambda i: (i.guild_id, i.user.id)) @app_commands.describe(user="The user who violated rules", reason="Reason for XCOM") async def xcom(self, interaction: discord.Interaction, user: discord.Member, reason: Optional[str] = "Flooding the channel"): """Excommunicate the user by assigning the Excommunicado role so they are contained in the xcom-lounge.""" if user.bot: await interaction.response.send_message("Cannot XCOM bots.", ephemeral=True) return if user.id == interaction.user.id: await interaction.response.send_message("You cannot XCOM yourself.", ephemeral=True) return if user.id == interaction.guild.owner_id: await interaction.response.send_message("You cannot XCOM the server owner.", ephemeral=True) return # Role-hierarchy safety: the invoker must outrank the target, # and the bot must be able to manage the target. invoker = interaction.user if isinstance(invoker, discord.Member) and invoker.id != interaction.guild.owner_id: if user.top_role >= invoker.top_role: await interaction.response.send_message( "You cannot XCOM a member with an equal or higher role than yours.", ephemeral=True, ) return me = interaction.guild.me if me is not None and user.top_role >= me.top_role: await interaction.response.send_message( "I cannot XCOM this member — their highest role is above mine. Move my role higher.", ephemeral=True, ) return # Defer the response because this command performs multiple async operations await interaction.response.defer(ephemeral=True) guild = interaction.guild try: channel, already = await self._perform_xcom(guild, user, reason, f"by {interaction.user}") except discord.Forbidden: await interaction.followup.send("Missing permissions to assign role. Check bot role hierarchy.", ephemeral=True) return if already: await interaction.followup.send(f"{user.mention} is already under XCOM.", ephemeral=True) return # Log action log_embed = discord.Embed( title="🪐 User XCOM'd", description=f"{user.mention} ({user.id}) was placed in {channel.mention}", color=discord.Color.dark_red(), timestamp=datetime.now(timezone.utc) ) log_embed.add_field(name="Reason", value=reason, inline=False) log_embed.add_field(name="Admin", value=interaction.user.mention, inline=True) if hasattr(self.bot, "log_action"): await self.bot.log_action(guild, log_embed) await interaction.followup.send( f"✅ {user.mention} has been XCOM'd into the shared room {channel.mention}.\nThey continue to see the server normally.", ephemeral=True ) @app_commands.command(name="xcom-release", description="Release a user from XCOM back to normal server access (XCOM Mark remains until admin removes it)") @app_commands.checks.has_permissions(manage_roles=True) @app_commands.describe(user="User to release from XCOM") async def xcom_release(self, interaction: discord.Interaction, user: discord.Member): """Remove the Excommunicado role. The [XCOM] nickname mark stays until an admin runs /xcom-unmark.""" await interaction.response.defer(ephemeral=True) guild = interaction.guild data = get_guild_data(guild.id) role_id = data.get("xcom_role_id") role = guild.get_role(role_id) if role_id else None if not role or role not in user.roles: await interaction.followup.send(f"{user.mention} is not currently under XCOM.", ephemeral=True) return try: await user.remove_roles(role, reason=f"Released from XCOM by {interaction.user}") except discord.Forbidden: await interaction.followup.send("Missing permissions to remove role. Check bot role hierarchy.", ephemeral=True) return remove_isolated_user(guild.id, user.id) release_embed = discord.Embed( title="✅ You have been released from XCOM", description="You now have full access to the server again. Please follow the rules.", color=discord.Color.green() ) try: await user.send(embed=release_embed) except discord.HTTPException: pass await interaction.followup.send( f"✅ {user.mention} has been released from XCOM and restored to normal access. The [XCOM] mark remains until an admin removes it.", ephemeral=True ) @app_commands.command(name="xcom-unmark", description="Remove the permanent [XCOM] nickname mark from a user (admin only)") @app_commands.checks.has_permissions(manage_roles=True) @app_commands.describe(user="User whose XCOM mark should be removed") async def xcom_unmark(self, interaction: discord.Interaction, user: discord.Member): """Remove the [XCOM] nickname prefix. Only admins can do this.""" try: current_nick = user.nick or user.name if current_nick.startswith("[XCOM]"): new_nick = current_nick[7:].strip() or user.name # remove "[XCOM] " await user.edit(nick=new_nick, reason=f"XCOM mark removed by {interaction.user}") await interaction.response.send_message(f"✅ Removed XCOM mark from {user.mention}.", ephemeral=True) else: await interaction.response.send_message(f"{user.mention} does not have an XCOM mark.", ephemeral=True) except discord.Forbidden: await interaction.response.send_message("Bot lacks permission to change this user's nickname.", ephemeral=True) @app_commands.command(name="xcom-vow", description="Submit a Vow of Redemption while under XCOM (visible to staff in the lounge)") @app_commands.describe(vow="Your promise or statement (max 200 characters)") async def xcom_vow(self, interaction: discord.Interaction, vow: str): """Allow an XCOM user to submit a vow in the lounge.""" guild = interaction.guild data = get_guild_data(guild.id) xcom_role_id = data.get("xcom_role_id") xcom_channel_id = data.get("xcom_channel_id") if not xcom_role_id or xcom_role_id not in [r.id for r in interaction.user.roles]: await interaction.response.send_message("You must be under XCOM to submit a vow.", ephemeral=True) return if len(vow) > 200: await interaction.response.send_message("Vow is too long (max 200 characters).", ephemeral=True) return channel = guild.get_channel(xcom_channel_id) if xcom_channel_id else None if channel: vow_embed = discord.Embed( title="📜 XCOM Vow Submitted", description=f"**{interaction.user.mention}** has made the following vow:\n\n> {vow}", color=discord.Color.dark_red() ) await channel.send(embed=vow_embed) await interaction.response.send_message("Your vow has been submitted to staff.", ephemeral=True) else: await interaction.response.send_message("XCOM lounge not found.", ephemeral=True) @app_commands.command(name="xcom-witness", description="Add a staff member to the xcom-lounge so they can support a specific user") @app_commands.checks.has_permissions(manage_roles=True) @app_commands.describe(staff="Staff member to add as witness", target="XCOM user they will support") async def xcom_witness(self, interaction: discord.Interaction, staff: discord.Member, target: discord.Member): """Give a staff member the Excommunicado role so they can join the shared room.""" guild = interaction.guild data = get_guild_data(guild.id) role_id = data.get("xcom_role_id") role = guild.get_role(role_id) if role_id else None if not role: await interaction.response.send_message("XCOM system not set up. Run /xcom-setup first.", ephemeral=True) return if role in staff.roles: await interaction.response.send_message(f"{staff.mention} is already a witness.", ephemeral=True) return try: await staff.add_roles(role, reason=f"Added as XCOM witness for {target} by {interaction.user}") await interaction.response.send_message(f"✅ {staff.mention} can now join the xcom-lounge as a witness.", ephemeral=True) except discord.Forbidden: await interaction.response.send_message("Missing permissions to assign the role.", ephemeral=True) @app_commands.command(name="xcom-absolve", description="Check absolution points and release a user early (admin only)") @app_commands.checks.has_permissions(manage_roles=True) @app_commands.checks.cooldown(5, 15.0, key=lambda i: (i.guild_id, i.user.id)) @app_commands.describe(user="XCOM user to evaluate for absolution") async def xcom_absolve(self, interaction: discord.Interaction, user: discord.Member): """If the user has enough absolution points, remove role and optionally the mark.""" guild = interaction.guild data = get_guild_data(guild.id) role_id = data.get("xcom_role_id") role = guild.get_role(role_id) if role_id else None if not role or role not in user.roles: await interaction.response.send_message(f"{user.mention} is not under XCOM.", ephemeral=True) return # Defer because this command performs async operations await interaction.response.defer(ephemeral=True) isolated = get_isolated_users(guild.id) user_data = isolated.get(str(user.id), {}) points = user_data.get("absolution_points", 0) if points < 10: # Threshold for absolution await interaction.followup.send( f"{user.mention} has {points} absolution points (need 10). Not ready for absolution yet.", ephemeral=True ) return # Absolve: remove role (mark stays unless admin uses /xcom-unmark) await user.remove_roles(role, reason=f"Absolved by {interaction.user} ({points} points)") # Optional: remove from isolated list but keep the mark flag remove_isolated_user(guild.id, user.id) await interaction.followup.send( f"✅ {user.mention} has been absolved ({points} points). The [XCOM] mark remains until manually removed.", ephemeral=True ) @app_commands.command(name="xcom-list", description="List all users currently under XCOM") @app_commands.checks.has_permissions(manage_roles=True) async def xcom_list(self, interaction: discord.Interaction): """Show currently isolated users with reasons.""" guild = interaction.guild isolated = get_isolated_users(guild.id) if not isolated: await interaction.response.send_message("No users are currently under XCOM.", ephemeral=True) return embed = discord.Embed( title="🪐 Currently XCOM Users", color=discord.Color.orange() ) for uid, info in list(isolated.items())[:15]: # limit embed size member = guild.get_member(int(uid)) name = member.mention if member else f"Unknown ({uid})" reason = info.get("reason", "Unknown") ts = info.get("timestamp", "")[:10] embed.add_field(name=name, value=f"Reason: {reason}\nSince: {ts}", inline=False) embed.set_footer(text=f"Total: {len(isolated)} | Use /xcom-release to restore access") await interaction.response.send_message(embed=embed, ephemeral=True) @app_commands.command(name="xcom-vote", description="Cast a community vote to excommunicate (up) or defend (down) a user") @app_commands.checks.cooldown(10, 60.0, key=lambda i: (i.guild_id, i.user.id)) @app_commands.describe(user="The user to vote on", direction="up = excommunicate, down = defend") @app_commands.choices(direction=[ app_commands.Choice(name="up (excommunicate)", value="up"), app_commands.Choice(name="down (defend)", value="down"), ]) async def xcom_vote(self, interaction: discord.Interaction, user: discord.Member, direction: app_commands.Choice[str]): """Community-driven XCOM. Net score >= threshold (with quorum) auto-isolates the target.""" guild = interaction.guild voter = interaction.user # --- Guardrails --- if user.bot: await interaction.response.send_message("You cannot vote on bots.", ephemeral=True) return if user.id == voter.id: await interaction.response.send_message("You cannot vote on yourself.", ephemeral=True) return if user.id == guild.owner_id: await interaction.response.send_message("You cannot vote against the server owner.", ephemeral=True) return # Staff / moderator immunity if user.guild_permissions.manage_roles or user.guild_permissions.administrator: await interaction.response.send_message("You cannot vote against staff or moderators.", ephemeral=True) return data = get_guild_data(guild.id) role_id = data.get("xcom_role_id") role = guild.get_role(role_id) if role_id else None if role and role in user.roles: await interaction.response.send_message(f"{user.mention} is already under XCOM.", ephemeral=True) return # The bot must be able to enforce the result me = guild.me if me is not None and user.top_role >= me.top_role: await interaction.response.send_message( "That member ranks above me, so a community XCOM could not be enforced.", ephemeral=True ) return await interaction.response.defer(ephemeral=True) is_staff = voter.guild_permissions.administrator or voter.guild_permissions.manage_roles weight = VOTE_ADMIN_WEIGHT if is_staff else VOTE_USER_WEIGHT dir_val = 1 if direction.value == "up" else -1 result = cast_vote( guild.id, user.id, voter.id, dir_val, weight, cooldown_hours=VOTE_COOLDOWN_HOURS, reset_days=VOTE_RESET_DAYS, ) if not result["ok"]: if result["reason"] == "cooldown": await interaction.followup.send( f"⏳ You've already voted on this user. You can vote again in ~{result['retry_hours']:.1f}h " "(one vote per user per 24h).", ephemeral=True, ) else: await interaction.followup.send("Your vote could not be recorded.", ephemeral=True) return score = result["score"] unique_voters = result["unique_voters"] # --- Threshold reached: trigger the community XCOM --- if score >= VOTE_XCOM_THRESHOLD and unique_voters >= VOTE_MIN_UNIQUE_VOTERS: try: channel, already = await self._perform_xcom( guild, user, "Community vote threshold reached", "community vote" ) except discord.Forbidden: await interaction.followup.send( "Your vote was recorded, but I lack permission to XCOM this user.", ephemeral=True ) return clear_votes(guild.id, user.id) log_embed = discord.Embed( title="🪐 Community XCOM Triggered", description=f"{user.mention} ({user.id}) reached the community vote threshold and was placed in {channel.mention}.", color=discord.Color.dark_red(), timestamp=datetime.now(timezone.utc), ) log_embed.add_field(name="Final score", value=str(score), inline=True) log_embed.add_field(name="Distinct voters", value=str(unique_voters), inline=True) if hasattr(self.bot, "log_action"): await self.bot.log_action(guild, log_embed) await interaction.followup.send( f"✅ The community has spoken — the score reached **{score}** and the user has been excommunicated.", ephemeral=True, ) return # --- Not yet --- arrow = "⬆️" if dir_val > 0 else "⬇️" remaining = VOTE_XCOM_THRESHOLD - score progress = f" — {remaining} more to trigger XCOM" if remaining > 0 else "" quorum_note = ( "" if unique_voters >= VOTE_MIN_UNIQUE_VOTERS else f"\n_Needs ≥{VOTE_MIN_UNIQUE_VOTERS} distinct voters (currently {unique_voters})._" ) await interaction.followup.send( f"{arrow} Vote recorded. Current score: **{score}/{VOTE_XCOM_THRESHOLD}**{progress}." f"{quorum_note}\n_Tallies reset weekly. One vote per user per 24h._", ephemeral=True, ) @app_commands.command(name="xcom-votes", description="View the current community vote tally for a user (staff)") @app_commands.checks.has_permissions(manage_roles=True) @app_commands.describe(user="User to inspect") async def xcom_votes(self, interaction: discord.Interaction, user: discord.Member): """Show the running community vote score and quorum status for a user.""" score, unique_voters = get_vote_score(interaction.guild.id, user.id, reset_days=VOTE_RESET_DAYS) kw_points = get_keyword_points(interaction.guild.id, user.id, reset_days=VOTE_RESET_DAYS) embed = discord.Embed(title="🗳️ Community Vote Standing", color=discord.Color.orange()) embed.add_field(name="Target", value=user.mention, inline=True) embed.add_field(name="Score", value=f"{score}/{VOTE_XCOM_THRESHOLD}", inline=True) embed.add_field(name="Distinct voters", value=f"{unique_voters}/{VOTE_MIN_UNIQUE_VOTERS}", inline=True) embed.add_field(name="From human votes", value=str(score - kw_points), inline=True) embed.add_field(name="From keyword flags", value=str(kw_points), inline=True) embed.set_footer(text=f"Threshold {VOTE_XCOM_THRESHOLD} • quorum {VOTE_MIN_UNIQUE_VOTERS} • resets weekly") await interaction.response.send_message(embed=embed, ephemeral=True) @app_commands.command(name="xcom-keywords", description="Manage auto-score keywords/phrases (admin panel)") @app_commands.checks.has_permissions(manage_guild=True) async def xcom_keywords(self, interaction: discord.Interaction): """Open the interactive panel to add, edit, or remove auto-score keywords.""" view = KeywordManagerView(interaction.user.id) await interaction.response.send_message( embed=_keyword_list_embed(interaction.guild), view=view, ephemeral=True ) @xcom.error @xcom_release.error @xcom_unmark.error @xcom_vow.error @xcom_witness.error @xcom_absolve.error @xcom_setup.error @xcom_list.error @xcom_vote.error @xcom_votes.error @xcom_keywords.error async def permission_error(self, interaction: discord.Interaction, error: app_commands.AppCommandError): try: if isinstance(error, app_commands.MissingPermissions): if not interaction.response.is_done(): await interaction.response.send_message( "You need the 'Manage Roles' (or Manage Server for setup) permission to use this command.", ephemeral=True ) else: await interaction.followup.send( "You need the 'Manage Roles' (or Manage Server for setup) permission to use this command.", ephemeral=True ) elif isinstance(error, app_commands.CommandOnCooldown): msg = f"⏳ Slow down — try again in {error.retry_after:.1f}s." if not interaction.response.is_done(): await interaction.response.send_message(msg, ephemeral=True) else: await interaction.followup.send(msg, ephemeral=True) else: if not interaction.response.is_done(): await interaction.response.send_message(f"Error: {error}", ephemeral=True) else: await interaction.followup.send(f"Error: {error}", ephemeral=True) except discord.errors.InteractionResponded: pass # Interaction was already responded to async def setup(bot: commands.Bot): """Load the cog.""" await bot.add_cog(ModerationCog(bot)) print("[COG] Moderation (XCOM) loaded.")