From 4f0e000c9355494b16a3b9c3b9de5cc8db07a842 Mon Sep 17 00:00:00 2001 From: Franz Rolfsvaag Date: Mon, 11 Aug 2025 09:26:56 +0200 Subject: [PATCH] 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 --- bot.py | 2 +- modules/pirate_report/pirate_report.py | 91 +++++++++++++++++++++----- 2 files changed, 76 insertions(+), 17 deletions(-) diff --git a/bot.py b/bot.py index c56026d..df951a1 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.7.a2" +VERSION = "0.3.9.7.a3" # ---------- Env loading ---------- diff --git a/modules/pirate_report/pirate_report.py b/modules/pirate_report/pirate_report.py index 4122307..df49929 100644 --- a/modules/pirate_report/pirate_report.py +++ b/modules/pirate_report/pirate_report.py @@ -27,12 +27,15 @@ 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())) + def _now_utc_str() -> str: return datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC') + def _parse_bool(s: str) -> bool: v = s.strip().lower() if v in ('y', 'yes', 'true', 't', '1'): @@ -41,6 +44,7 @@ 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). @@ -68,24 +72,53 @@ def _classify_discord_media(url: str): except Exception: 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): 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) + + proof_url = (report_dict.get('proof_url') or "").strip() + proof_type = (report_dict.get('proof_type') or "").strip() + 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', '')) + + # Show proof inline if it's an image; for videos we'll keep the URL in message content. + 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 - e.set_footer(text="Proof is included in this message.") + e.set_footer(text="Attach proof when possible to speed up moderation.") 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 ---------------------- class ReportModal(discord.ui.Modal, title="Submit Pirate Report"): @@ -106,9 +139,9 @@ class ReportModal(discord.ui.Modal, title="Submit Pirate Report"): required=True ) self.proof_url = discord.ui.TextInput( - label="Proof (Discord media URL)", - placeholder="Direct link to image/video from Discord (CDN)", - required=True, + label="Proof (Discord media URL — optional)", + placeholder="Direct Discord CDN link to image/video (highly encouraged, but optional)", + required=False, max_length=300 ) @@ -122,7 +155,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() + proof_raw = (self.proof_url.value or "").strip() if not _acct_ok(acct): return await interaction.response.send_message( @@ -130,9 +163,13 @@ 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) + proof_type = "" + proof_val = "" + 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 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)) 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) if not mod_ch: 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, } + # 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: 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) + mod_msg = await mod_ch.send(content=content, embed=embed, view=view) await mod_msg.add_reaction(CHECK) await mod_msg.add_reaction(CROSS) 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) + class EditPirateModal(discord.ui.Modal, title="Edit Pirate Entry"): def __init__(self, cog: "PirateReportCog"): 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 self.cog._refresh_pirates_list(interaction.guild) + class EncounterModal(discord.ui.Modal, title="Log Pirate Encounter"): def __init__(self, cog: "PirateReportCog"): super().__init__() @@ -409,6 +457,7 @@ class EncounterModal(discord.ui.Modal, title="Log Pirate Encounter"): await self.cog._refresh_pirates_list(interaction.guild) + # -------------- Cog: commands + listeners --------------- class PirateReportCog(commands.Cog): @@ -596,7 +645,7 @@ class PirateReportCog(commands.Cog): guild = channel.guild 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}" report_view = { '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. 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: await msg.clear_reactions() except Exception as e: print("[pirate_report] clear reactions failed:", repr(e)) 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: 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) + async def setup(bot): - await bot.add_cog(PirateReportCog(bot)) \ No newline at end of file + await bot.add_cog(PirateReportCog(bot))