From 3034a474be39677ef5ed29f36eadbdff61935b1e Mon Sep 17 00:00:00 2001 From: Subinacls Date: Tue, 9 Jun 2026 07:39:43 -0400 Subject: [PATCH] patched vow system --- bot.py | 15 ++++-- cogs/moderation.py | 131 ++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 130 insertions(+), 16 deletions(-) diff --git a/bot.py b/bot.py index d72fad5..a395ae1 100644 --- a/bot.py +++ b/bot.py @@ -30,6 +30,8 @@ from utils.config import ( from cogs.moderation import ( get_or_create_xcom_role, get_or_create_xcom_channel, + get_or_create_xcom_mod_channel, + _post_to_mod_channel, VOTE_XCOM_THRESHOLD, VOTE_RESET_DAYS, ) @@ -157,9 +159,9 @@ class ExcommunicadoBot(commands.Bot): result = add_keyword_points( message.guild.id, author.id, matched_weight, reset_days=VOTE_RESET_DAYS ) - print( - f"[KEYWORD] {author} matched {matched_terms} (+{matched_weight}) " - f"-> score {result['score']} in {message.guild.name}" + logger.info( + "Keyword match: user=%s terms=%s (+%d) -> score=%d guild=%s", + author, matched_terms, matched_weight, result["score"], message.guild.name, ) me = message.guild.me can_manage = me is not None and author.top_role < me.top_role @@ -178,10 +180,15 @@ class ExcommunicadoBot(commands.Bot): timestamp=now, ) log_embed.add_field(name="Matched", value=", ".join(matched_terms[:10]) or "—", inline=False) + await _post_to_mod_channel(message.guild, None, log_embed) await self.log_action(message.guild, log_embed) + logger.warning( + "Keyword auto-XCOM: user=%s guild=%s score=%d", + author, message.guild.name, result["score"], + ) return except Exception as kw_err: - print(f"[KEYWORD] Auto-XCOM failed: {kw_err}") + logger.exception("Keyword auto-XCOM failed: %s", kw_err) times = USER_MESSAGE_TIMES[message.author.id] times.append(now) diff --git a/cogs/moderation.py b/cogs/moderation.py index 658e889..f8e8ecd 100644 --- a/cogs/moderation.py +++ b/cogs/moderation.py @@ -9,6 +9,8 @@ Commands: from datetime import datetime, timezone from typing import Optional +import logging + import discord from discord.ext import commands from discord import app_commands @@ -28,6 +30,8 @@ from utils.config import ( get_keywords, ) +logger = logging.getLogger("excommunicado.moderation") + # --- Community vote configuration --- VOTE_XCOM_THRESHOLD = 10 # net score required to auto-XCOM a user VOTE_ADMIN_WEIGHT = 2 # admins / moderators count double @@ -87,6 +91,79 @@ async def get_or_create_xcom_channel(guild: discord.Guild, role: discord.Role) - return channel +def _staff_roles(guild: discord.Guild): + """Yield roles that should be treated as moderation staff. + + A role counts as staff if it grants administrator or any common + moderation permission. Used to auto-grant access to the private + xcom-mod channel. + """ + for r in guild.roles: + p = r.permissions + if (p.administrator or p.manage_guild or p.manage_roles + or p.ban_members or p.kick_members or p.manage_messages): + yield r + + +async def get_or_create_xcom_mod_channel(guild: discord.Guild, role: Optional[discord.Role] = None) -> discord.TextChannel: + """Get or create the private 'xcom-mod' staff-only review channel. + + This channel is hidden from everyone (including the Excommunicado role) and + is where the bot relays vows, vote/keyword auto-XCOM alerts, and other + moderation notices. Isolated users can never see it; the bot posts on their + behalf, so they never need write access here. + """ + if role is None: + role = await get_or_create_xcom_role(guild) + data = get_guild_data(guild.id) + ch_id = data.get("xcom_mod_channel_id") + if ch_id: + channel = guild.get_channel(ch_id) + if channel and isinstance(channel, discord.TextChannel): + return channel + + overwrites = { + # Hidden from regular members... + guild.default_role: discord.PermissionOverwrite(view_channel=False), + # ...and explicitly hidden from isolated users. + role: discord.PermissionOverwrite(view_channel=False), + # The bot must be able to post relayed vows and alerts here. + guild.me: discord.PermissionOverwrite( + view_channel=True, send_messages=True, embed_links=True, read_message_history=True + ), + } + # Grant staff roles read/write access. + for staff_role in _staff_roles(guild): + overwrites[staff_role] = discord.PermissionOverwrite( + view_channel=True, send_messages=True, read_message_history=True, embed_links=True + ) + + channel = await guild.create_text_channel( + name="xcom-mod", + overwrites=overwrites, + topic="Private staff channel: XCOM vows, auto-XCOM alerts, and moderation review. Not visible to isolated users.", + reason="Creating private staff review channel for Excommunicado", + ) + update_guild_data(guild.id, "xcom_mod_channel_id", channel.id) + return channel + + +async def _post_to_mod_channel(guild: discord.Guild, role: Optional[discord.Role], embed: discord.Embed) -> bool: + """Relay an embed to the private xcom-mod channel, creating it if needed. + + Returns True if the message was delivered. + """ + try: + channel = await get_or_create_xcom_mod_channel(guild, role) + await channel.send(embed=embed) + return True + except discord.Forbidden: + logger.warning("Missing permission to create/post in xcom-mod for guild %s", guild.id) + except discord.HTTPException as err: + logger.warning("Failed to post to xcom-mod for guild %s: %s", guild.id, err) + return False + + def _keyword_list_embed(guild: discord.Guild) -> discord.Embed: """Build an embed listing the guild's auto-score keywords.""" keywords = get_keywords(guild.id) @@ -274,6 +351,10 @@ class ModerationCog(commands.Cog): guild = interaction.guild role = await get_or_create_xcom_role(guild) channel = await get_or_create_xcom_channel(guild, role) + try: + mod_channel = await get_or_create_xcom_mod_channel(guild, role) + except discord.Forbidden: + mod_channel = None embed = discord.Embed( title="🪐 XCOM System Ready", @@ -281,10 +362,23 @@ class ModerationCog(commands.Cog): 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="Lounge Channel", value=f"{channel.mention}", inline=True) + embed.add_field( + name="Staff Review Channel", + value=(mod_channel.mention if mod_channel + else "⚠️ Could not create `xcom-mod` (missing Manage Channels permission)."), + 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.", + 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).\n" + "Normal members cannot see xcom-lounge (already configured).\n\n" + "Staff members you want to 'share the room' with the isolated user can be given the Excommunicado role manually.\n\n" + "🔒 **xcom-mod** is a private staff channel (hidden from members and isolated users). " + "The bot relays vows and auto-XCOM alerts there — isolated users never see it." + ), inline=False ) embed.set_footer(text="After setup, use /xcom @user to excommunicate offenders. They will not realize they have been moved.") @@ -417,7 +511,6 @@ class ModerationCog(commands.Cog): 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) @@ -427,17 +520,30 @@ class ModerationCog(commands.Cog): 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() + role = guild.get_role(xcom_role_id) + vow_embed = discord.Embed( + title="📜 XCOM Vow Submitted", + description=f"**{interaction.user.mention}** ({interaction.user.id}) has made the following vow:\n\n> {vow}", + color=discord.Color.dark_red(), + timestamp=datetime.now(timezone.utc), + ) + vow_embed.set_footer(text="Relayed by the bot to staff — the user cannot see this channel.") + + # The bot relays the vow into the private staff channel, so the isolated + # user never needs (or gets) access to xcom-mod themselves. + delivered = await _post_to_mod_channel(guild, role, vow_embed) if role else False + + if delivered: + logger.info("Vow relayed to xcom-mod: guild=%s user=%s", guild.id, interaction.user.id) + await interaction.response.send_message( + "Your vow has been submitted to staff for review.", ephemeral=True ) - 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) + await interaction.response.send_message( + "Could not deliver your vow — staff have been unable to set up the review channel. " + "Please try again later.", + 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) @@ -618,6 +724,7 @@ class ModerationCog(commands.Cog): ) 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) + await _post_to_mod_channel(guild, role, log_embed) if hasattr(self.bot, "log_action"): await self.bot.log_action(guild, log_embed)