EveCogs/wormhole/commands/network.py
Viktor AI 2ad7a7b691 fix: wire up missing feature enforcement in relay
- Mirror channels now receive relayed messages (was blocked from sending but never sent to)
- Server allowlist enforced in relay (allowlist_servers gate check)
- Silent mode suppresses status broadcasts (_status respects 'silent' flag)
- Welcome message sent when channel joins via 'wh open'
2026-05-14 19:33:58 +00:00

174 lines
8.6 KiB
Python

"""Network lifecycle commands — create, delete, open, close, list, discover, info."""
from __future__ import annotations
from copy import deepcopy
from datetime import datetime, timezone
from typing import Optional
import discord
from redbot.core import commands
from ..models.config import DEFAULT_NETWORK
from ..models.permissions import Role, has_role, requires_role, role_name, get_role, list_staff
from ..utils import ok_embed, err_embed, info_embed, warn_embed, COLOUR_INFO, COLOUR_OK, truncate
from ._base import WormholeBase
class NetworkCommands(WormholeBase):
"""Mixin — network lifecycle commands."""
@WormholeBase.wh.command(name="create")
async def wh_create(self, ctx: commands.Context, name: str, *, description: str = "") -> None:
"""Create a new wormhole network."""
nets = await self.config.networks()
if name in nets:
return await ctx.send(embed=err_embed(f"Network `{name}` already exists."))
mx = await self.config.max_networks_per_user()
if sum(1 for n in nets.values() if n["owner_id"] == ctx.author.id) >= mx:
return await ctx.send(embed=err_embed(f"You've hit the limit of {mx} networks."))
d = deepcopy(DEFAULT_NETWORK)
d["owner_id"] = ctx.author.id
d["description"] = description
d["created_at"] = datetime.now(timezone.utc).isoformat()
async with self.config.networks() as ns:
ns[name] = d
from ..utils import CooldownBucket
self.cooldowns[name] = CooldownBucket(d["rate_limit_rate"], d["rate_limit_per"])
await ctx.send(embed=ok_embed(
f"Network `{name}` created!\n"
f"Open channels with `wh open {name}` in target channels."
))
await self._audit(name, "create", str(ctx.author))
@WormholeBase.wh.command(name="delete")
async def wh_delete(self, ctx: commands.Context, name: str) -> None:
"""Delete a network (owner or bot owner only)."""
nd = await self._net(name)
if not nd:
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
if not await self.bot.is_owner(ctx.author):
if not has_role(nd, ctx.author.id, Role.OWNER):
return await ctx.send(embed=err_embed("Only the network owner or bot owner can delete a network."))
async with self.config.networks() as ns:
ns.pop(name, None)
self.cooldowns.pop(name, None)
await ctx.send(embed=ok_embed(f"Network `{name}` deleted."))
@WormholeBase.wh.command(name="open")
@requires_role(Role.ADMIN)
async def wh_open(self, ctx: commands.Context, name: str) -> None:
"""Add the current channel to a network."""
nd = await self._net(name)
if not nd:
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
if ctx.channel.id in nd.get("channels", []):
return await ctx.send(embed=err_embed("This channel is already in the network."))
existing = await self._net_for_ch(ctx.channel.id)
if existing:
return await ctx.send(embed=err_embed(f"This channel is already in network `{existing}`."))
async with self.config.networks() as ns:
ns[name].setdefault("channels", []).append(ctx.channel.id)
await ctx.send(embed=ok_embed(f"Channel linked to `{name}`. Messages will now relay."))
await self._audit(name, "open", str(ctx.author), str(ctx.channel.id))
# Send welcome message if configured
nd_fresh = await self._net(name)
welcome = nd_fresh.get("welcome_message") if nd_fresh else None
if welcome:
try:
await ctx.channel.send(embed=info_embed(welcome, title=f"🌀 Welcome to {name}"))
except Exception:
pass
@WormholeBase.wh.command(name="close")
async def wh_close(self, ctx: commands.Context, name: str = None) -> None:
"""Remove the current channel from a network."""
if not name:
name = await self._net_for_ch(ctx.channel.id)
if not name:
return await ctx.send(embed=err_embed("This channel isn't in any network."))
nd = await self._net(name)
if not nd:
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
if not has_role(nd, ctx.author.id, Role.ADMIN) and not await self.bot.is_owner(ctx.author):
return await ctx.send(embed=err_embed("You need Admin role or higher."))
if ctx.channel.id not in nd.get("channels", []):
return await ctx.send(embed=err_embed("This channel isn't in that network."))
async with self.config.networks() as ns:
ns[name]["channels"].remove(ctx.channel.id)
await ctx.send(embed=ok_embed(f"Channel removed from `{name}`."))
await self._audit(name, "close", str(ctx.author), str(ctx.channel.id))
@WormholeBase.wh.command(name="list")
async def wh_list(self, ctx: commands.Context) -> None:
"""List all networks you have access to."""
nets = await self.config.networks()
if not nets:
return await ctx.send(embed=info_embed("No networks exist yet."))
lines = []
for n, d in sorted(nets.items()):
status = "❄️" if d.get("frozen") else "🟢"
ch_count = len(d.get("channels", []))
role = get_role(d, ctx.author.id)
role_tag = f" [{role_name(role)}]" if role > Role.MEMBER else ""
lines.append(f"{status} **{n}** — {ch_count} channels, {d.get('total_messages', 0):,} msgs{role_tag}")
em = discord.Embed(title="🌀 Wormhole Networks", description="\n".join(lines), colour=COLOUR_INFO)
await ctx.send(embed=em)
@WormholeBase.wh.command(name="discover")
async def wh_discover(self, ctx: commands.Context) -> None:
"""Browse public networks."""
nets = await self.config.networks()
public = [(n, d) for n, d in nets.items() if d.get("public")]
if not public:
return await ctx.send(embed=info_embed("No public networks available."))
lines = []
for n, d in public:
desc = truncate(d.get("description", ""), 60) or "No description"
tags = ", ".join(d.get("tags", [])) or "—"
lines.append(f"**{n}** — {desc}\n Tags: {tags} · {len(d.get('channels', []))} ch")
em = discord.Embed(title="🔍 Public Networks", description="\n".join(lines), colour=COLOUR_INFO)
await ctx.send(embed=em)
@WormholeBase.wh.command(name="info")
async def wh_info(self, ctx: commands.Context, name: str) -> None:
"""Show detailed info about a network."""
nd = await self._net(name)
if not nd:
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
colour = discord.Colour(nd["colour"]) if nd.get("colour") else COLOUR_INFO
status = "❄️ Frozen" if nd.get("frozen") else "🟢 Active"
owner = self.bot.get_user(nd["owner_id"])
em = discord.Embed(title=f"🌀 {name}", description=nd.get("description") or "No description", colour=colour)
em.add_field(name="Status", value=status, inline=True)
em.add_field(name="Owner", value=str(owner or nd["owner_id"]), inline=True)
staff = list_staff(nd)
staff_summary = []
for role in (Role.ADMIN, Role.MODERATOR, Role.HELPER):
count = sum(1 for r in staff.values() if r == role)
if count:
staff_summary.append(f"{role_name(role)}: {count}")
em.add_field(name="Staff", value="\n".join(staff_summary) if staff_summary else "None", inline=True)
em.add_field(name="Channels", value=str(len(nd.get("channels", []))), inline=True)
em.add_field(name="Messages", value=f"{nd.get('total_messages', 0):,}", inline=True)
em.add_field(name="Relay Mode", value=nd.get("relay_mode", "webhook"), inline=True)
features = []
for key, label in [
("sync_edits", "Edit sync"), ("sync_deletes", "Delete sync"),
("sync_reactions", "Reaction sync"), ("sync_replies", "Reply sync"),
("dm_enabled", "DM relay"), ("anonymous", "Anonymous"),
("rules_required", "ToS gate"), ("media_only", "Media-only"),
]:
if nd.get(key):
features.append(f"✅ {label}")
if features:
em.add_field(name="Features", value=" · ".join(features), inline=False)
your_role = get_role(nd, ctx.author.id)
em.set_footer(text=f"Your role: {role_name(your_role)} · Created {(nd.get('created_at') or '?')[:10]}")
await ctx.send(embed=em)