EveCogs/wormhole/commands/bridge.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

111 lines
5 KiB
Python

"""Bridge commands — cross-network bridging and mirror channels."""
from __future__ import annotations
import discord
from redbot.core import commands
from ..models.permissions import Role, requires_role
from ..utils import ok_embed, err_embed, info_embed, COLOUR_INFO
class BridgeCommands:
"""Mixin — network bridging and mirror channels."""
@commands.group(name="wh-bridge", aliases=["whbridge"], invoke_without_command=True)
async def wh_bridge(self, ctx: commands.Context) -> None:
"""Cross-network bridging."""
await ctx.send_help(ctx.command)
@wh_bridge.command(name="link")
@requires_role(Role.OWNER)
async def wh_bridge_link(self, ctx: commands.Context, source: str, target: str) -> None:
"""Bridge messages from one network to another (one-way)."""
nd_src = await self._net(source)
nd_tgt = await self._net(target)
if not nd_src:
return await ctx.send(embed=err_embed(f"Network `{source}` not found."))
if not nd_tgt:
return await ctx.send(embed=err_embed(f"Network `{target}` not found."))
async with self.config.networks() as ns:
bridge_to = ns[source].setdefault("bridge_to", [])
if target not in bridge_to:
bridge_to.append(target)
bridge_from = ns[target].setdefault("bridge_from", [])
if source not in bridge_from:
bridge_from.append(source)
await ctx.send(embed=ok_embed(f"Bridge: `{source}` → `{target}` (one-way)."))
await self._audit(source, "bridge_link", str(ctx.author), target)
@wh_bridge.command(name="unlink")
@requires_role(Role.OWNER)
async def wh_bridge_unlink(self, ctx: commands.Context, source: str, target: str) -> None:
"""Remove a bridge."""
async with self.config.networks() as ns:
if source in ns:
bt = ns[source].get("bridge_to", [])
if target in bt:
bt.remove(target)
if target in ns:
bf = ns[target].get("bridge_from", [])
if source in bf:
bf.remove(source)
await ctx.send(embed=ok_embed(f"Bridge `{source}` → `{target}` removed."))
await self._audit(source, "bridge_unlink", str(ctx.author), target)
@wh_bridge.command(name="list")
async def wh_bridge_list(self, ctx: commands.Context, name: str) -> None:
"""List bridges for a network."""
nd = await self._net(name)
if not nd:
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
lines = []
for t in nd.get("bridge_to", []):
lines.append(f"➡️ `{name}` → `{t}`")
for f in nd.get("bridge_from", []):
lines.append(f"⬅️ `{f}` → `{name}`")
if not lines:
lines.append("No bridges configured.")
await ctx.send(embed=info_embed("\n".join(lines), title=f"🌉 Bridges — {name}"))
# ── Mirror channels ────────────────────────────────────────────────────
@commands.group(name="wh-mirror", aliases=["whmirror"], invoke_without_command=True)
async def wh_mirror(self, ctx: commands.Context) -> None:
"""Mirror channels — exact copy of relay to an extra channel."""
await ctx.send_help(ctx.command)
@wh_mirror.command(name="add")
@requires_role(Role.ADMIN)
async def wh_mirror_add(self, ctx: commands.Context, name: str, channel: discord.TextChannel) -> None:
"""Add a mirror channel."""
async with self.config.networks() as ns:
mirrors = ns[name].setdefault("mirror_channels", [])
if channel.id not in mirrors:
mirrors.append(channel.id)
await ctx.send(embed=ok_embed(f"{channel.mention} added as mirror for `{name}`."))
@wh_mirror.command(name="remove", aliases=["rm"])
@requires_role(Role.ADMIN)
async def wh_mirror_rm(self, ctx: commands.Context, name: str, channel: discord.TextChannel) -> None:
"""Remove a mirror channel."""
async with self.config.networks() as ns:
mirrors = ns[name].get("mirror_channels", [])
if channel.id in mirrors:
mirrors.remove(channel.id)
await ctx.send(embed=ok_embed(f"{channel.mention} removed from mirrors for `{name}`."))
@wh_mirror.command(name="list")
async def wh_mirror_list(self, ctx: commands.Context, name: str) -> None:
"""List mirror channels."""
nd = await self._net(name)
if not nd:
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
mirrors = nd.get("mirror_channels", [])
if not mirrors:
return await ctx.send(embed=info_embed("No mirror channels."))
lines = []
for ch_id in mirrors:
ch = self.bot.get_channel(ch_id)
lines.append(f"• {ch.mention if ch else ch_id}")
await ctx.send(embed=info_embed("\n".join(lines), title=f"🪞 Mirrors — {name}"))