feat(wormhole): add 'Jump to replied message' button on relayed replies

When a reply is relayed across servers, each copy now includes a URL
button underneath the quoted reply text:

- If the referenced message has a relayed copy in the target channel,
  the button links to that local copy (so users stay in their own server).
- Otherwise, it falls back to the original message URL.

Works across all relay modes (webhook, embed, compact) and leverages
the existing msg_map to resolve per-channel message IDs.

Bumps version to 3.4.3.
This commit is contained in:
Viktor 2026-05-14 15:13:20 +00:00
parent 0413fbcb66
commit 13b28d3800

View file

@ -29,6 +29,10 @@ Phase 5.2: Fix empty-prefix bug — get_context() with an empty-string prefix
matched every message, silently blocking all relay traffic. Now matched every message, silently blocking all relay traffic. Now
requires ctx.prefix to be non-empty before filtering. Re-added requires ctx.prefix to be non-empty before filtering. Re-added
empty-prefix warning to wh debug. empty-prefix warning to wh debug.
Phase 5.3: Enhanced reply buttons — relayed replies now include a
"Jump to replied message" URL button that links to the local
copy of the referenced message (if available in the target
server) or falls back to the original message URL.
""" """
from __future__ import annotations from __future__ import annotations
@ -270,6 +274,18 @@ _DEFAULT_GLOBAL = {
} }
_MAP_LIMIT = 2_000 _MAP_LIMIT = 2_000
def _reply_jump_view(url: str) -> discord.ui.View:
"""Build a View with a single URL button that jumps to the replied message."""
view = discord.ui.View()
view.add_item(discord.ui.Button(
style=discord.ButtonStyle.link,
label="↩ Jump to replied message",
url=url,
))
return view
_AUDIT_LIMIT = 500 _AUDIT_LIMIT = 500
@ -378,7 +394,7 @@ class _ReportModal(discord.ui.Modal, title="Report Message"):
class Wormhole(commands.Cog): class Wormhole(commands.Cog):
"""The ultimate cross-server relay: networks, DMs, starboard, auto-mod, invites, portals & more.""" """The ultimate cross-server relay: networks, DMs, starboard, auto-mod, invites, portals & more."""
__version__ = "3.4.2" __version__ = "3.4.3"
def __init__(self, bot: Red): def __init__(self, bot: Red):
self.bot = bot self.bot = bot
@ -4203,12 +4219,31 @@ class Wormhole(commands.Cog):
user_colour = nd.get("user_colours", {}).get(str(message.author.id)) user_colour = nd.get("user_colours", {}).get(str(message.author.id))
# Reply context # Reply context
reply_jump_urls: Dict[int, str] = {} # {target_ch_id: jump_url}
reply_fallback_url: Optional[str] = None
if nd.get("sync_replies") and message.reference and message.reference.message_id: if nd.get("sync_replies") and message.reference and message.reference.message_id:
try: try:
ref = message.reference.cached_message or await message.channel.fetch_message(message.reference.message_id) ref = message.reference.cached_message or await message.channel.fetch_message(message.reference.message_id)
ref_name = self._anon_name(nd, ref.author.id) if is_anon else ref.author.display_name ref_name = self._anon_name(nd, ref.author.id) if is_anon else ref.author.display_name
preview = truncate(ref.content, 100) if ref.content else "*[attachment]*" preview = truncate(ref.content, 100) if ref.content else "*[attachment]*"
content = f"> **↩ {ref_name}:** {preview}\n{content}" content = f"> **↩ {ref_name}:** {preview}\n{content}"
# Build per-channel "Jump to replied message" URLs
if ref.guild:
reply_fallback_url = f"https://discord.com/channels/{ref.guild.id}/{ref.channel.id}/{ref.id}"
# Look up relayed copies of the referenced message
orig_ref_id = self.msg_map.get_original(net_name, ref.id)
ref_relayed = (
self.msg_map.forward.get(net_name, {}).get(orig_ref_id, {})
if orig_ref_id
else self.msg_map.forward.get(net_name, {}).get(ref.id, {})
)
for cid, mid in ref_relayed.items():
target_ch = self.bot.get_channel(cid)
if target_ch and target_ch.guild:
reply_jump_urls[cid] = f"https://discord.com/channels/{target_ch.guild.id}/{cid}/{mid}"
# Include the channel where the ref message lives
reply_jump_urls.setdefault(ref.channel.id, reply_fallback_url)
except: content = f"> ↩ *[reply]*\n{content}" except: content = f"> ↩ *[reply]*\n{content}"
# Stickers # Stickers
@ -4235,6 +4270,11 @@ class Wormhole(commands.Cog):
ch = self.bot.get_channel(ch_id) ch = self.bot.get_channel(ch_id)
if not ch: continue if not ch: continue
ch_mode = self._get_override(nd, ch_id, "relay_mode") or relay_mode ch_mode = self._get_override(nd, ch_id, "relay_mode") or relay_mode
# Build reply-jump view for this channel (if replying)
jump_url = reply_jump_urls.get(ch_id) or reply_fallback_url
reply_view = _reply_jump_view(jump_url) if jump_url else None
try: try:
sent_msg = None sent_msg = None
if ch_mode == "webhook": if ch_mode == "webhook":
@ -4253,6 +4293,7 @@ class Wormhole(commands.Cog):
avatar_url=avatar, avatar_url=avatar,
files=files or discord.utils.MISSING, files=files or discord.utils.MISSING,
embeds=extra_embeds or discord.utils.MISSING, embeds=extra_embeds or discord.utils.MISSING,
view=reply_view,
wait=True, wait=True,
) )
mapping[ch_id] = sent_msg.id mapping[ch_id] = sent_msg.id
@ -4272,22 +4313,23 @@ class Wormhole(commands.Cog):
avatar_url=avatar, avatar_url=avatar,
files=files2 or discord.utils.MISSING, files=files2 or discord.utils.MISSING,
embeds=extra_embeds or discord.utils.MISSING, embeds=extra_embeds or discord.utils.MISSING,
view=reply_view,
wait=True, wait=True,
) )
mapping[ch_id] = sent_msg.id mapping[ch_id] = sent_msg.id
except Exception: except Exception:
log.warning("Webhook retry failed for %s, falling back to embed", ch_id) log.warning("Webhook retry failed for %s, falling back to embed", ch_id)
em = build_relay_embed(message, nick, nd.get("colour")) em = build_relay_embed(message, nick, nd.get("colour"))
sent_msg = await ch.send(embeds=[em] + extra_embeds[:9]) sent_msg = await ch.send(embeds=[em] + extra_embeds[:9], view=reply_view)
mapping[ch_id] = sent_msg.id mapping[ch_id] = sent_msg.id
except discord.Forbidden: except discord.Forbidden:
log.warning("No webhook perms in %s, falling back to embed", ch_id) log.warning("No webhook perms in %s, falling back to embed", ch_id)
em = build_relay_embed(message, nick, nd.get("colour")) em = build_relay_embed(message, nick, nd.get("colour"))
sent_msg = await ch.send(embeds=[em] + extra_embeds[:9]) sent_msg = await ch.send(embeds=[em] + extra_embeds[:9], view=reply_view)
mapping[ch_id] = sent_msg.id mapping[ch_id] = sent_msg.id
elif ch_mode == "embed": elif ch_mode == "embed":
em = build_relay_embed(message, nick, user_colour or nd.get("colour")) em = build_relay_embed(message, nick, user_colour or nd.get("colour"))
sent_msg = await ch.send(embeds=[em] + extra_embeds[:9]) sent_msg = await ch.send(embeds=[em] + extra_embeds[:9], view=reply_view)
mapping[ch_id] = sent_msg.id mapping[ch_id] = sent_msg.id
elif ch_mode == "compact": elif ch_mode == "compact":
g = nick or message.guild.name g = nick or message.guild.name
@ -4296,7 +4338,7 @@ class Wormhole(commands.Cog):
for a in message.attachments: for a in message.attachments:
try: files.append(await a.to_file()) try: files.append(await a.to_file())
except: pass except: pass
sent_msg = await ch.send(content=compact_format(g, display, content), files=files or None) sent_msg = await ch.send(content=compact_format(g, display, content), files=files or None, view=reply_view)
mapping[ch_id] = sent_msg.id mapping[ch_id] = sent_msg.id
# Ephemeral deletion # Ephemeral deletion