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:
# 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 ----------

View File

@ -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)
bot.tree.add_command(cog.encounter)