Initial commit - Excommunicado Discord bot (XCOM stealth isolation)

This commit is contained in:
Subinacls
2026-06-08 20:47:32 -04:00
commit fc9845772d
9 changed files with 925 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""Cogs for the Discord bot."""
+372
View File
@@ -0,0 +1,372 @@
"""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,
)
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
class ModerationCog(commands.Cog):
"""XCOM commands and logic."""
def __init__(self, bot: commands.Bot):
self.bot = bot
@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.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
guild = interaction.guild
role = await get_or_create_xcom_role(guild)
channel = await get_or_create_xcom_channel(guild, role)
if role in user.roles:
await interaction.response.send_message(f"{user.mention} is already under XCOM.", ephemeral=True)
return
try:
await user.add_roles(role, reason=f"XCOM by {interaction.user}: {reason}")
except discord.Forbidden:
await interaction.response.send_message("Missing permissions to assign role. Check bot role hierarchy.", ephemeral=True)
return
# Apply permanent XCOM Mark (nickname prefix) - survives release
try:
current_nick = user.nick or user.name
if not current_nick.startswith("[XCOM]"):
new_nick = f"[XCOM] {current_nick}"[:32] # Discord nick limit
await user.edit(nick=new_nick, reason="XCOM permanent mark applied")
except discord.Forbidden:
pass # Bot lacks permission to change nickname
timestamp = datetime.now(timezone.utc).isoformat()
add_isolated_user(guild.id, user.id, reason, timestamp)
# Welcome message in the isolation channel
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
# DM the user
try:
await user.send(embed=welcome)
except discord.Forbidden:
pass # DMs disabled
# 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.response.send_message(
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."""
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 currently under XCOM.", ephemeral=True)
return
await user.remove_roles(role, reason=f"Released from XCOM by {interaction.user}")
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:
pass
await interaction.response.send_message(
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.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
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.response.send_message(
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.response.send_message(
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)
@xcom.error
@xcom_release.error
@xcom_unmark.error
@xcom_vow.error
@xcom_witness.error
@xcom_absolve.error
@xcom_setup.error
@xcom_list.error
async def permission_error(self, interaction: discord.Interaction, error: app_commands.AppCommandError):
if isinstance(error, app_commands.MissingPermissions):
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.response.send_message(f"Error: {error}", ephemeral=True)
async def setup(bot: commands.Bot):
"""Load the cog."""
await bot.add_cog(ModerationCog(bot))
print("[COG] Moderation (XCOM) loaded.")