Re-enable Report/Delete/Profile context menus, remove Bookmark, fix reports
Re-enabled the "Report to Wormhole", "Delete from Network", and "Wormhole Profile" context menus (previously disabled entirely). Removed "Bookmark (Wormhole)" per request — the prefix `wh bm list`/`clear` commands are untouched, but note this was the only way to *add* a bookmark, so that feature is now list/clear-only for existing entries. Wormhole Profile is now considerably richer: account creation date, total cross-network message count, global-ban status, and per-network karma/ server-count/muted/banned status, sorted busiest-network-first. Fixed both report-creation paths (the "Report to Wormhole" context menu's modal, and `wh report msg`): report_counter was read before the config lock and written after, so concurrent reports could collide on the same id with the counter never advancing. Now computed and saved atomically inside the lock. Also gave both paths full parity — message id, jump link, and a real content preview (the context-menu path previously stored `hash()` of the content, which is randomized per-process in Python 3 and useless for anything; the prefix-command path never even recorded who wrote the message). `wh report view` now surfaces all of it. Also added the "Find Origin" context menu (right-click a message -> find its true source server/channel/author across the network) as a separate, complementary feature to Profile — not a replacement. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
ece196748c
commit
5e41658d7d
3 changed files with 111 additions and 77 deletions
|
|
@ -21,23 +21,41 @@ class ReportCommands(WormholeBase):
|
|||
net_name = await self._net_for_ch(ctx.channel.id)
|
||||
if not net_name:
|
||||
return await ctx.send(embed=err_embed("This channel isn't in a network."))
|
||||
nd = await self._net(net_name)
|
||||
report_id = nd.get("report_counter", 0) + 1
|
||||
report = {
|
||||
"id": report_id,
|
||||
"reporter_id": ctx.author.id,
|
||||
"message_id": message_id,
|
||||
"channel_id": ctx.channel.id,
|
||||
"guild_id": ctx.guild.id if ctx.guild else None,
|
||||
"reason": reason,
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"resolved": False,
|
||||
"resolved_by": None,
|
||||
"resolution": None,
|
||||
}
|
||||
|
||||
# Fetch the message so the report actually records who wrote it and
|
||||
# what it said, instead of just a bare, unverified id.
|
||||
target_message = None
|
||||
try:
|
||||
target_message = await ctx.channel.fetch_message(message_id)
|
||||
except (discord.NotFound, discord.Forbidden, discord.HTTPException):
|
||||
pass
|
||||
|
||||
# report_id must be computed and written inside the same locked
|
||||
# section — reading report_counter beforehand let two concurrent
|
||||
# reports land on the same id and left the counter not advanced.
|
||||
async with self.config.networks() as ns:
|
||||
ns[net_name].setdefault("reports", []).append(report)
|
||||
ns[net_name]["report_counter"] = report_id
|
||||
nd = ns.get(net_name)
|
||||
if not nd:
|
||||
return await ctx.send(embed=err_embed(f"Network `{net_name}` not found."))
|
||||
report_id = nd.get("report_counter", 0) + 1
|
||||
report = {
|
||||
"id": report_id,
|
||||
"reporter_id": ctx.author.id,
|
||||
"author_id": target_message.author.id if target_message else None,
|
||||
"message_id": message_id,
|
||||
"channel_id": ctx.channel.id,
|
||||
"guild_id": ctx.guild.id if ctx.guild else None,
|
||||
"content_preview": truncate(target_message.content or "*[no text]*", 300) if target_message else None,
|
||||
"jump_url": target_message.jump_url if target_message else None,
|
||||
"reason": reason,
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"resolved": False,
|
||||
"resolved_by": None,
|
||||
"resolution": None,
|
||||
}
|
||||
nd.setdefault("reports", []).append(report)
|
||||
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"
|
||||
|
|
@ -87,11 +105,19 @@ class ReportCommands(WormholeBase):
|
|||
reporter = self.bot.get_user(report.get("reporter_id"))
|
||||
em = discord.Embed(title=f"🚩 Report #{report_id}", colour=COLOUR_INFO)
|
||||
em.add_field(name="Reporter", value=str(reporter or report.get("reporter_id")), inline=True)
|
||||
if report.get("author_id"):
|
||||
author = self.bot.get_user(report["author_id"])
|
||||
em.add_field(name="Message Author", value=str(author or report["author_id"]), inline=True)
|
||||
em.add_field(name="Status", value="Resolved" if report.get("resolved") else "Open", inline=True)
|
||||
em.add_field(name="Reason", value=report.get("reason", "N/A"), inline=False)
|
||||
if report.get("content_preview"):
|
||||
em.add_field(name="Message Content", value=truncate(report["content_preview"], 1024), inline=False)
|
||||
em.add_field(name="Timestamp", value=report.get("timestamp", "?")[:16], inline=True)
|
||||
if report.get("message_id"):
|
||||
em.add_field(name="Message ID", value=str(report["message_id"]), inline=True)
|
||||
id_value = f"`{report['message_id']}`"
|
||||
if report.get("jump_url"):
|
||||
id_value += f" — [Jump]({report['jump_url']})"
|
||||
em.add_field(name="Message ID", value=id_value, inline=True)
|
||||
if report.get("resolved_by"):
|
||||
resolver = self.bot.get_user(report["resolved_by"])
|
||||
em.add_field(name="Resolved By", value=str(resolver or report["resolved_by"]), inline=True)
|
||||
|
|
|
|||
|
|
@ -158,8 +158,7 @@ class WormholeV4(
|
|||
self._bg_tasks.append(asyncio.create_task(self._scheduled_msg_loop()))
|
||||
self._bg_tasks.append(asyncio.create_task(self._health_check_loop()))
|
||||
self._bg_tasks.append(asyncio.create_task(self._poll_expiry_loop()))
|
||||
# Context menus disabled - eliminates registration complexity
|
||||
# self._register_context_menus()
|
||||
self._register_context_menus()
|
||||
self._register_find_origin_context_menu()
|
||||
self._ready.set()
|
||||
log.info("Wormhole v%s ready — %d networks loaded", self.__version__, len(nets))
|
||||
|
|
@ -173,8 +172,7 @@ class WormholeV4(
|
|||
decorator on the mixin classes — do NOT manually register duplicates.
|
||||
"""
|
||||
# Remove any existing context menus first (idempotent registration)
|
||||
menu_names = ("Report to Wormhole", "Bookmark (Wormhole)",
|
||||
"Delete from Network", "Wormhole Profile")
|
||||
menu_names = ("Report to Wormhole", "Delete from Network", "Wormhole Profile")
|
||||
for name in menu_names:
|
||||
try:
|
||||
self.bot.tree.remove_command(name, type=discord.AppCommandType.message)
|
||||
|
|
@ -183,11 +181,9 @@ class WormholeV4(
|
|||
|
||||
# Register context menus
|
||||
ctx_report = app_commands.ContextMenu(name="Report to Wormhole", callback=self._ctx_report_message)
|
||||
ctx_bookmark = app_commands.ContextMenu(name="Bookmark (Wormhole)", callback=self._ctx_bookmark_message)
|
||||
ctx_delete = app_commands.ContextMenu(name="Delete from Network", callback=self._ctx_delete_message)
|
||||
ctx_profile = app_commands.ContextMenu(name="Wormhole Profile", callback=self._ctx_view_profile)
|
||||
self.bot.tree.add_command(ctx_report)
|
||||
self.bot.tree.add_command(ctx_bookmark)
|
||||
self.bot.tree.add_command(ctx_delete)
|
||||
self.bot.tree.add_command(ctx_profile)
|
||||
|
||||
|
|
@ -195,8 +191,7 @@ class WormholeV4(
|
|||
for task in self._bg_tasks:
|
||||
task.cancel()
|
||||
# Remove context menus (must specify type=discord.AppCommandType.message)
|
||||
for cmd_name in ("Report to Wormhole", "Bookmark (Wormhole)",
|
||||
"Delete from Network", "Wormhole Profile", "Find Origin"):
|
||||
for cmd_name in ("Report to Wormhole", "Delete from Network", "Wormhole Profile", "Find Origin"):
|
||||
self.bot.tree.remove_command(cmd_name, type=discord.AppCommandType.message)
|
||||
|
||||
# ── Background task loops ──────────────────────────────────────────────
|
||||
|
|
@ -662,25 +657,6 @@ class WormholeV4(
|
|||
modal = ReportModal(self, net_name, message)
|
||||
await interaction.response.send_modal(modal)
|
||||
|
||||
async def _ctx_bookmark_message(self, interaction: discord.Interaction, message: discord.Message) -> None:
|
||||
uid_str = str(interaction.user.id)
|
||||
bookmark = {
|
||||
"content": truncate(message.content or "*[no text]*", 500),
|
||||
"author": str(message.author),
|
||||
"server": message.guild.name if message.guild else "DM",
|
||||
"channel": message.channel.name if hasattr(message.channel, "name") else "DM",
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"jump_url": message.jump_url,
|
||||
}
|
||||
async with self.config.bookmarks() as bm:
|
||||
bm.setdefault(uid_str, []).append(bookmark)
|
||||
if len(bm[uid_str]) > 50:
|
||||
bm[uid_str] = bm[uid_str][-50:]
|
||||
await interaction.response.send_message(
|
||||
embed=ok_embed("Message bookmarked! Use `wh bm list` to view."),
|
||||
ephemeral=True,
|
||||
)
|
||||
|
||||
async def _ctx_delete_message(self, interaction: discord.Interaction, message: discord.Message) -> None:
|
||||
net_name = await self._net_for_ch(message.channel.id)
|
||||
if not net_name:
|
||||
|
|
@ -716,30 +692,59 @@ class WormholeV4(
|
|||
|
||||
async def _ctx_view_profile(self, interaction: discord.Interaction, user: discord.User) -> None:
|
||||
nets = await self.config.networks()
|
||||
global_banned = await self.config.global_banned_users()
|
||||
|
||||
profiles_found = []
|
||||
total_messages = 0
|
||||
for name, nd in nets.items():
|
||||
p = nd.get("user_profiles", {}).get(str(user.id))
|
||||
if p:
|
||||
role = get_role(nd, user.id)
|
||||
profiles_found.append((name, p, role))
|
||||
if not p:
|
||||
continue
|
||||
role = get_role(nd, user.id)
|
||||
karma = nd.get("karma_scores", {}).get(str(user.id), 0)
|
||||
is_banned = user.id in nd.get("banned_users", [])
|
||||
is_muted = user.id in nd.get("muted_users", [])
|
||||
profiles_found.append((name, p, role, karma, is_banned, is_muted))
|
||||
total_messages += p.get("message_count", 0)
|
||||
|
||||
if not profiles_found:
|
||||
await interaction.response.send_message("No wormhole profile found for this user.", ephemeral=True)
|
||||
return
|
||||
em = discord.Embed(
|
||||
title=f"🌀 Wormhole Profile — {user.display_name}",
|
||||
colour=COLOUR_INFO,
|
||||
)
|
||||
|
||||
# Busiest networks first — most relevant at a glance
|
||||
profiles_found.sort(key=lambda t: t[1].get("message_count", 0), reverse=True)
|
||||
|
||||
em = discord.Embed(title=f"🌀 Wormhole Profile — {user.display_name}", colour=COLOUR_INFO)
|
||||
em.set_thumbnail(url=user.display_avatar.url)
|
||||
for name, p, role in profiles_found[:10]:
|
||||
em.add_field(name="User", value=f"{user}\n`{user.id}`", inline=True)
|
||||
em.add_field(name="Account Created", value=discord.utils.format_dt(user.created_at, "R"), inline=True)
|
||||
em.add_field(name="Networks Active In", value=str(len(profiles_found)), inline=True)
|
||||
em.add_field(name="Total Messages (all networks)", value=f"{total_messages:,}", inline=True)
|
||||
|
||||
if user.id in global_banned:
|
||||
em.add_field(name="⚠️ Global Status", value="**Banned from Wormhole network-wide**", inline=False)
|
||||
|
||||
for name, p, role, karma, is_banned, is_muted in profiles_found[:10]:
|
||||
status_flags = []
|
||||
if is_banned:
|
||||
status_flags.append("🚫 Banned")
|
||||
if is_muted:
|
||||
status_flags.append("🔇 Muted")
|
||||
status_str = " · ".join(status_flags) if status_flags else "✅ Active"
|
||||
em.add_field(
|
||||
name=f"Network: {name}",
|
||||
value=(
|
||||
f"Role: **{role_name(role)}**\n"
|
||||
f"Messages: {p.get('message_count', 0):,}\n"
|
||||
f"First seen: {p.get('first_seen', 'Unknown')[:10]}"
|
||||
f"Karma: {karma}\n"
|
||||
f"Servers seen in: {len(p.get('servers', []))}\n"
|
||||
f"First seen: {p.get('first_seen', 'Unknown')[:10]}\n"
|
||||
f"Status: {status_str}"
|
||||
),
|
||||
inline=True,
|
||||
)
|
||||
if len(profiles_found) > 10:
|
||||
em.set_footer(text=f"Showing 10 of {len(profiles_found)} networks (busiest first)")
|
||||
await interaction.response.send_message(embed=em, ephemeral=True)
|
||||
|
||||
async def _ctx_find_origin(self, interaction: discord.Interaction, message: discord.Message) -> None:
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from typing import TYPE_CHECKING
|
|||
|
||||
import discord
|
||||
|
||||
from ..utils import ok_embed, warn_embed
|
||||
from ..utils import ok_embed, truncate, warn_embed
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..core import Wormhole
|
||||
|
|
@ -29,30 +29,33 @@ class ReportModal(discord.ui.Modal, title="Report Message"):
|
|||
self.message = message
|
||||
|
||||
async def on_submit(self, interaction: discord.Interaction) -> None:
|
||||
nets = await self.cog.config.networks()
|
||||
nd = nets.get(self.net_name)
|
||||
if not nd:
|
||||
await interaction.response.send_message("Network not found.", ephemeral=True)
|
||||
return
|
||||
|
||||
report_id = nd.get("report_counter", 0) + 1
|
||||
report = {
|
||||
"id": report_id,
|
||||
"reporter_id": interaction.user.id,
|
||||
"author_id": self.message.author.id,
|
||||
"channel_id": self.message.channel.id,
|
||||
"guild_id": self.message.guild.id if self.message.guild else None,
|
||||
"message_content_hash": str(hash(self.message.content or "")),
|
||||
"reason": self.reason.value,
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"resolved": False,
|
||||
"resolved_by": None,
|
||||
}
|
||||
|
||||
# report_id must be computed and written inside the same locked
|
||||
# section — reading report_counter beforehand let two concurrent
|
||||
# reports land on the same id and left the counter not advanced.
|
||||
async with self.cog.config.networks() as ns:
|
||||
if self.net_name in ns:
|
||||
ns[self.net_name].setdefault("reports", []).append(report)
|
||||
ns[self.net_name]["report_counter"] = report_id
|
||||
nd = ns.get(self.net_name)
|
||||
if not nd:
|
||||
await interaction.response.send_message("Network not found.", ephemeral=True)
|
||||
return
|
||||
|
||||
report_id = nd.get("report_counter", 0) + 1
|
||||
report = {
|
||||
"id": report_id,
|
||||
"reporter_id": interaction.user.id,
|
||||
"author_id": self.message.author.id,
|
||||
"message_id": self.message.id,
|
||||
"channel_id": self.message.channel.id,
|
||||
"guild_id": self.message.guild.id if self.message.guild else None,
|
||||
"content_preview": truncate(self.message.content or "*[no text]*", 300),
|
||||
"jump_url": self.message.jump_url,
|
||||
"reason": self.reason.value,
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"resolved": False,
|
||||
"resolved_by": None,
|
||||
"resolution": None,
|
||||
}
|
||||
nd.setdefault("reports", []).append(report)
|
||||
nd["report_counter"] = report_id
|
||||
|
||||
owner = self.cog.bot.get_user(nd.get("owner_id"))
|
||||
if owner:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue