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
This commit is contained in:
Franz Rolfsvaag 2025-08-11 00:30:23 +02:00
parent 7b5bcff6ac
commit b74002e69f
2 changed files with 99 additions and 19 deletions

2
bot.py
View File

@ -9,7 +9,7 @@ from modules.common.boot_notice import post_boot_notice
# Version consists of: # Version consists of:
# Major.Enhancement.Minor.Patch.Test (Test is alphanumeric; doesnt trigger auto update) # Major.Enhancement.Minor.Patch.Test (Test is alphanumeric; doesnt trigger auto update)
VERSION = "0.3.9.3.a4" VERSION = "0.3.9.4.a1"
# ---------- Env loading ---------- # ---------- Env loading ----------

View File

@ -2,6 +2,7 @@
import re import re
import time import time
from datetime import datetime, timezone from datetime import datetime, timezone
from urllib.parse import urlparse
import discord import discord
from discord.ext import commands from discord.ext import commands
from discord import app_commands from discord import app_commands
@ -21,6 +22,11 @@ CROSS = '❌'
_ACCT_RE = re.compile(r'.+#\d{5}$') _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: def _acct_ok(s: str) -> bool:
return bool(_ACCT_RE.fullmatch(s.strip())) return bool(_ACCT_RE.fullmatch(s.strip()))
@ -35,6 +41,51 @@ def _parse_bool(s: str) -> bool:
return False return False
raise ValueError("Please enter yes or no") 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 ---------------------- # --------------------- Modals ----------------------
class ReportModal(discord.ui.Modal, title="Submit Pirate Report"): 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, max_length=64,
required=True 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.character_name)
self.add_item(self.account_name) self.add_item(self.account_name)
self.add_item(self.proof_url)
async def on_submit(self, interaction: discord.Interaction): async def on_submit(self, interaction: discord.Interaction):
if not interaction.guild: if not interaction.guild:
@ -64,6 +122,7 @@ class ReportModal(discord.ui.Modal, title="Submit Pirate Report"):
char = self.character_name.value.strip() char = self.character_name.value.strip()
acct = self.account_name.value.strip() acct = self.account_name.value.strip()
proof_raw = self.proof_url.value.strip()
if not _acct_ok(acct): if not _acct_ok(acct):
return await interaction.response.send_message( return await interaction.response.send_message(
@ -71,6 +130,10 @@ class ReportModal(discord.ui.Modal, title="Submit Pirate Report"):
ephemeral=True 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 dm = self.cog.bot.data_manager
char_l = char.lower() char_l = char.lower()
acct_l = acct.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)) print("[pirate_report] ack send failed:", repr(e))
ack = None 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) mod_ch = interaction.guild.get_channel(self.cog.mod_channel)
if not mod_ch: if not mod_ch:
return await interaction.response.send_message("❌ Mod channel not configured.", ephemeral=True) 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: try:
mod_msg = await mod_ch.send( embed = _make_report_embed("🚩 Pirate Report", discord.Color.orange(), report_payload, include_status=False)
f"🚩 **Pirate Report**\n" # Video must be in message content for inline player; images live in the embed
f"**Character:** {char}\n" content = proof_val if proof_type == 'video' else None
f"**Account:** {acct}\n" mod_msg = await mod_ch.send(content=content, embed=embed)
f"**Submitted by:** {interaction.user.mention}"
)
await mod_msg.add_reaction(CHECK) await mod_msg.add_reaction(CHECK)
await mod_msg.add_reaction(CROSS) await mod_msg.add_reaction(CROSS)
except Exception as e: 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, 'origin_channel_id': interaction.channel.id if interaction.channel else 0,
'ack_message_id': ack.id if ack else 0, 'ack_message_id': ack.id if ack else 0,
'status': 'pending', '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) await interaction.response.send_message("✅ Report submitted to moderators.", ephemeral=True)
@ -525,20 +596,29 @@ class PirateReportCog(commands.Cog):
guild = channel.guild guild = channel.guild
stamp = _now_utc_str() stamp = _now_utc_str()
header_emoji = CHECK if approved else CROSS # Build new embed + content (keep proof visible)
new_content = ( status_text = f"{'Approved' if approved else 'Rejected'} by <@{payload.user_id}> on {stamp}"
f"{header_emoji} **Pirate Report**\n" report_view = {
f"**Character:** {report['character_name']}\n" 'character_name': report['character_name'],
f"**Account:** {report['account_name']}\n" 'account_name': report['account_name'],
f"**Submitted by:** <@{report['submitter_id']}>\n\n" 'submitter_id': report['submitter_id'],
f"**Status:** {'Approved' if approved else 'Rejected'} by <@{payload.user_id}> on {stamp}" '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: try:
await msg.clear_reactions() await msg.clear_reactions()
except Exception as e: except Exception as e:
print("[pirate_report] clear reactions failed:", repr(e)) print("[pirate_report] clear reactions failed:", repr(e))
try: try:
await msg.edit(content=new_content) await msg.edit(content=new_content, embed=new_embed)
except Exception as e: except Exception as e:
print("[pirate_report] edit mod msg failed:", repr(e)) print("[pirate_report] edit mod msg failed:", repr(e))
@ -595,4 +675,4 @@ async def setup(bot):
else: else:
bot.tree.add_command(cog.report) bot.tree.add_command(cog.report)
bot.tree.add_command(cog.edit_pirate) bot.tree.add_command(cog.edit_pirate)
bot.tree.add_command(cog.encounter) bot.tree.add_command(cog.encounter)