BREAKING: Complete cog restructure from monolithic wormhole.py into modular architecture. ## Architecture - models/permissions.py — Role IntEnum (MEMBER→OWNER), requires_role() decorator - models/config.py — DEFAULT_NETWORK/DEFAULT_GLOBAL with 80+ config keys - models/message_map.py — MessageMap class (extracted from _MessageMap) - services/emoji.py — Foreign emoji resolution and relay - ui/views.py — Reply jump button view - ui/modals.py — Report modal - core.py — Main Wormhole class, helpers, background loops, context menus - commands/ — 13 command modules as mixin classes - listeners/ — relay pipeline, edit/delete/reaction sync, typing/pin sync ## Staff System (new) Per-network hierarchical roles replacing flat owner/staff: Bot Owner (Red built-in) — global override └─ Network Owner → Admin → Moderator → Helper → Member Each role assigned per-network. Higher roles can assign lower. Legacy staff_ids auto-migrated to ADMIN role. ## Enhanced Moderation - Server-level ban/mute (not just user-level) - Allowlist mode (only whitelisted servers can send) - Mod edit and nuke (delete N messages by user) - Anti-raid detector with auto-freeze - Expanded automod: anti-zalgo, anti-emote-spam, anti-newline-spam ## Commands (100+) network, settings, staff, moderation, filters, social, dm, mentions, tos, reports, bridge, advanced (polls/afk/auto-responses/scheduled/ analytics/quiet-hours/colours/invites/announcements/blackout/portal), debug (health/trace/testsend/relaydebug/backup/restore/version) ## Files - 30 Python files, 5,274 lines total (was 4,782 in monolith) - Original wormhole.py preserved for reference - utils.py unchanged (all helpers reused)
78 lines
2.5 KiB
Python
78 lines
2.5 KiB
Python
"""
|
||
Foreign emoji relay — resolve custom emojis the bot can't see.
|
||
|
||
Unknown emojis are replaced with ``:name:`` in text and their images
|
||
are downloaded from the CDN, then shown as embed thumbnails (~80×80 px)
|
||
via the ``attachment://`` scheme.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import io
|
||
import re
|
||
from typing import TYPE_CHECKING
|
||
|
||
import discord
|
||
|
||
if TYPE_CHECKING:
|
||
from redbot.core.bot import Red
|
||
|
||
_CUSTOM_EMOJI_RE = re.compile(r"<(a?):(\w+):(\d+)>")
|
||
_EMOJI_IMAGE_LIMIT = 5 # max emoji thumbnails per message
|
||
|
||
|
||
async def resolve_foreign_emojis(
|
||
bot: Red, content: str
|
||
) -> tuple[str, list[tuple[str, bytes]]]:
|
||
"""Replace unknown custom emojis with ``:name:`` and fetch small images.
|
||
|
||
Returns ``(new_content, [(filename, image_bytes), ...])``.
|
||
"""
|
||
unknown: list[tuple[str, int, str]] = []
|
||
|
||
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)
|
||
ext = "gif" if animated else "png"
|
||
unknown.append((name, eid, ext))
|
||
return f":{name}:"
|
||
|
||
new_content = _CUSTOM_EMOJI_RE.sub(_check, content)
|
||
|
||
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=48"
|
||
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
|
||
|
||
return new_content, emoji_data
|
||
|
||
|
||
def build_emoji_embeds_and_files(
|
||
emoji_data: list[tuple[str, bytes]],
|
||
) -> tuple[list[discord.Embed], list[discord.File]]:
|
||
"""Build embed-thumbnail pairs for foreign emoji images.
|
||
|
||
Each unknown emoji gets a tiny embed whose thumbnail references the
|
||
attached file via ``attachment://``. Thumbnails render at ~80×80 px.
|
||
"""
|
||
embeds: list[discord.Embed] = []
|
||
files: list[discord.File] = []
|
||
for fname, fdata in emoji_data:
|
||
f = discord.File(io.BytesIO(fdata), filename=fname)
|
||
em = discord.Embed()
|
||
em.set_thumbnail(url=f"attachment://{fname}")
|
||
files.append(f)
|
||
embeds.append(em)
|
||
return embeds, files
|