Applying previous commit.
This commit is contained in:
parent
22d22e4452
commit
d845418443
@ -39,12 +39,18 @@ from .aimod_config import (
|
|||||||
GUILD_CONFIG_PATH,
|
GUILD_CONFIG_PATH,
|
||||||
USER_INFRACTIONS_PATH,
|
USER_INFRACTIONS_PATH,
|
||||||
INFRACTION_BACKUP_DIR,
|
INFRACTION_BACKUP_DIR,
|
||||||
|
USER_APPEALS_PATH,
|
||||||
|
APPEAL_AI_MODEL,
|
||||||
|
APPEAL_AI_THINKING_BUDGET,
|
||||||
CONFIG_LOCK,
|
CONFIG_LOCK,
|
||||||
save_user_infractions,
|
save_user_infractions,
|
||||||
|
save_user_appeals,
|
||||||
get_guild_config,
|
get_guild_config,
|
||||||
set_guild_config,
|
set_guild_config,
|
||||||
get_user_infraction_history,
|
get_user_infraction_history,
|
||||||
add_user_infraction,
|
add_user_infraction,
|
||||||
|
get_user_appeals,
|
||||||
|
add_user_appeal,
|
||||||
SERVER_RULES,
|
SERVER_RULES,
|
||||||
SUICIDAL_HELP_RESOURCES,
|
SUICIDAL_HELP_RESOURCES,
|
||||||
)
|
)
|
||||||
@ -508,6 +514,9 @@ class AIModerationCog(commands.Cog):
|
|||||||
infractions_subgroup = app_commands.Group(
|
infractions_subgroup = app_commands.Group(
|
||||||
name="infractions", description="Manage user infractions.", parent=aimod_group
|
name="infractions", description="Manage user infractions.", parent=aimod_group
|
||||||
)
|
)
|
||||||
|
appeal_subgroup = app_commands.Group(
|
||||||
|
name="appeal", description="Appeal AI moderation actions.", parent=aimod_group
|
||||||
|
)
|
||||||
model_subgroup = app_commands.Group(
|
model_subgroup = app_commands.Group(
|
||||||
name="model",
|
name="model",
|
||||||
description="Manage the AI model for moderation.",
|
description="Manage the AI model for moderation.",
|
||||||
@ -844,6 +853,59 @@ class AIModerationCog(commands.Cog):
|
|||||||
|
|
||||||
await interaction.response.send_message(embed=embed, ephemeral=False)
|
await interaction.response.send_message(embed=embed, ephemeral=False)
|
||||||
|
|
||||||
|
@appeal_subgroup.command(
|
||||||
|
name="human_review",
|
||||||
|
description="Request a human moderator to review your case.",
|
||||||
|
)
|
||||||
|
@app_commands.describe(
|
||||||
|
reason="Explain why you want a human to review the AI decision",
|
||||||
|
guild_id="If using in DMs, provide the server ID",
|
||||||
|
)
|
||||||
|
async def appeal_human_review(
|
||||||
|
self,
|
||||||
|
interaction: discord.Interaction,
|
||||||
|
reason: str,
|
||||||
|
guild_id: int | None = None,
|
||||||
|
):
|
||||||
|
"""Let a user request a manual moderator review."""
|
||||||
|
guild = interaction.guild or (
|
||||||
|
self.bot.get_guild(guild_id) if guild_id else None
|
||||||
|
)
|
||||||
|
if not guild:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"Invalid or missing guild ID.", ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
log_channel_id = get_guild_config(guild.id, "MOD_LOG_CHANNEL_ID")
|
||||||
|
log_channel = self.bot.get_channel(log_channel_id) if log_channel_id else None
|
||||||
|
if not log_channel:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"Appeals are not enabled for this server.", ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
timestamp = datetime.datetime.utcnow().isoformat()
|
||||||
|
await add_user_appeal(
|
||||||
|
guild.id, interaction.user.id, "HUMAN_REVIEW", reason, timestamp, ""
|
||||||
|
)
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title="Human Review Requested", color=discord.Color.orange()
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name="User",
|
||||||
|
value=f"{interaction.user} ({interaction.user.id})",
|
||||||
|
inline=False,
|
||||||
|
)
|
||||||
|
embed.add_field(name="Request", value=reason, inline=False)
|
||||||
|
embed.timestamp = discord.utils.utcnow()
|
||||||
|
await log_channel.send(embed=embed)
|
||||||
|
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"Your request for a human review has been sent.", ephemeral=True
|
||||||
|
)
|
||||||
|
|
||||||
@infractions_subgroup.command(
|
@infractions_subgroup.command(
|
||||||
name="clear",
|
name="clear",
|
||||||
description="Clear a user's AI moderation infraction history (admin only).",
|
description="Clear a user's AI moderation infraction history (admin only).",
|
||||||
@ -933,6 +995,102 @@ class AIModerationCog(commands.Cog):
|
|||||||
f"Failed to restore infractions: {e}", ephemeral=True
|
f"Failed to restore infractions: {e}", ephemeral=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@appeal_subgroup.command(name="submit", description="Submit a moderation appeal.")
|
||||||
|
@app_commands.describe(
|
||||||
|
action="The action you are appealing",
|
||||||
|
reason="Explain why you believe the action was incorrect",
|
||||||
|
guild_id="If using in DMs, provide the server ID",
|
||||||
|
)
|
||||||
|
async def appeal_submit(
|
||||||
|
self,
|
||||||
|
interaction: discord.Interaction,
|
||||||
|
action: str,
|
||||||
|
reason: str,
|
||||||
|
guild_id: int | None = None,
|
||||||
|
):
|
||||||
|
guild = interaction.guild or (
|
||||||
|
self.bot.get_guild(guild_id) if guild_id else None
|
||||||
|
)
|
||||||
|
if not guild:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"Invalid or missing guild ID.", ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
log_channel_id = get_guild_config(guild.id, "MOD_LOG_CHANNEL_ID")
|
||||||
|
log_channel = self.bot.get_channel(log_channel_id) if log_channel_id else None
|
||||||
|
if not log_channel:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"Appeals are not enabled for this server.", ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
ai_review = await self.run_appeal_ai(guild, interaction.user, action, reason)
|
||||||
|
timestamp = datetime.datetime.utcnow().isoformat()
|
||||||
|
await add_user_appeal(
|
||||||
|
guild.id, interaction.user.id, action, reason, timestamp, ai_review
|
||||||
|
)
|
||||||
|
|
||||||
|
embed = discord.Embed(title="New Appeal", color=discord.Color.blue())
|
||||||
|
embed.add_field(
|
||||||
|
name="User",
|
||||||
|
value=f"{interaction.user} ({interaction.user.id})",
|
||||||
|
inline=False,
|
||||||
|
)
|
||||||
|
embed.add_field(name="Action", value=action, inline=False)
|
||||||
|
embed.add_field(name="Appeal", value=reason, inline=False)
|
||||||
|
embed.add_field(name="AI Review", value=ai_review[:1000], inline=False)
|
||||||
|
embed.timestamp = discord.utils.utcnow()
|
||||||
|
await log_channel.send(embed=embed)
|
||||||
|
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"Your appeal has been submitted.", ephemeral=True
|
||||||
|
)
|
||||||
|
|
||||||
|
@appeal_subgroup.command(
|
||||||
|
name="list", description="View a user's appeals (mods only)."
|
||||||
|
)
|
||||||
|
@app_commands.describe(user="The user to view appeals for")
|
||||||
|
async def appeal_list(self, interaction: discord.Interaction, user: discord.Member):
|
||||||
|
moderator_role_id = get_guild_config(interaction.guild.id, "MODERATOR_ROLE_ID")
|
||||||
|
moderator_role = (
|
||||||
|
interaction.guild.get_role(moderator_role_id) if moderator_role_id else None
|
||||||
|
)
|
||||||
|
has_permission = interaction.user.guild_permissions.administrator or (
|
||||||
|
moderator_role and moderator_role in interaction.user.roles
|
||||||
|
)
|
||||||
|
if not has_permission:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"You must be an administrator or have the moderator role to use this command.",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
appeals = get_user_appeals(interaction.guild.id, user.id)
|
||||||
|
if not appeals:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
f"{user.mention} has no appeals.", ephemeral=False
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=f"Appeals for {user.display_name}", color=discord.Color.blue()
|
||||||
|
)
|
||||||
|
for i, appeal in enumerate(appeals, 1):
|
||||||
|
ts = appeal.get("timestamp", "?")[:19].replace("T", " ")
|
||||||
|
summary = appeal.get("appeal_text", "")
|
||||||
|
ai_sum = appeal.get("ai_review", "")
|
||||||
|
if len(summary) > 150:
|
||||||
|
summary = summary[:147] + "..."
|
||||||
|
if len(ai_sum) > 150:
|
||||||
|
ai_sum = ai_sum[:147] + "..."
|
||||||
|
embed.add_field(
|
||||||
|
name=f"Appeal #{i} - {ts}",
|
||||||
|
value=f"Action: {appeal.get('action')}\nReason: {summary}\nAI: {ai_sum}",
|
||||||
|
inline=False,
|
||||||
|
)
|
||||||
|
await interaction.response.send_message(embed=embed, ephemeral=False)
|
||||||
|
|
||||||
@model_subgroup.command(
|
@model_subgroup.command(
|
||||||
name="set", description="Change the AI model used for moderation (admin only)."
|
name="set", description="Change the AI model used for moderation (admin only)."
|
||||||
)
|
)
|
||||||
@ -1480,9 +1638,7 @@ CRITICAL: Do NOT output anything other than the required JSON response.
|
|||||||
else:
|
else:
|
||||||
model_path = model_id_to_use
|
model_path = model_id_to_use
|
||||||
|
|
||||||
thinking_config = types.ThinkingConfig(
|
thinking_config = types.ThinkingConfig(thinking_budget=0)
|
||||||
thinking_budget=0
|
|
||||||
)
|
|
||||||
|
|
||||||
generation_config = types.GenerateContentConfig(
|
generation_config = types.GenerateContentConfig(
|
||||||
temperature=0.2,
|
temperature=0.2,
|
||||||
@ -1985,10 +2141,24 @@ CRITICAL: Do NOT output anything other than the required JSON response.
|
|||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
dm_channel = await message.author.create_dm()
|
dm_channel = await message.author.create_dm()
|
||||||
await dm_channel.send(
|
warn_embed = discord.Embed(
|
||||||
f"Your recent message in **{message.guild.name}** was removed for violating Rule **{rule_violated}**. "
|
title="⚠️ Moderation Warning",
|
||||||
f"Reason: _{reasoning}_. Please review the server rules. This is a formal warning."
|
description=(
|
||||||
|
f"Your recent message in **{message.guild.name}** was removed for violating **Rule {rule_violated}**."
|
||||||
|
),
|
||||||
|
color=discord.Color.orange(),
|
||||||
)
|
)
|
||||||
|
if message.content:
|
||||||
|
warn_embed.add_field(
|
||||||
|
name="Message Content",
|
||||||
|
value=message.content[:1024],
|
||||||
|
inline=False,
|
||||||
|
)
|
||||||
|
warn_embed.add_field(name="Reason", value=reasoning, inline=False)
|
||||||
|
warn_embed.set_footer(
|
||||||
|
text="Please review the server rules. This is a formal warning."
|
||||||
|
)
|
||||||
|
await dm_channel.send(embed=warn_embed)
|
||||||
action_taken_message += " User notified via DM with warning."
|
action_taken_message += " User notified via DM with warning."
|
||||||
except discord.Forbidden:
|
except discord.Forbidden:
|
||||||
print(
|
print(
|
||||||
@ -2164,6 +2334,55 @@ CRITICAL: Do NOT output anything other than the required JSON response.
|
|||||||
"FATAL: Bot lacks permission to send messages, even error notifications."
|
"FATAL: Bot lacks permission to send messages, even error notifications."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def run_appeal_ai(
|
||||||
|
self, guild: discord.Guild, member: discord.User, action: str, appeal_text: str
|
||||||
|
) -> str:
|
||||||
|
"""Run the appeal text through the higher tier AI model."""
|
||||||
|
if not self.genai_client:
|
||||||
|
return "AI review unavailable."
|
||||||
|
|
||||||
|
history = get_user_infraction_history(guild.id, member.id)
|
||||||
|
history_text = json.dumps(history, indent=2) if history else "None"
|
||||||
|
|
||||||
|
system_prompt = (
|
||||||
|
"You are reviewing a user's appeal of a moderation action. "
|
||||||
|
"Think very extensively about the appeal, the provided history, and the server rules. "
|
||||||
|
"Return a short verdict (UPHOLD or OVERTURN) and your reasoning in plain text."
|
||||||
|
)
|
||||||
|
|
||||||
|
user_prompt = (
|
||||||
|
f"Server Rules:\n{SERVER_RULES}\n\n"
|
||||||
|
f"User History:\n{history_text}\n\n"
|
||||||
|
f"Action Appealed: {action}\n"
|
||||||
|
f"Appeal Text: {appeal_text}"
|
||||||
|
)
|
||||||
|
|
||||||
|
generation_config = types.GenerateContentConfig(
|
||||||
|
temperature=0.2,
|
||||||
|
max_output_tokens=8192,
|
||||||
|
safety_settings=STANDARD_SAFETY_SETTINGS,
|
||||||
|
thinking_config=types.ThinkingConfig(
|
||||||
|
thinking_budget=APPEAL_AI_THINKING_BUDGET
|
||||||
|
),
|
||||||
|
system_instruction=types.Content(
|
||||||
|
role="system", parts=[types.Part(text=system_prompt)]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = await self.genai_client.aio.models.generate_content(
|
||||||
|
model=f"publishers/google/models/{APPEAL_AI_MODEL}",
|
||||||
|
contents=[
|
||||||
|
types.Content(role="user", parts=[types.Part(text=user_prompt)])
|
||||||
|
],
|
||||||
|
config=generation_config,
|
||||||
|
)
|
||||||
|
result = self._get_response_text(response)
|
||||||
|
return result or "AI review failed to produce output."
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f"Appeal AI error: {e}")
|
||||||
|
return "AI review encountered an error."
|
||||||
|
|
||||||
@commands.Cog.listener(name="on_message")
|
@commands.Cog.listener(name="on_message")
|
||||||
async def message_listener(self, message: discord.Message):
|
async def message_listener(self, message: discord.Message):
|
||||||
"""Listens to messages and triggers moderation checks."""
|
"""Listens to messages and triggers moderation checks."""
|
||||||
|
@ -31,6 +31,12 @@ GUILD_CONFIG_DIR = "data/"
|
|||||||
GUILD_CONFIG_PATH = os.path.join(GUILD_CONFIG_DIR, "guild_config.json")
|
GUILD_CONFIG_PATH = os.path.join(GUILD_CONFIG_DIR, "guild_config.json")
|
||||||
USER_INFRACTIONS_PATH = os.path.join(GUILD_CONFIG_DIR, "user_infractions.json")
|
USER_INFRACTIONS_PATH = os.path.join(GUILD_CONFIG_DIR, "user_infractions.json")
|
||||||
INFRACTION_BACKUP_DIR = os.path.join(GUILD_CONFIG_DIR, "infraction_backups")
|
INFRACTION_BACKUP_DIR = os.path.join(GUILD_CONFIG_DIR, "infraction_backups")
|
||||||
|
USER_APPEALS_PATH = os.path.join(GUILD_CONFIG_DIR, "user_appeals.json")
|
||||||
|
|
||||||
|
# AI model used for appeal reviews
|
||||||
|
APPEAL_AI_MODEL = "gemini-2.5-pro-preview-06-05"
|
||||||
|
# Thinking budget for appeal AI
|
||||||
|
APPEAL_AI_THINKING_BUDGET = 32768
|
||||||
|
|
||||||
os.makedirs(INFRACTION_BACKUP_DIR, exist_ok=True)
|
os.makedirs(INFRACTION_BACKUP_DIR, exist_ok=True)
|
||||||
os.makedirs(GUILD_CONFIG_DIR, exist_ok=True)
|
os.makedirs(GUILD_CONFIG_DIR, exist_ok=True)
|
||||||
@ -55,6 +61,16 @@ except Exception as e: # noqa: BLE001
|
|||||||
print(f"Failed to load user infractions from {USER_INFRACTIONS_PATH}: {e}")
|
print(f"Failed to load user infractions from {USER_INFRACTIONS_PATH}: {e}")
|
||||||
USER_INFRACTIONS = {}
|
USER_INFRACTIONS = {}
|
||||||
|
|
||||||
|
if not os.path.exists(USER_APPEALS_PATH):
|
||||||
|
with open(USER_APPEALS_PATH, "w", encoding="utf-8") as f:
|
||||||
|
json.dump({}, f)
|
||||||
|
try:
|
||||||
|
with open(USER_APPEALS_PATH, "r", encoding="utf-8") as f:
|
||||||
|
USER_APPEALS = json.load(f)
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f"Failed to load user appeals from {USER_APPEALS_PATH}: {e}")
|
||||||
|
USER_APPEALS = {}
|
||||||
|
|
||||||
CONFIG_LOCK = asyncio.Lock()
|
CONFIG_LOCK = asyncio.Lock()
|
||||||
|
|
||||||
|
|
||||||
@ -76,6 +92,15 @@ async def save_user_infractions():
|
|||||||
print(f"Failed to save user infractions: {e}")
|
print(f"Failed to save user infractions: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def save_user_appeals():
|
||||||
|
async with CONFIG_LOCK:
|
||||||
|
try:
|
||||||
|
async with aiofiles.open(USER_APPEALS_PATH, "w", encoding="utf-8") as f:
|
||||||
|
await f.write(json.dumps(USER_APPEALS, indent=2))
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f"Failed to save user appeals: {e}")
|
||||||
|
|
||||||
|
|
||||||
def get_guild_config(guild_id: int, key: str, default=None):
|
def get_guild_config(guild_id: int, key: str, default=None):
|
||||||
guild_str = str(guild_id)
|
guild_str = str(guild_id)
|
||||||
if guild_str in GUILD_CONFIG and key in GUILD_CONFIG[guild_str]:
|
if guild_str in GUILD_CONFIG and key in GUILD_CONFIG[guild_str]:
|
||||||
@ -119,6 +144,34 @@ async def add_user_infraction(
|
|||||||
await save_user_infractions()
|
await save_user_infractions()
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_appeals(guild_id: int, user_id: int) -> list:
|
||||||
|
key = f"{guild_id}_{user_id}"
|
||||||
|
return USER_APPEALS.get(key, [])
|
||||||
|
|
||||||
|
|
||||||
|
async def add_user_appeal(
|
||||||
|
guild_id: int,
|
||||||
|
user_id: int,
|
||||||
|
action: str,
|
||||||
|
appeal_text: str,
|
||||||
|
timestamp: str,
|
||||||
|
ai_review: str,
|
||||||
|
):
|
||||||
|
key = f"{guild_id}_{user_id}"
|
||||||
|
if key not in USER_APPEALS:
|
||||||
|
USER_APPEALS[key] = []
|
||||||
|
|
||||||
|
appeal_record = {
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"action": action,
|
||||||
|
"appeal_text": appeal_text,
|
||||||
|
"ai_review": ai_review,
|
||||||
|
}
|
||||||
|
USER_APPEALS[key].append(appeal_record)
|
||||||
|
USER_APPEALS[key] = USER_APPEALS[key][-10:]
|
||||||
|
await save_user_appeals()
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_SERVER_RULES = """
|
DEFAULT_SERVER_RULES = """
|
||||||
# Server Rules
|
# Server Rules
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user