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:
parent
7b5bcff6ac
commit
b74002e69f
2
bot.py
2
bot.py
@ -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; doesn’t trigger auto update)
|
# 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 ----------
|
# ---------- Env loading ----------
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user