patched vow system
This commit is contained in:
@@ -30,6 +30,8 @@ from utils.config import (
|
|||||||
from cogs.moderation import (
|
from cogs.moderation import (
|
||||||
get_or_create_xcom_role,
|
get_or_create_xcom_role,
|
||||||
get_or_create_xcom_channel,
|
get_or_create_xcom_channel,
|
||||||
|
get_or_create_xcom_mod_channel,
|
||||||
|
_post_to_mod_channel,
|
||||||
VOTE_XCOM_THRESHOLD,
|
VOTE_XCOM_THRESHOLD,
|
||||||
VOTE_RESET_DAYS,
|
VOTE_RESET_DAYS,
|
||||||
)
|
)
|
||||||
@@ -157,9 +159,9 @@ class ExcommunicadoBot(commands.Bot):
|
|||||||
result = add_keyword_points(
|
result = add_keyword_points(
|
||||||
message.guild.id, author.id, matched_weight, reset_days=VOTE_RESET_DAYS
|
message.guild.id, author.id, matched_weight, reset_days=VOTE_RESET_DAYS
|
||||||
)
|
)
|
||||||
print(
|
logger.info(
|
||||||
f"[KEYWORD] {author} matched {matched_terms} (+{matched_weight}) "
|
"Keyword match: user=%s terms=%s (+%d) -> score=%d guild=%s",
|
||||||
f"-> score {result['score']} in {message.guild.name}"
|
author, matched_terms, matched_weight, result["score"], message.guild.name,
|
||||||
)
|
)
|
||||||
me = message.guild.me
|
me = message.guild.me
|
||||||
can_manage = me is not None and author.top_role < me.top_role
|
can_manage = me is not None and author.top_role < me.top_role
|
||||||
@@ -178,10 +180,15 @@ class ExcommunicadoBot(commands.Bot):
|
|||||||
timestamp=now,
|
timestamp=now,
|
||||||
)
|
)
|
||||||
log_embed.add_field(name="Matched", value=", ".join(matched_terms[:10]) or "—", inline=False)
|
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)
|
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
|
return
|
||||||
except Exception as kw_err:
|
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 = USER_MESSAGE_TIMES[message.author.id]
|
||||||
times.append(now)
|
times.append(now)
|
||||||
|
|||||||
+119
-12
@@ -9,6 +9,8 @@ Commands:
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
from discord import app_commands
|
from discord import app_commands
|
||||||
@@ -28,6 +30,8 @@ from utils.config import (
|
|||||||
get_keywords,
|
get_keywords,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger("excommunicado.moderation")
|
||||||
|
|
||||||
# --- Community vote configuration ---
|
# --- Community vote configuration ---
|
||||||
VOTE_XCOM_THRESHOLD = 10 # net score required to auto-XCOM a user
|
VOTE_XCOM_THRESHOLD = 10 # net score required to auto-XCOM a user
|
||||||
VOTE_ADMIN_WEIGHT = 2 # admins / moderators count double
|
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
|
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:
|
def _keyword_list_embed(guild: discord.Guild) -> discord.Embed:
|
||||||
"""Build an embed listing the guild's auto-score keywords."""
|
"""Build an embed listing the guild's auto-score keywords."""
|
||||||
keywords = get_keywords(guild.id)
|
keywords = get_keywords(guild.id)
|
||||||
@@ -274,6 +351,10 @@ class ModerationCog(commands.Cog):
|
|||||||
guild = interaction.guild
|
guild = interaction.guild
|
||||||
role = await get_or_create_xcom_role(guild)
|
role = await get_or_create_xcom_role(guild)
|
||||||
channel = await get_or_create_xcom_channel(guild, role)
|
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(
|
embed = discord.Embed(
|
||||||
title="🪐 XCOM System Ready",
|
title="🪐 XCOM System Ready",
|
||||||
@@ -281,10 +362,23 @@ class ModerationCog(commands.Cog):
|
|||||||
color=discord.Color.orange()
|
color=discord.Color.orange()
|
||||||
)
|
)
|
||||||
embed.add_field(name="Role Created", value=f"{role.mention} (ID: {role.id})", inline=True)
|
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(
|
embed.add_field(
|
||||||
name="⚠️ Stealth Mode Active",
|
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
|
inline=False
|
||||||
)
|
)
|
||||||
embed.set_footer(text="After setup, use /xcom @user to excommunicate offenders. They will not realize they have been moved.")
|
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
|
guild = interaction.guild
|
||||||
data = get_guild_data(guild.id)
|
data = get_guild_data(guild.id)
|
||||||
xcom_role_id = data.get("xcom_role_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]:
|
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)
|
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)
|
await interaction.response.send_message("Vow is too long (max 200 characters).", ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
channel = guild.get_channel(xcom_channel_id) if xcom_channel_id else None
|
role = guild.get_role(xcom_role_id)
|
||||||
if channel:
|
vow_embed = discord.Embed(
|
||||||
vow_embed = discord.Embed(
|
title="📜 XCOM Vow Submitted",
|
||||||
title="📜 XCOM Vow Submitted",
|
description=f"**{interaction.user.mention}** ({interaction.user.id}) has made the following vow:\n\n> {vow}",
|
||||||
description=f"**{interaction.user.mention}** has made the following vow:\n\n> {vow}",
|
color=discord.Color.dark_red(),
|
||||||
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:
|
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.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.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="Final score", value=str(score), inline=True)
|
||||||
log_embed.add_field(name="Distinct voters", value=str(unique_voters), 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"):
|
if hasattr(self.bot, "log_action"):
|
||||||
await self.bot.log_action(guild, log_embed)
|
await self.bot.log_action(guild, log_embed)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user