EveCogs/wormhole/services/emoji.py
Viktor AI 0b028e7bb9 refactor: Wormhole v4.0.0 — full modular architecture with hierarchical staff
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)
2026-05-14 19:07:49 +00:00

78 lines
2.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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