0.3.9.7.a3

Changes to Pirate Reports:
- Proof is no longer **required** but **encouraged**.
- Mods can click a button on the review to jump to the report ACK message
This commit is contained in:
Franz Rolfsvaag 2025-08-11 09:26:56 +02:00
parent b6980794d7
commit 4f0e000c93
2 changed files with 76 additions and 17 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.7.a2" VERSION = "0.3.9.7.a3"
# ---------- Env loading ---------- # ---------- Env loading ----------

View File

@ -27,12 +27,15 @@ MEDIA_EXTS_IMAGE = {".png", ".jpg", ".jpeg", ".gif", ".webp"}
MEDIA_EXTS_VIDEO = {".mp4", ".webm", ".mov"} MEDIA_EXTS_VIDEO = {".mp4", ".webm", ".mov"}
MEDIA_EXTS_ALL = MEDIA_EXTS_IMAGE | MEDIA_EXTS_VIDEO 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()))
def _now_utc_str() -> str: def _now_utc_str() -> str:
return datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC') return datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')
def _parse_bool(s: str) -> bool: def _parse_bool(s: str) -> bool:
v = s.strip().lower() v = s.strip().lower()
if v in ('y', 'yes', 'true', 't', '1'): if v in ('y', 'yes', 'true', 't', '1'):
@ -41,6 +44,7 @@ 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): def _classify_discord_media(url: str):
""" """
Return ('image'|'video', normalized_url) if valid Discord CDN media; else (None, reason). Return ('image'|'video', normalized_url) if valid Discord CDN media; else (None, reason).
@ -68,24 +72,53 @@ def _classify_discord_media(url: str):
except Exception: except Exception:
return (None, "Invalid URL format.") return (None, "Invalid URL format.")
def _jump_url(guild_id: int, channel_id: int, message_id: int) -> str:
return f"https://discord.com/channels/{guild_id}/{channel_id}/{message_id}"
def _make_report_embed(title: str, color: discord.Color, report_dict: dict, include_status: bool = False): 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 = 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="Character", value=report_dict['character_name'], inline=False)
e.add_field(name="Account", value=report_dict['account_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) e.add_field(name="Submitted by", value=f"<@{report_dict['submitter_id']}>", inline=False)
proof_url = (report_dict.get('proof_url') or "").strip()
proof_type = (report_dict.get('proof_type') or "").strip()
if include_status: if include_status:
e.add_field( e.add_field(
name="Status", name="Status",
value=report_dict.get('status_line', 'Pending'), value=report_dict.get('status_line', 'Pending'),
inline=False 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': # Show proof inline if it's an image; for videos we'll keep the URL in message content.
e.set_image(url=report_dict.get('proof_url', '')) if proof_url and proof_type == 'image':
e.set_image(url=proof_url)
# Add a compact proof field for quick visibility (always safe to include)
e.add_field(
name="Proof",
value=proof_url if proof_url else "_No proof provided — strongly encouraged._",
inline=False
)
# A small hint footer # A small hint footer
e.set_footer(text="Proof is included in this message.") e.set_footer(text="Attach proof when possible to speed up moderation.")
return e return e
# --------------------- Views ----------------------
class ReportJumpView(discord.ui.View):
"""Simple link button to jump to the user's original ack message."""
def __init__(self, url: str):
super().__init__(timeout=None)
if url:
self.add_item(discord.ui.Button(label="Jump to message", style=discord.ButtonStyle.link, url=url))
# --------------------- Modals ---------------------- # --------------------- Modals ----------------------
class ReportModal(discord.ui.Modal, title="Submit Pirate Report"): class ReportModal(discord.ui.Modal, title="Submit Pirate Report"):
@ -106,9 +139,9 @@ class ReportModal(discord.ui.Modal, title="Submit Pirate Report"):
required=True required=True
) )
self.proof_url = discord.ui.TextInput( self.proof_url = discord.ui.TextInput(
label="Proof (Discord media URL)", label="Proof (Discord media URL — optional)",
placeholder="Direct link to image/video from Discord (CDN)", placeholder="Direct Discord CDN link to image/video (highly encouraged, but optional)",
required=True, required=False,
max_length=300 max_length=300
) )
@ -122,7 +155,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() proof_raw = (self.proof_url.value or "").strip()
if not _acct_ok(acct): if not _acct_ok(acct):
return await interaction.response.send_message( return await interaction.response.send_message(
@ -130,9 +163,13 @@ class ReportModal(discord.ui.Modal, title="Submit Pirate Report"):
ephemeral=True ephemeral=True
) )
proof_type, proof_val = _classify_discord_media(proof_raw) proof_type = ""
if proof_type is None: proof_val = ""
return await interaction.response.send_message(f"❌ Invalid proof link: {proof_val}", ephemeral=True) if proof_raw:
pt, pv = _classify_discord_media(proof_raw)
if pt is None:
return await interaction.response.send_message(f"❌ Invalid proof link: {pv}", ephemeral=True)
proof_type, proof_val = pt, pv # valid
dm = self.cog.bot.data_manager dm = self.cog.bot.data_manager
char_l = char.lower() char_l = char.lower()
@ -160,7 +197,7 @@ 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 ✅/❌, including inline media # Send to mod channel with ✅/❌, including inline media if present
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)
@ -173,11 +210,20 @@ class ReportModal(discord.ui.Modal, title="Submit Pirate Report"):
'proof_type': proof_type, 'proof_type': proof_type,
} }
# Build jump button (if ack exists)
view = None
if ack:
try:
url = _jump_url(interaction.guild.id, interaction.channel.id, ack.id)
view = ReportJumpView(url)
except Exception:
view = None
try: try:
embed = _make_report_embed("🚩 Pirate Report", discord.Color.orange(), report_payload, include_status=False) 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 # Video must be in message content for inline player; images live in the embed
content = proof_val if proof_type == 'video' else None content = proof_val if proof_type == 'video' else None
mod_msg = await mod_ch.send(content=content, embed=embed) mod_msg = await mod_ch.send(content=content, embed=embed, view=view)
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:
@ -200,6 +246,7 @@ class ReportModal(discord.ui.Modal, title="Submit Pirate Report"):
await interaction.response.send_message("✅ Report submitted to moderators.", ephemeral=True) await interaction.response.send_message("✅ Report submitted to moderators.", ephemeral=True)
class EditPirateModal(discord.ui.Modal, title="Edit Pirate Entry"): class EditPirateModal(discord.ui.Modal, title="Edit Pirate Entry"):
def __init__(self, cog: "PirateReportCog"): def __init__(self, cog: "PirateReportCog"):
super().__init__() super().__init__()
@ -274,6 +321,7 @@ class EditPirateModal(discord.ui.Modal, title="Edit Pirate Entry"):
await interaction.response.send_message("✅ Pirate updated.", ephemeral=True) await interaction.response.send_message("✅ Pirate updated.", ephemeral=True)
await self.cog._refresh_pirates_list(interaction.guild) await self.cog._refresh_pirates_list(interaction.guild)
class EncounterModal(discord.ui.Modal, title="Log Pirate Encounter"): class EncounterModal(discord.ui.Modal, title="Log Pirate Encounter"):
def __init__(self, cog: "PirateReportCog"): def __init__(self, cog: "PirateReportCog"):
super().__init__() super().__init__()
@ -409,6 +457,7 @@ class EncounterModal(discord.ui.Modal, title="Log Pirate Encounter"):
await self.cog._refresh_pirates_list(interaction.guild) await self.cog._refresh_pirates_list(interaction.guild)
# -------------- Cog: commands + listeners --------------- # -------------- Cog: commands + listeners ---------------
class PirateReportCog(commands.Cog): class PirateReportCog(commands.Cog):
@ -596,7 +645,7 @@ class PirateReportCog(commands.Cog):
guild = channel.guild guild = channel.guild
stamp = _now_utc_str() stamp = _now_utc_str()
# Build new embed + content (keep proof visible) # Build new embed + content (keep proof visible if present)
status_text = f"{'Approved' if approved else 'Rejected'} by <@{payload.user_id}> on {stamp}" status_text = f"{'Approved' if approved else 'Rejected'} by <@{payload.user_id}> on {stamp}"
report_view = { report_view = {
'character_name': report['character_name'], 'character_name': report['character_name'],
@ -613,12 +662,21 @@ class PirateReportCog(commands.Cog):
# For videos, ensure the URL stays in message content so the inline player remains visible. # 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 new_content = report_view['proof_url'] if report_view.get('proof_type') == 'video' else None
# Always (re)attach jump button if we have the ack info
view = None
try:
if report.get('origin_channel_id') and report.get('ack_message_id'):
url = _jump_url(guild.id, report['origin_channel_id'], report['ack_message_id'])
view = ReportJumpView(url)
except Exception:
view = 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, embed=new_embed) await msg.edit(content=new_content, embed=new_embed, view=view)
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))
@ -648,5 +706,6 @@ class PirateReportCog(commands.Cog):
dm.remove('reports', lambda r: r.get('report_id') == msg.id) dm.remove('reports', lambda r: r.get('report_id') == msg.id)
async def setup(bot): async def setup(bot):
await bot.add_cog(PirateReportCog(bot)) await bot.add_cog(PirateReportCog(bot))