From 424403cf4fda441890f0a847050481f26525f87b Mon Sep 17 00:00:00 2001 From: Viktor Date: Thu, 14 May 2026 15:40:36 +0000 Subject: [PATCH] feat(wormhole): relay unknown custom emojis as 64px image attachments Custom emojis the bot can't see (bot.get_emoji returns None) are now: 1. Replaced with :name: in message text 2. Downloaded from Discord CDN at 64px 3. Attached as small image files to the relay message Known emojis (from any server the bot is in) keep the <:name:id> syntax and render normally. Up to 5 emoji images per message. Works in webhook and compact relay modes. Bumps to v3.4.4. --- wormhole/wormhole.py | 59 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/wormhole/wormhole.py b/wormhole/wormhole.py index e690124..281efb9 100644 --- a/wormhole/wormhole.py +++ b/wormhole/wormhole.py @@ -33,6 +33,9 @@ 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. +Phase 5.4: Foreign emoji relay — custom emojis unknown to the bot are + replaced with :name: and their 64 px image is attached as a + file so users still see the emoji visually. """ from __future__ import annotations @@ -286,6 +289,46 @@ def _reply_jump_view(url: str) -> discord.ui.View: )) return view +_CUSTOM_EMOJI_RE = re.compile(r'<(a?):(\w+):(\d+)>') +_EMOJI_IMAGE_LIMIT = 5 # max emoji images to attach per message + + +async def _resolve_foreign_emojis( + bot, content: str +) -> tuple[str, list[tuple[str, bytes]]]: + """Replace custom emojis the bot can't see with :name: and fetch images. + + Returns (new_content, [(filename, image_bytes), ...]). + """ + unknown: list[tuple[str, int, str]] = [] # (name, id, ext) + + def _check(m: re.Match) -> str: + animated, name, eid = m.group(1), m.group(2), int(m.group(3)) + if bot.get_emoji(eid): + return m.group(0) # known emoji — keep original syntax + ext = "gif" if animated else "png" + unknown.append((name, eid, ext)) + return f":{name}:" + + new_content = _CUSTOM_EMOJI_RE.sub(_check, content) + + # Download images from Discord CDN (already public URLs, no auth needed) + emoji_data: list[tuple[str, bytes]] = [] + if unknown: + import aiohttp + async with aiohttp.ClientSession() as session: + for name, eid, ext in unknown[:_EMOJI_IMAGE_LIMIT]: + try: + url = f"https://cdn.discordapp.com/emojis/{eid}.{ext}?size=64" + async with session.get(url, timeout=aiohttp.ClientTimeout(total=5)) as resp: + if resp.status == 200: + emoji_data.append((f"{name}.{ext}", await resp.read())) + except Exception: + pass # best-effort — skip if CDN unreachable + + return new_content, emoji_data + + _AUDIT_LIMIT = 500 @@ -394,7 +437,7 @@ class _ReportModal(discord.ui.Modal, title="Report Message"): class Wormhole(commands.Cog): """The ultimate cross-server relay: networks, DMs, starboard, auto-mod, invites, portals & more.""" - __version__ = "3.4.3" + __version__ = "3.4.4" def __init__(self, bot: Red): self.bot = bot @@ -4268,6 +4311,14 @@ class Wormhole(commands.Cog): if nd.get("forward_embeds") and message.embeds: extra_embeds = [e for e in message.embeds if e.type == "rich"] + # ── Resolve foreign emojis ───────────────────────────────────────── + # If any custom emojis in the content are unknown to the bot, replace + # them with :name: and download the image from Discord CDN so it can + # be attached as a small file. + emoji_file_data: list[tuple[str, bytes]] = [] + if content: + content, emoji_file_data = await _resolve_foreign_emojis(self.bot, content) + # ── Relay to channels ────────────────────────────────────────────── mapping: Dict[int, int] = {} # Build target list: own channels + bridge_to networks' channels @@ -4297,6 +4348,8 @@ class Wormhole(commands.Cog): for a in message.attachments: try: files.append(await a.to_file()) except: pass + for fname, fdata in emoji_file_data: + files.append(discord.File(io.BytesIO(fdata), filename=fname)) send_content = content if content else None if not send_content and not files and not extra_embeds: send_content = "*[empty message]*" @@ -4317,6 +4370,8 @@ class Wormhole(commands.Cog): for a in message.attachments: try: files2.append(await a.to_file()) except: pass + for fname, fdata in emoji_file_data: + files2.append(discord.File(io.BytesIO(fdata), filename=fname)) send_content = content if content else None if not send_content and not files2 and not extra_embeds: send_content = "*[empty message]*" @@ -4351,6 +4406,8 @@ class Wormhole(commands.Cog): for a in message.attachments: try: files.append(await a.to_file()) except: pass + for fname, fdata in emoji_file_data: + files.append(discord.File(io.BytesIO(fdata), filename=fname)) sent_msg = await ch.send(content=compact_format(g, display, content), files=files or None, **_view_kw) mapping[ch_id] = sent_msg.id