From b74002e69f52cae15ed785a5f0bedcfae1c3ca85 Mon Sep 17 00:00:00 2001 From: Franz Rolfsvaag Date: Mon, 11 Aug 2025 00:30:23 +0200 Subject: [PATCH] 0.3.9.4.a1 Added image/video proof to pirate reports as a requirement. This is displayed in-line for moderators in the review message --- bot.py | 2 +- modules/pirate_report/pirate_report.py | 116 +++++++++++++++++++++---- 2 files changed, 99 insertions(+), 19 deletions(-) diff --git a/bot.py b/bot.py index c85e294..80bf758 100644 --- a/bot.py +++ b/bot.py @@ -9,7 +9,7 @@ from modules.common.boot_notice import post_boot_notice # Version consists of: # Major.Enhancement.Minor.Patch.Test (Test is alphanumeric; doesn’t trigger auto update) -VERSION = "0.3.9.3.a4" +VERSION = "0.3.9.4.a1" # ---------- Env loading ---------- diff --git a/modules/pirate_report/pirate_report.py b/modules/pirate_report/pirate_report.py index 13cf025..5b6d654 100644 --- a/modules/pirate_report/pirate_report.py +++ b/modules/pirate_report/pirate_report.py @@ -2,6 +2,7 @@ import re import time from datetime import datetime, timezone +from urllib.parse import urlparse import discord from discord.ext import commands from discord import app_commands @@ -21,6 +22,11 @@ CROSS = '❌' _ACCT_RE = re.compile(r'.+#\d{5}$') +DISCORD_MEDIA_HOSTS = {"cdn.discordapp.com", "media.discordapp.net"} +MEDIA_EXTS_IMAGE = {".png", ".jpg", ".jpeg", ".gif", ".webp"} +MEDIA_EXTS_VIDEO = {".mp4", ".webm", ".mov"} +MEDIA_EXTS_ALL = MEDIA_EXTS_IMAGE | MEDIA_EXTS_VIDEO + def _acct_ok(s: str) -> bool: return bool(_ACCT_RE.fullmatch(s.strip())) @@ -35,6 +41,51 @@ def _parse_bool(s: str) -> bool: return False raise ValueError("Please enter yes or no") +def _classify_discord_media(url: str): + """ + Return ('image'|'video', normalized_url) if valid Discord CDN media; else (None, reason). + We only accept direct CDN links so media can render inline without leaving the channel. + """ + try: + u = url.strip() + if not u: + return (None, "Empty URL.") + pr = urlparse(u) + if pr.scheme not in ("http", "https"): + return (None, "URL must start with http:// or https://") + host = pr.netloc.lower() + if host not in DISCORD_MEDIA_HOSTS: + return (None, "URL must be a **Discord media** link (cdn.discordapp.com or media.discordapp.net).") + path = pr.path or "" + dot = path.rfind(".") + if dot == -1: + return (None, "URL must end with a known media file extension.") + ext = path[dot:].lower() + if ext not in MEDIA_EXTS_ALL: + return (None, f"Unsupported media type `{ext}`. Allowed: images {sorted(MEDIA_EXTS_IMAGE)}, videos {sorted(MEDIA_EXTS_VIDEO)}.") + kind = "image" if ext in MEDIA_EXTS_IMAGE else "video" + return (kind, u) + except Exception: + return (None, "Invalid URL format.") + +def _make_report_embed(title: str, color: discord.Color, report_dict: dict, include_status: bool = False): + e = discord.Embed(title=title, color=color, timestamp=datetime.utcnow()) + e.add_field(name="Character", value=report_dict['character_name'], inline=False) + e.add_field(name="Account", value=report_dict['account_name'], inline=False) + e.add_field(name="Submitted by", value=f"<@{report_dict['submitter_id']}>", inline=False) + if include_status: + e.add_field( + name="Status", + value=report_dict.get('status_line', 'Pending'), + inline=False + ) + # If proof is an image, show it in the embed (videos are posted as message content) + if report_dict.get('proof_type') == 'image': + e.set_image(url=report_dict.get('proof_url', '')) + # A small hint footer + e.set_footer(text="Proof is included in this message.") + return e + # --------------------- Modals ---------------------- class ReportModal(discord.ui.Modal, title="Submit Pirate Report"): @@ -54,9 +105,16 @@ class ReportModal(discord.ui.Modal, title="Submit Pirate Report"): max_length=64, required=True ) + self.proof_url = discord.ui.TextInput( + label="Proof (Discord media URL)", + placeholder="Direct link to image/video from Discord (CDN)", + required=True, + max_length=300 + ) self.add_item(self.character_name) self.add_item(self.account_name) + self.add_item(self.proof_url) async def on_submit(self, interaction: discord.Interaction): if not interaction.guild: @@ -64,6 +122,7 @@ class ReportModal(discord.ui.Modal, title="Submit Pirate Report"): char = self.character_name.value.strip() acct = self.account_name.value.strip() + proof_raw = self.proof_url.value.strip() if not _acct_ok(acct): return await interaction.response.send_message( @@ -71,6 +130,10 @@ class ReportModal(discord.ui.Modal, title="Submit Pirate Report"): ephemeral=True ) + proof_type, proof_val = _classify_discord_media(proof_raw) + if proof_type is None: + return await interaction.response.send_message(f"❌ Invalid proof link: {proof_val}", ephemeral=True) + dm = self.cog.bot.data_manager char_l = char.lower() acct_l = acct.lower() @@ -97,18 +160,24 @@ class ReportModal(discord.ui.Modal, title="Submit Pirate Report"): print("[pirate_report] ack send failed:", repr(e)) ack = None - # Send to mod channel with ✅/❌ + # Send to mod channel with ✅/❌, including inline media mod_ch = interaction.guild.get_channel(self.cog.mod_channel) if not mod_ch: return await interaction.response.send_message("❌ Mod channel not configured.", ephemeral=True) + report_payload = { + 'character_name': char, + 'account_name': acct, + 'submitter_id': interaction.user.id, + 'proof_url': proof_val, + 'proof_type': proof_type, + } + try: - mod_msg = await mod_ch.send( - f"🚩 **Pirate Report**\n" - f"**Character:** {char}\n" - f"**Account:** {acct}\n" - f"**Submitted by:** {interaction.user.mention}" - ) + embed = _make_report_embed("🚩 Pirate Report", discord.Color.orange(), report_payload, include_status=False) + # Video must be in message content for inline player; images live in the embed + content = proof_val if proof_type == 'video' else None + mod_msg = await mod_ch.send(content=content, embed=embed) await mod_msg.add_reaction(CHECK) await mod_msg.add_reaction(CROSS) except Exception as e: @@ -124,7 +193,9 @@ class ReportModal(discord.ui.Modal, title="Submit Pirate Report"): 'origin_channel_id': interaction.channel.id if interaction.channel else 0, 'ack_message_id': ack.id if ack else 0, 'status': 'pending', - 'ts': now + 'ts': now, + 'proof_url': proof_val, + 'proof_type': proof_type, }) await interaction.response.send_message("✅ Report submitted to moderators.", ephemeral=True) @@ -525,20 +596,29 @@ class PirateReportCog(commands.Cog): guild = channel.guild stamp = _now_utc_str() - header_emoji = CHECK if approved else CROSS - new_content = ( - f"{header_emoji} **Pirate Report**\n" - f"**Character:** {report['character_name']}\n" - f"**Account:** {report['account_name']}\n" - f"**Submitted by:** <@{report['submitter_id']}>\n\n" - f"**Status:** {'Approved' if approved else 'Rejected'} by <@{payload.user_id}> on {stamp}" - ) + # Build new embed + content (keep proof visible) + status_text = f"{'Approved' if approved else 'Rejected'} by <@{payload.user_id}> on {stamp}" + report_view = { + 'character_name': report['character_name'], + 'account_name': report['account_name'], + 'submitter_id': report['submitter_id'], + 'proof_url': report.get('proof_url', ''), + 'proof_type': report.get('proof_type', ''), + 'status_line': status_text, + } + color = discord.Color.green() if approved else discord.Color.red() + title = "✅ Pirate Report — Approved" if approved else "❌ Pirate Report — Rejected" + new_embed = _make_report_embed(title, color, report_view, include_status=True) + + # For videos, ensure the URL stays in message content so the inline player remains visible. + new_content = report_view['proof_url'] if report_view.get('proof_type') == 'video' else None + try: await msg.clear_reactions() except Exception as e: print("[pirate_report] clear reactions failed:", repr(e)) try: - await msg.edit(content=new_content) + await msg.edit(content=new_content, embed=new_embed) except Exception as e: print("[pirate_report] edit mod msg failed:", repr(e)) @@ -595,4 +675,4 @@ async def setup(bot): else: bot.tree.add_command(cog.report) bot.tree.add_command(cog.edit_pirate) - bot.tree.add_command(cog.encounter) \ No newline at end of file + bot.tree.add_command(cog.encounter)