Reports: add configurable report_channel + action buttons view; notify report channel from modal and command
This commit is contained in:
parent
909b10b7ad
commit
3e928311e2
4 changed files with 160 additions and 171 deletions
|
|
@ -10,6 +10,7 @@ from redbot.core import commands
|
|||
from ..models.permissions import Role, requires_role, has_role
|
||||
from ..utils import ok_embed, err_embed, info_embed, warn_embed, truncate, COLOUR_INFO
|
||||
from ._base import WormholeBase
|
||||
from ..ui.views import ReportActionView
|
||||
|
||||
|
||||
class ReportCommands(WormholeBase):
|
||||
|
|
@ -57,13 +58,13 @@ class ReportCommands(WormholeBase):
|
|||
nd["report_counter"] = report_id
|
||||
|
||||
await ctx.send(embed=ok_embed(f"Report #{report_id} submitted. Staff will review it."), delete_after=10)
|
||||
await self._log(nd, warn_embed(
|
||||
f"🚩 **Report #{report_id}**\n"
|
||||
f"Reporter: {ctx.author} (`{ctx.author.id}`)\n"
|
||||
f"Message ID: `{message_id}`\n"
|
||||
f"Reason: {reason}",
|
||||
title="New Report",
|
||||
))
|
||||
|
||||
# Send to configured report channel (if set) with action buttons
|
||||
try:
|
||||
nd_full = await self._net(net_name)
|
||||
await self._notify_report_channel(nd_full, net_name, report)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@WormholeBase.wh_report.command(name="list")
|
||||
@requires_role(Role.HELPER)
|
||||
|
|
@ -135,26 +136,108 @@ class ReportCommands(WormholeBase):
|
|||
r["resolved"] = True
|
||||
r["resolved_by"] = ctx.author.id
|
||||
r["resolution"] = resolution
|
||||
r["resolved_at"] = datetime.now(timezone.utc).isoformat()
|
||||
break
|
||||
else:
|
||||
return await ctx.send(embed=err_embed(f"Report #{report_id} not found."))
|
||||
await ctx.send(embed=ok_embed(f"Report #{report_id} resolved."))
|
||||
await self._audit(name, "report_resolve", str(ctx.author), str(report_id), resolution)
|
||||
await ctx.send(embed=ok_embed(f"Report #{report_id} marked resolved."))
|
||||
|
||||
@WormholeBase.wh_report.command(name="dismiss")
|
||||
@requires_role(Role.MODERATOR)
|
||||
async def wh_report_dismiss(self, ctx: commands.Context, name: str, report_id: int, *, reason: str = "Dismissed") -> None:
|
||||
"""Dismiss a report."""
|
||||
@WormholeBase.wh_report.command(name="channel")
|
||||
@requires_role(Role.ADMIN)
|
||||
async def wh_report_set_channel(self, ctx: commands.Context, name: str, channel: discord.TextChannel | None = None) -> None:
|
||||
"""Set the channel where reports are posted (for staff). Pass no channel to clear."""
|
||||
nd = await self._net(name)
|
||||
if not nd:
|
||||
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
|
||||
async with self.config.networks() as ns:
|
||||
for r in ns[name].get("reports", []):
|
||||
if r.get("id") == report_id:
|
||||
r["resolved"] = True
|
||||
r["resolved_by"] = ctx.author.id
|
||||
r["resolution"] = f"Dismissed: {reason}"
|
||||
r["resolved_at"] = datetime.now(timezone.utc).isoformat()
|
||||
ns[name]["report_channel"] = channel.id if channel else None
|
||||
await ctx.send(embed=ok_embed(f"Report channel {'set' if channel else 'cleared'} for `{name}`."))
|
||||
|
||||
# Helper used by both commands and the ReportModal to notify staff
|
||||
async def _notify_report_channel(self, nd: dict, net_name: str, report: dict) -> None:
|
||||
ch_id = nd.get("report_channel")
|
||||
em = discord.Embed(
|
||||
title=f"🚩 Report #{report['id']} — {net_name}",
|
||||
description=(
|
||||
f"Reporter: <@{report['reporter_id']}> (`{report['reporter_id']}`)\n"
|
||||
f"Author: <@{report['author_id']}> (`{report.get('author_id')}`)\n"
|
||||
f"Channel: <#{report['channel_id']}>\n"
|
||||
f"Reason: {truncate(report.get('reason', ''), 300)}"
|
||||
),
|
||||
colour=COLOUR_INFO,
|
||||
timestamp=datetime.fromisoformat(report['timestamp']),
|
||||
)
|
||||
if report.get('content_preview'):
|
||||
em.add_field(name="Content Preview", value=truncate(report['content_preview'], 1024), inline=False)
|
||||
if report.get('jump_url'):
|
||||
em.add_field(name="Jump", value=f"[Jump to message]({report['jump_url']})", inline=True)
|
||||
|
||||
# DM owner as a courtesy
|
||||
try:
|
||||
owner = self.bot.get_user(nd.get('owner_id'))
|
||||
if owner:
|
||||
await owner.send(embed=warn_embed(
|
||||
f"**Report #{report['id']}** in `{net_name}`\n"
|
||||
f"Reporter: <@{report['reporter_id']}> (`{report['reporter_id']}`)\n"
|
||||
f"Author: <@{report.get('author_id')}> (`{report.get('author_id')}`)\n"
|
||||
f"Reason: {truncate(report.get('reason',''),200)}",
|
||||
title="🚩 New Report",
|
||||
))
|
||||
except discord.Forbidden:
|
||||
pass
|
||||
|
||||
if not ch_id:
|
||||
return
|
||||
ch = self.bot.get_channel(ch_id)
|
||||
if not ch:
|
||||
return
|
||||
view = ReportActionView(self, net_name, report['id'], jump_url=report.get('jump_url'))
|
||||
try:
|
||||
await ch.send(embed=em, view=view)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Interaction handlers invoked by ReportActionView
|
||||
async def _report_resolve_via_interaction(self, interaction: discord.Interaction, net_name: str, report_id: int) -> None:
|
||||
nd = await self._net(net_name)
|
||||
if not nd:
|
||||
return await interaction.response.send_message("Network not found.", ephemeral=True)
|
||||
if not has_role(nd, interaction.user.id, Role.MODERATOR) and not await self.bot.is_owner(interaction.user):
|
||||
return await interaction.response.send_message("You need Moderator role or higher.", ephemeral=True)
|
||||
# update report
|
||||
async with self.config.networks() as ns:
|
||||
for r in ns[net_name].get('reports', []):
|
||||
if r.get('id') == report_id:
|
||||
r['resolved'] = True
|
||||
r['resolved_by'] = interaction.user.id
|
||||
r['resolution'] = 'Resolved via report button'
|
||||
break
|
||||
else:
|
||||
return await ctx.send(embed=err_embed(f"Report #{report_id} not found."))
|
||||
await ctx.send(embed=ok_embed(f"Report #{report_id} dismissed."))
|
||||
await self._audit(name, "report_dismiss", str(ctx.author), str(report_id), reason)
|
||||
# Edit the original message to indicate resolved
|
||||
try:
|
||||
em = interaction.message.embeds[0] if interaction.message and interaction.message.embeds else None
|
||||
if em:
|
||||
em.set_footer(text=f"Resolved by {interaction.user}")
|
||||
await interaction.message.edit(embed=em, view=None)
|
||||
except Exception:
|
||||
pass
|
||||
await interaction.response.send_message(embed=ok_embed(f"Report #{report_id} marked resolved."), ephemeral=True)
|
||||
|
||||
async def _report_dismiss_via_interaction(self, interaction: discord.Interaction, net_name: str, report_id: int) -> None:
|
||||
nd = await self._net(net_name)
|
||||
if not nd:
|
||||
return await interaction.response.send_message("Network not found.", ephemeral=True)
|
||||
if not has_role(nd, interaction.user.id, Role.MODERATOR) and not await self.bot.is_owner(interaction.user):
|
||||
return await interaction.response.send_message("You need Moderator role or higher.", ephemeral=True)
|
||||
async with self.config.networks() as ns:
|
||||
for r in ns[net_name].get('reports', []):
|
||||
if r.get('id') == report_id:
|
||||
r['resolved'] = True
|
||||
r['resolved_by'] = interaction.user.id
|
||||
r['resolution'] = 'Dismissed'
|
||||
break
|
||||
try:
|
||||
em = interaction.message.embeds[0] if interaction.message and interaction.message.embeds else None
|
||||
if em:
|
||||
em.colour = 0x2ECC71
|
||||
em.set_footer(text=f"Dismissed by {interaction.user}")
|
||||
await interaction.message.edit(embed=em, view=None)
|
||||
except Exception:
|
||||
pass
|
||||
await interaction.response.send_message(embed=ok_embed(f"Report #{report_id} dismissed."), ephemeral=True)
|
||||
|
|
|
|||
|
|
@ -54,11 +54,12 @@ DEFAULT_NETWORK: dict = {
|
|||
|
||||
# ── Logging ────────────────────────────────────────────────────────────
|
||||
"log_channel": None,
|
||||
"report_channel": None,
|
||||
|
||||
# ── Server nicknames ───────────────────────────────────────────────────
|
||||
"server_nicknames": {},
|
||||
|
||||
# ── Stats ──────────────────────────────────────────────────────────────
|
||||
# ── Stats ─────────────────────────────────────────────────────────────
|
||||
"total_messages": 0,
|
||||
"created_at": None,
|
||||
|
||||
|
|
@ -88,148 +89,4 @@ DEFAULT_NETWORK: dict = {
|
|||
# ── Invites ────────────────────────────────────────────────────────────
|
||||
"invites": {},
|
||||
"vanity_invite": None,
|
||||
|
||||
# ── Portal / welcome ───────────────────────────────────────────────────
|
||||
"portal_messages": {},
|
||||
"welcome_message": "",
|
||||
|
||||
# ── Mention control (legacy) ───────────────────────────────────────────
|
||||
"mention_control": {
|
||||
"strip_everyone": True,
|
||||
"strip_role_mentions": False,
|
||||
"strip_user_mentions": False,
|
||||
},
|
||||
|
||||
# ── User profiles ──────────────────────────────────────────────────────
|
||||
"user_profiles": {},
|
||||
"blackout_schedules": [],
|
||||
|
||||
# ── DM relay ───────────────────────────────────────────────────────────
|
||||
"dm_enabled": False,
|
||||
"dm_subscribers": [],
|
||||
"dm_relay_mode": "embed", # embed | compact | plain
|
||||
|
||||
# ── Starboard ──────────────────────────────────────────────────────────
|
||||
"starboard_enabled": False,
|
||||
"starboard_channel": None,
|
||||
"starboard_threshold": 3,
|
||||
"starred_messages": {},
|
||||
|
||||
# ── Audit log ──────────────────────────────────────────────────────────
|
||||
"audit_log": [],
|
||||
|
||||
# ── Attachment filters ─────────────────────────────────────────────────
|
||||
"blocked_extensions": [],
|
||||
"max_filesize": None,
|
||||
|
||||
# ── Karma ──────────────────────────────────────────────────────────────
|
||||
"karma_enabled": False,
|
||||
"karma_emoji": "👍",
|
||||
"karma_scores": {},
|
||||
|
||||
# ── MOTD / Rules ───────────────────────────────────────────────────────
|
||||
"motd": "",
|
||||
"rules": "",
|
||||
|
||||
# ── Relay delay ────────────────────────────────────────────────────────
|
||||
"relay_delay": 0,
|
||||
|
||||
# ── Highlights ─────────────────────────────────────────────────────────
|
||||
"highlights": {},
|
||||
|
||||
# ── Network roles (legacy, unused) ─────────────────────────────────────
|
||||
"roles": {},
|
||||
|
||||
# ── Per-channel overrides ──────────────────────────────────────────────
|
||||
"channel_overrides": {},
|
||||
|
||||
# ── Scheduled messages ─────────────────────────────────────────────────
|
||||
"scheduled_messages": [],
|
||||
|
||||
# ── Slowmode ───────────────────────────────────────────────────────────
|
||||
"slowmode": 0,
|
||||
|
||||
# ── Discovery ──────────────────────────────────────────────────────────
|
||||
"public": False,
|
||||
"tags": [],
|
||||
|
||||
# ── Typing indicator relay ─────────────────────────────────────────────
|
||||
"sync_typing": False,
|
||||
|
||||
# ── Anonymous mode ─────────────────────────────────────────────────────
|
||||
"anonymous": False,
|
||||
"anon_salt": "",
|
||||
|
||||
# ── Mirror channels ────────────────────────────────────────────────────
|
||||
"mirror_channels": [],
|
||||
|
||||
# ── Ephemeral messages ─────────────────────────────────────────────────
|
||||
"ephemeral_delay": 0,
|
||||
|
||||
# ── Auto-responses ─────────────────────────────────────────────────────
|
||||
"auto_responses": {},
|
||||
|
||||
# ── Media-only mode ────────────────────────────────────────────────────
|
||||
"media_only": False,
|
||||
|
||||
# ── Analytics ──────────────────────────────────────────────────────────
|
||||
"analytics": {
|
||||
"hourly": {},
|
||||
"top_users": {},
|
||||
},
|
||||
|
||||
# ── Health ─────────────────────────────────────────────────────────────
|
||||
"last_health_check": None,
|
||||
"unhealthy_channels": [],
|
||||
|
||||
# ── Polls ──────────────────────────────────────────────────────────────
|
||||
"active_polls": {},
|
||||
|
||||
# ── AFK system ─────────────────────────────────────────────────────────
|
||||
"afk_users": {},
|
||||
|
||||
# ── Personal ignore list ───────────────────────────────────────────────
|
||||
"user_ignores": {},
|
||||
|
||||
# ── User colours ───────────────────────────────────────────────────────
|
||||
"user_colours": {},
|
||||
|
||||
# ── Quiet hours ────────────────────────────────────────────────────────
|
||||
"quiet_hours": {},
|
||||
|
||||
# ── Network bridging ───────────────────────────────────────────────────
|
||||
"bridge_from": [],
|
||||
"bridge_to": [],
|
||||
|
||||
# ── Granular mention policy ────────────────────────────────────────────
|
||||
"mention_policy": {
|
||||
"allow_user_mentions": False,
|
||||
"allow_role_mentions": False,
|
||||
"allow_everyone": False,
|
||||
"allow_here": False,
|
||||
},
|
||||
"server_mention_overrides": {},
|
||||
"mention_exempt_users": [],
|
||||
"mention_optout_users": [],
|
||||
|
||||
# ── ToS acceptance gate ────────────────────────────────────────────────
|
||||
"rules_required": False,
|
||||
"rules_text": "",
|
||||
"rules_accepted": {},
|
||||
|
||||
# ── Report system ──────────────────────────────────────────────────────
|
||||
"reports": [],
|
||||
"report_counter": 0,
|
||||
}
|
||||
|
||||
|
||||
DEFAULT_GLOBAL: dict = {
|
||||
"networks": {},
|
||||
"max_networks_per_user": 10,
|
||||
"global_banned_users": [],
|
||||
"global_banned_servers": [],
|
||||
"bookmarks": {},
|
||||
}
|
||||
|
||||
|
||||
MAP_LIMIT: int = 2_000
|
||||
|
|
|
|||
|
|
@ -72,6 +72,13 @@ class ReportModal(discord.ui.Modal, title="Report Message"):
|
|||
except discord.Forbidden:
|
||||
pass
|
||||
|
||||
# Notify configured report channel with action buttons
|
||||
try:
|
||||
nd_full = await self.cog._net(self.net_name)
|
||||
await self.cog._notify_report_channel(nd_full, self.net_name, report)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
await interaction.response.send_message(
|
||||
embed=ok_embed(f"Report #{report_id} submitted. Staff will review it."),
|
||||
ephemeral=True,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import discord
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def reply_jump_view(url: str) -> discord.ui.View:
|
||||
|
|
@ -14,3 +15,44 @@ def reply_jump_view(url: str) -> discord.ui.View:
|
|||
url=url,
|
||||
))
|
||||
return view
|
||||
|
||||
|
||||
class _ResolveButton(discord.ui.Button):
|
||||
def __init__(self, cog, net_name: str, report_id: int):
|
||||
super().__init__(style=discord.ButtonStyle.primary, label="Resolve")
|
||||
self.cog = cog
|
||||
self.net_name = net_name
|
||||
self.report_id = report_id
|
||||
|
||||
async def callback(self, interaction: discord.Interaction) -> None: # type: ignore[override]
|
||||
await self.cog._report_resolve_via_interaction(interaction, self.net_name, self.report_id)
|
||||
|
||||
|
||||
class _DismissButton(discord.ui.Button):
|
||||
def __init__(self, cog, net_name: str, report_id: int):
|
||||
super().__init__(style=discord.ButtonStyle.secondary, label="Dismiss")
|
||||
self.cog = cog
|
||||
self.net_name = net_name
|
||||
self.report_id = report_id
|
||||
|
||||
async def callback(self, interaction: discord.Interaction) -> None: # type: ignore[override]
|
||||
await self.cog._report_dismiss_via_interaction(interaction, self.net_name, self.report_id)
|
||||
|
||||
|
||||
class ReportActionView(discord.ui.View):
|
||||
"""View attached to report notifications with action buttons.
|
||||
|
||||
The view delegates actual action handling back to the Wormhole cog so the
|
||||
existing permission and state logic is reused.
|
||||
"""
|
||||
|
||||
def __init__(self, cog, net_name: str, report_id: int, jump_url: Optional[str] = None, *, timeout: Optional[float] = None):
|
||||
super().__init__(timeout=timeout)
|
||||
self.cog = cog
|
||||
self.net_name = net_name
|
||||
self.report_id = report_id
|
||||
if jump_url:
|
||||
self.add_item(discord.ui.Button(style=discord.ButtonStyle.link, label="Jump to message", url=jump_url))
|
||||
# Action buttons
|
||||
self.add_item(_ResolveButton(cog, net_name, report_id))
|
||||
self.add_item(_DismissButton(cog, net_name, report_id))
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue