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:
parent
b6980794d7
commit
4f0e000c93
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.7.a2"
|
VERSION = "0.3.9.7.a3"
|
||||||
|
|
||||||
# ---------- Env loading ----------
|
# ---------- Env loading ----------
|
||||||
|
|
||||||
|
@ -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))
|
Loading…
Reference in New Issue
Block a user