added community voting and fixed bugs
This commit is contained in:
+399
-60
@@ -19,8 +19,23 @@ from utils.config import (
|
||||
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."""
|
||||
@@ -72,12 +87,184 @@ async def get_or_create_xcom_channel(guild: discord.Guild, role: discord.Role) -
|
||||
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):
|
||||
@@ -106,6 +293,7 @@ class ModerationCog(commands.Cog):
|
||||
|
||||
@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."""
|
||||
@@ -115,58 +303,42 @@ class ModerationCog(commands.Cog):
|
||||
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
|
||||
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)
|
||||
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
|
||||
|
||||
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)
|
||||
if already:
|
||||
await interaction.followup.send(f"{user.mention} is already under XCOM.", 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",
|
||||
@@ -179,7 +351,7 @@ class ModerationCog(commands.Cog):
|
||||
if hasattr(self.bot, "log_action"):
|
||||
await self.bot.log_action(guild, log_embed)
|
||||
|
||||
await interaction.response.send_message(
|
||||
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
|
||||
)
|
||||
@@ -189,16 +361,21 @@ class ModerationCog(commands.Cog):
|
||||
@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.response.send_message(f"{user.mention} is not currently under XCOM.", ephemeral=True)
|
||||
await interaction.followup.send(f"{user.mention} is not currently under XCOM.", ephemeral=True)
|
||||
return
|
||||
|
||||
await user.remove_roles(role, reason=f"Released from XCOM by {interaction.user}")
|
||||
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)
|
||||
|
||||
@@ -209,10 +386,10 @@ class ModerationCog(commands.Cog):
|
||||
)
|
||||
try:
|
||||
await user.send(embed=release_embed)
|
||||
except:
|
||||
except discord.HTTPException:
|
||||
pass
|
||||
|
||||
await interaction.response.send_message(
|
||||
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
|
||||
)
|
||||
@@ -288,6 +465,7 @@ class ModerationCog(commands.Cog):
|
||||
|
||||
@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."""
|
||||
@@ -300,12 +478,15 @@ class ModerationCog(commands.Cog):
|
||||
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.response.send_message(
|
||||
await interaction.followup.send(
|
||||
f"{user.mention} has {points} absolution points (need 10). Not ready for absolution yet.",
|
||||
ephemeral=True
|
||||
)
|
||||
@@ -317,7 +498,7 @@ class ModerationCog(commands.Cog):
|
||||
# Optional: remove from isolated list but keep the mark flag
|
||||
remove_isolated_user(guild.id, user.id)
|
||||
|
||||
await interaction.response.send_message(
|
||||
await interaction.followup.send(
|
||||
f"✅ {user.mention} has been absolved ({points} points). The [XCOM] mark remains until manually removed.",
|
||||
ephemeral=True
|
||||
)
|
||||
@@ -348,6 +529,143 @@ class ModerationCog(commands.Cog):
|
||||
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
|
||||
@@ -356,14 +674,35 @@ class ModerationCog(commands.Cog):
|
||||
@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):
|
||||
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)
|
||||
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):
|
||||
|
||||
Reference in New Issue
Block a user