diff --git a/cogs/aimod_cog.py b/cogs/aimod_cog.py index 723986a..c754233 100644 --- a/cogs/aimod_cog.py +++ b/cogs/aimod_cog.py @@ -1001,6 +1001,7 @@ class AIModerationCog(commands.Cog): 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", + message_id="ID of the moderated message you are appealing (optional)", ) async def appeal_submit( self, @@ -1008,6 +1009,7 @@ class AIModerationCog(commands.Cog): action: str, reason: str, guild_id: int | None = None, + message_id: int | None = None, ): guild = interaction.guild or ( self.bot.get_guild(guild_id) if guild_id else None @@ -1026,10 +1028,33 @@ class AIModerationCog(commands.Cog): ) return - ai_review = await self.run_appeal_ai(guild, interaction.user, action, reason) + infractions = get_user_infraction_history(guild.id, interaction.user.id) + target_infraction = None + if message_id: + for infr in infractions[::-1]: + if infr.get("message_id") == message_id: + target_infraction = infr + break + if not target_infraction and infractions: + target_infraction = infractions[-1] + + ai_review = await self.run_appeal_ai( + guild, + interaction.user, + action, + reason, + target_infraction, + ) timestamp = datetime.datetime.utcnow().isoformat() + ref = target_infraction.get("message_id") if target_infraction else None await add_user_appeal( - guild.id, interaction.user.id, action, reason, timestamp, ai_review + guild.id, + interaction.user.id, + action, + reason, + timestamp, + ai_review, + str(ref) if ref else None, ) embed = discord.Embed(title="New Appeal", color=discord.Color.blue()) @@ -1039,6 +1064,8 @@ class AIModerationCog(commands.Cog): inline=False, ) embed.add_field(name="Action", value=action, inline=False) + if ref: + embed.add_field(name="Infraction", value=f"Message ID: {ref}", 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() @@ -1085,11 +1112,11 @@ class AIModerationCog(commands.Cog): 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, - ) + value = f"Action: {appeal.get('action')}\nReason: {summary}\nAI: {ai_sum}" + ref = appeal.get("infraction_reference") + if ref: + value = f"Infraction: {ref}\n" + value + embed.add_field(name=f"Appeal #{i} - {ts}", value=value, inline=False) await interaction.response.send_message(embed=embed, ephemeral=False) @appeal_subgroup.command( @@ -1121,7 +1148,7 @@ class AIModerationCog(commands.Cog): results: list[tuple[str, str]] = [] for action, text in scenarios: result = await self.run_appeal_ai( - interaction.guild, interaction.user, action, text + interaction.guild, interaction.user, action, text, None ) results.append((action, result)) @@ -1820,6 +1847,7 @@ CRITICAL: Do NOT output anything other than the required JSON response. message: discord.Message, ai_decision: dict, notify_mods_message: str = None, + link_urls: list[str] | None = None, ): """ Takes action based on the AI's violation decision. @@ -2071,6 +2099,10 @@ CRITICAL: Do NOT output anything other than the required JSON response. "BAN", reasoning, current_timestamp_iso, + message.id, + message.channel.id, + message.content[:100] if message.content else "", + [a.url for a in message.attachments] + (link_urls or []), ) elif action == "KICK": @@ -2103,6 +2135,10 @@ CRITICAL: Do NOT output anything other than the required JSON response. "KICK", reasoning, current_timestamp_iso, + message.id, + message.channel.id, + message.content[:100] if message.content else "", + [a.url for a in message.attachments] + (link_urls or []), ) elif action.startswith("TIMEOUT"): @@ -2154,6 +2190,10 @@ CRITICAL: Do NOT output anything other than the required JSON response. action, reasoning, current_timestamp_iso, + message.id, + message.channel.id, + message.content[:100] if message.content else "", + [a.url for a in message.attachments] + (link_urls or []), ) else: action_taken_message = ( @@ -2221,6 +2261,10 @@ CRITICAL: Do NOT output anything other than the required JSON response. "WARN", reasoning, current_timestamp_iso, + message.id, + message.channel.id, + message.content[:100] if message.content else "", + [a.url for a in message.attachments] + (link_urls or []), ) elif action == "NOTIFY_MODS": @@ -2381,7 +2425,12 @@ CRITICAL: Do NOT output anything other than the required JSON response. ) async def run_appeal_ai( - self, guild: discord.Guild, member: discord.User, action: str, appeal_text: str + self, + guild: discord.Guild, + member: discord.User, + action: str, + appeal_text: str, + infraction: dict | None = None, ) -> str: """Run the appeal text through the higher tier AI model.""" if not self.genai_client: @@ -2396,10 +2445,28 @@ CRITICAL: Do NOT output anything other than the required JSON response. "Return a short verdict (UPHOLD or OVERTURN) and your reasoning in plain text." ) + context_lines = [] + if infraction: + channel_name = guild.get_channel(infraction.get("channel_id", 0)) + channel_display = ( + channel_name.name if channel_name else str(infraction.get("channel_id")) + ) + context_lines.append(f"Original Channel: {channel_display}") + msg_content = infraction.get("message_content") + if msg_content: + context_lines.append(f"Message Snippet: {msg_content}") + attachments = infraction.get("attachments") + if attachments: + context_lines.append(f"Attachments: {attachments}") + reasoning = infraction.get("reasoning") + if reasoning: + context_lines.append(f"AI Reasoning: {reasoning}") + user_prompt = ( f"Server Rules:\n{SERVER_RULES}\n\n" f"User History:\n{history_text}\n\n" - f"Action Appealed: {action}\n" + + ("\n".join(context_lines) + "\n\n" if context_lines else "") + + f"Action Appealed: {action}\n" f"Appeal Text: {appeal_text}" ) @@ -2439,6 +2506,7 @@ CRITICAL: Do NOT output anything other than the required JSON response. if message.author.bot: print(f"Ignoring message {message.id} from bot.") return + link_urls: list[str] = [] embed_urls = [embed.url for embed in message.embeds if embed.url] link_urls = ( self.extract_direct_attachment_urls(" ".join(embed_urls)) @@ -2613,7 +2681,9 @@ CRITICAL: Do NOT output anything other than the required JSON response. if ai_decision.get("action") == "NOTIFY_MODS" else None ) - await self.handle_violation(message, ai_decision, notify_mods_message) + await self.handle_violation( + message, ai_decision, notify_mods_message, link_urls + ) else: # AI found no violation print( @@ -2725,7 +2795,9 @@ CRITICAL: Do NOT output anything other than the required JSON response. results = [] guild = interaction.guild for action, text in scenarios: - review = await self.run_appeal_ai(guild, interaction.user, action, text) + review = await self.run_appeal_ai( + guild, interaction.user, action, text, None + ) results.append((action, text, review)) embed = discord.Embed( diff --git a/cogs/aimod_config.py b/cogs/aimod_config.py index 95e2913..0cecbdc 100644 --- a/cogs/aimod_config.py +++ b/cogs/aimod_config.py @@ -128,6 +128,10 @@ async def add_user_infraction( action_taken: str, reasoning: str, timestamp: str, + message_id: int | None = None, + channel_id: int | None = None, + message_content: str | None = None, + attachments: list[str] | None = None, ): key = f"{guild_id}_{user_id}" if key not in USER_INFRACTIONS: @@ -139,6 +143,14 @@ async def add_user_infraction( "action_taken": action_taken, "reasoning": reasoning, } + if message_id is not None: + infraction_record["message_id"] = message_id + if channel_id is not None: + infraction_record["channel_id"] = channel_id + if message_content is not None: + infraction_record["message_content"] = message_content + if attachments: + infraction_record["attachments"] = attachments USER_INFRACTIONS[key].append(infraction_record) USER_INFRACTIONS[key] = USER_INFRACTIONS[key][-10:] await save_user_infractions() @@ -156,6 +168,7 @@ async def add_user_appeal( appeal_text: str, timestamp: str, ai_review: str, + infraction_reference: str | None = None, ): key = f"{guild_id}_{user_id}" if key not in USER_APPEALS: @@ -166,6 +179,7 @@ async def add_user_appeal( "action": action, "appeal_text": appeal_text, "ai_review": ai_review, + "infraction_reference": infraction_reference, } USER_APPEALS[key].append(appeal_record) USER_APPEALS[key] = USER_APPEALS[key][-10:]