EveCogs/wormhole/commands/settings.py
Rosie Val 1fbb2657fd fix: add network validation and logging to all settings commands
- Added null check for network existence in all wh set commands
- All setting changes now log to the log channel with descriptive titles
- Prevents KeyError crashes on invalid network names
- Improves audit trail for administrator actions
2026-07-21 22:34:31 -04:00

458 lines
26 KiB
Python

"""Settings commands — all `wh set` sub-commands for network configuration."""
from __future__ import annotations
from typing import Optional
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, COLOUR_OK
from ._base import WormholeBase
class SettingsCommands(WormholeBase):
"""Mixin — network settings management."""
# ── Relay mode ─────────────────────────────────────────────────────────
@WormholeBase.wh_set.command(name="relaymode")
@requires_role(Role.ADMIN)
async def wh_set_relaymode(self, ctx: commands.Context, name: str, mode: str) -> None:
"""Set relay mode: ``webhook``, ``embed``, or ``compact``."""
nd = await self._net(name)
if not nd:
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
if mode not in ("webhook", "embed", "compact"):
return await ctx.send(embed=err_embed("Mode must be `webhook`, `embed`, or `compact`."))
async with self.config.networks() as ns:
ns[name]["relay_mode"] = mode
await ctx.send(embed=ok_embed(f"Relay mode for `{name}` set to **{mode}**."))
await self._audit(name, "set_relaymode", str(ctx.author), details=mode)
await self._log(nd, info_embed(f"Relay mode changed to **{mode}** by {ctx.author}", title="⚙️ Setting Changed"))
# ── Webhooks ──────────────────────────────────────────────────────────
@WormholeBase.wh_set.command(name="webhooks")
@requires_role(Role.ADMIN)
async def wh_set_webhooks(self, ctx: commands.Context, name: str, enabled: bool) -> None:
"""Enable or disable webhook relay."""
nd = await self._net(name)
if not nd:
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
async with self.config.networks() as ns:
ns[name]["use_webhooks"] = enabled
await ctx.send(embed=ok_embed(f"Webhooks {'enabled' if enabled else 'disabled'} for `{name}`."))
await self._audit(name, "set_webhooks", str(ctx.author), details=str(enabled))
await self._log(nd, info_embed(f"Webhooks **{'enabled' if enabled else 'disabled'}** by {ctx.author}", title="⚙️ Setting Changed"))
# ── Sync toggles ──────────────────────────────────────────────────────
@WormholeBase.wh_set.command(name="sync")
@requires_role(Role.ADMIN)
async def wh_set_sync(self, ctx: commands.Context, name: str, feature: str, enabled: bool) -> None:
"""Toggle sync features: ``edits``, ``deletes``, ``reactions``, ``replies``, ``stickers``, ``pins``, ``threads``, ``typing``."""
nd = await self._net(name)
if not nd:
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
key_map = {
"edits": "sync_edits", "deletes": "sync_deletes",
"reactions": "sync_reactions", "replies": "sync_replies",
"stickers": "sync_stickers", "pins": "sync_pins",
"threads": "sync_threads", "typing": "sync_typing",
}
if feature not in key_map:
return await ctx.send(embed=err_embed(f"Unknown feature. Options: {', '.join(key_map)}"))
async with self.config.networks() as ns:
ns[name][key_map[feature]] = enabled
await ctx.send(embed=ok_embed(f"`{feature}` sync {'enabled' if enabled else 'disabled'} for `{name}`."))
await self._audit(name, f"set_{feature}", str(ctx.author), details=str(enabled))
await self._log(nd, info_embed(f"`{feature}` sync **{'enabled' if enabled else 'disabled'}** by {ctx.author}", title="⚙️ Setting Changed"))
# ── Identity ──────────────────────────────────────────────────────────
@WormholeBase.wh_set.command(name="imagemode")
@requires_role(Role.ADMIN)
async def wh_set_imagemode(self, ctx: commands.Context, name: str, mode: str) -> None:
"""Set avatar mode: ``user``, ``server``, ``custom``."""
nd = await self._net(name)
if not nd:
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
if mode not in ("user", "server", "custom"):
return await ctx.send(embed=err_embed("Mode must be `user`, `server`, or `custom`."))
async with self.config.networks() as ns:
ns[name]["image_mode"] = mode
await ctx.send(embed=ok_embed(f"Image mode for `{name}` set to **{mode}**."))
await self._log(nd, info_embed(f"Image mode set to **{mode}** by {ctx.author}", title="⚙️ Setting Changed"))
@WormholeBase.wh_set.command(name="namemode")
@requires_role(Role.ADMIN)
async def wh_set_namemode(self, ctx: commands.Context, name: str, mode: str) -> None:
"""Set name mode: ``user``, ``server``, ``both``, ``custom``."""
nd = await self._net(name)
if not nd:
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
if mode not in ("user", "server", "both", "custom"):
return await ctx.send(embed=err_embed("Mode must be `user`, `server`, `both`, or `custom`."))
async with self.config.networks() as ns:
ns[name]["name_mode"] = mode
await ctx.send(embed=ok_embed(f"Name mode for `{name}` set to **{mode}**."))
await self._log(nd, info_embed(f"Name mode set to **{mode}** by {ctx.author}", title="⚙️ Setting Changed"))
@WormholeBase.wh_set.command(name="icon")
@requires_role(Role.ADMIN)
async def wh_set_icon(self, ctx: commands.Context, name: str, url: str = None) -> None:
"""Set a custom network icon URL."""
nd = await self._net(name)
if not nd:
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
async with self.config.networks() as ns:
ns[name]["custom_icon"] = url
await ctx.send(embed=ok_embed(f"Custom icon {'set' if url else 'cleared'} for `{name}`."))
await self._log(nd, info_embed(f"Custom icon **{'set' if url else 'cleared'}** by {ctx.author}", title="⚙️ Setting Changed"))
@WormholeBase.wh_set.command(name="customname")
@requires_role(Role.ADMIN)
async def wh_set_customname(self, ctx: commands.Context, name: str, *, value: str = None) -> None:
"""Set a custom display name for relayed messages."""
nd = await self._net(name)
if not nd:
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
async with self.config.networks() as ns:
ns[name]["custom_name"] = value
await ctx.send(embed=ok_embed(f"Custom name {'set' if value else 'cleared'} for `{name}`."))
await self._log(nd, info_embed(f"Custom name **{'set' if value else 'cleared'}** by {ctx.author}", title="⚙️ Setting Changed"))
@WormholeBase.wh_set.command(name="colour", aliases=["color"])
@requires_role(Role.ADMIN)
async def wh_set_colour(self, ctx: commands.Context, name: str, hex_code: str = None) -> None:
"""Set the network embed colour (hex, e.g. ``#ff5733``)."""
nd = await self._net(name)
if not nd:
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
if hex_code:
try:
colour = int(hex_code.lstrip("#"), 16)
except ValueError:
return await ctx.send(embed=err_embed("Invalid hex colour."))
else:
colour = None
async with self.config.networks() as ns:
ns[name]["colour"] = colour
await ctx.send(embed=ok_embed(f"Colour {'set' if colour else 'cleared'} for `{name}`."))
await self._log(nd, info_embed(f"Colour **{'set' if colour else 'cleared'}** by {ctx.author}", title="⚙️ Setting Changed"))
# ── Description / MOTD / rules ─────────────────────────────────────────
@WormholeBase.wh_set.command(name="description", aliases=["desc"])
@requires_role(Role.ADMIN)
async def wh_set_desc(self, ctx: commands.Context, name: str, *, text: str) -> None:
"""Set the network description."""
nd = await self._net(name)
if not nd:
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
async with self.config.networks() as ns:
ns[name]["description"] = text
await ctx.send(embed=ok_embed(f"Description updated for `{name}`."))
await self._log(nd, info_embed(f"Description updated by {ctx.author}", title="⚙️ Setting Changed"))
@WormholeBase.wh_set.command(name="motd")
@requires_role(Role.ADMIN)
async def wh_set_motd(self, ctx: commands.Context, name: str, *, text: str = "") -> None:
"""Set or clear the Message of the Day."""
nd = await self._net(name)
if not nd:
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
async with self.config.networks() as ns:
ns[name]["motd"] = text
await ctx.send(embed=ok_embed(f"MOTD {'set' if text else 'cleared'} for `{name}`."))
await self._log(nd, info_embed(f"MOTD **{'set' if text else 'cleared'}** by {ctx.author}", title="⚙️ Setting Changed"))
@WormholeBase.wh_set.command(name="welcome")
@requires_role(Role.ADMIN)
async def wh_set_welcome(self, ctx: commands.Context, name: str, *, text: str = "") -> None:
"""Set or clear the welcome message for new servers."""
nd = await self._net(name)
if not nd:
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
async with self.config.networks() as ns:
ns[name]["welcome_message"] = text
await ctx.send(embed=ok_embed(f"Welcome message {'set' if text else 'cleared'} for `{name}`."))
await self._log(nd, info_embed(f"Welcome message **{'set' if text else 'cleared'}** by {ctx.author}", title="⚙️ Setting Changed"))
# ── Network behaviour ──────────────────────────────────────────────────
@WormholeBase.wh_set.command(name="freeze")
@requires_role(Role.ADMIN)
async def wh_set_freeze(self, ctx: commands.Context, name: str) -> None:
"""Freeze the network — relay paused."""
nd = await self._net(name)
if not nd:
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
async with self.config.networks() as ns:
ns[name]["frozen"] = True
await ctx.send(embed=ok_embed(f"`{name}` is now ❄️ frozen. Messages won't relay."))
await self._audit(name, "freeze", str(ctx.author))
await self._log(nd, info_embed(f"Network frozen by {ctx.author}", title="❄️ Network Frozen"))
@WormholeBase.wh_set.command(name="unfreeze")
@requires_role(Role.ADMIN)
async def wh_set_unfreeze(self, ctx: commands.Context, name: str) -> None:
"""Unfreeze the network — resume relay."""
nd = await self._net(name)
if not nd:
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
async with self.config.networks() as ns:
ns[name]["frozen"] = False
await ctx.send(embed=ok_embed(f"`{name}` is now 🟢 active."))
await self._audit(name, "unfreeze", str(ctx.author))
await self._log(nd, info_embed(f"Network unfrozen by {ctx.author}", title="🟢 Network Active"))
@WormholeBase.wh_set.command(name="anonymous", aliases=["anon"])
@requires_role(Role.ADMIN)
async def wh_set_anonymous(self, ctx: commands.Context, name: str, enabled: bool) -> None:
"""Toggle anonymous mode."""
nd = await self._net(name)
if not nd:
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
import secrets
async with self.config.networks() as ns:
ns[name]["anonymous"] = enabled
if enabled and not ns[name].get("anon_salt"):
ns[name]["anon_salt"] = secrets.token_hex(8)
await ctx.send(embed=ok_embed(f"Anonymous mode {'enabled' if enabled else 'disabled'} for `{name}`."))
await self._log(nd, info_embed(f"Anonymous mode **{'enabled' if enabled else 'disabled'}** by {ctx.author}", title="⚙️ Setting Changed"))
@WormholeBase.wh_set.command(name="nsfw")
@requires_role(Role.ADMIN)
async def wh_set_nsfw(self, ctx: commands.Context, name: str, gate: bool) -> None:
"""Toggle NSFW gate (only relay to NSFW channels if source is NSFW)."""
nd = await self._net(name)
if not nd:
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
async with self.config.networks() as ns:
ns[name]["nsfw_gate"] = gate
await ctx.send(embed=ok_embed(f"NSFW gate {'enabled' if gate else 'disabled'} for `{name}`."))
await self._log(nd, info_embed(f"NSFW gate **{'enabled' if gate else 'disabled'}** by {ctx.author}", title="⚙️ Setting Changed"))
@WormholeBase.wh_set.command(name="mediaonly")
@requires_role(Role.ADMIN)
async def wh_set_mediaonly(self, ctx: commands.Context, name: str, enabled: bool) -> None:
"""Toggle media-only mode (only relay messages with attachments)."""
nd = await self._net(name)
if not nd:
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
async with self.config.networks() as ns:
ns[name]["media_only"] = enabled
await ctx.send(embed=ok_embed(f"Media-only mode {'enabled' if enabled else 'disabled'} for `{name}`."))
await self._log(nd, info_embed(f"Media-only mode **{'enabled' if enabled else 'disabled'}** by {ctx.author}", title="⚙️ Setting Changed"))
@WormholeBase.wh_set.command(name="silent")
@requires_role(Role.ADMIN)
async def wh_set_silent(self, ctx: commands.Context, name: str, enabled: bool) -> None:
"""Toggle silent mode (suppress status messages)."""
nd = await self._net(name)
if not nd:
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
async with self.config.networks() as ns:
ns[name]["silent"] = enabled
await ctx.send(embed=ok_embed(f"Silent mode {'enabled' if enabled else 'disabled'} for `{name}`."))
await self._log(nd, info_embed(f"Silent mode **{'enabled' if enabled else 'disabled'}** by {ctx.author}", title="⚙️ Setting Changed"))
@WormholeBase.wh_set.command(name="embeds")
@requires_role(Role.ADMIN)
async def wh_set_embeds(self, ctx: commands.Context, name: str, enabled: bool) -> None:
"""Toggle forwarding embeds from relayed messages."""
nd = await self._net(name)
if not nd:
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
async with self.config.networks() as ns:
ns[name]["forward_embeds"] = enabled
await ctx.send(embed=ok_embed(f"Embed forwarding {'enabled' if enabled else 'disabled'} for `{name}`."))
await self._log(nd, info_embed(f"Embed forwarding **{'enabled' if enabled else 'disabled'}** by {ctx.author}", title="⚙️ Setting Changed"))
# ── Rate limiting ──────────────────────────────────────────────────────
@WormholeBase.wh_set.command(name="ratelimit")
@requires_role(Role.ADMIN)
async def wh_set_ratelimit(self, ctx: commands.Context, name: str, rate: int = 5, per: float = 10.0) -> None:
"""Set rate limit: *rate* messages per *per* seconds."""
nd = await self._net(name)
if not nd:
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
async with self.config.networks() as ns:
ns[name]["rate_limit_rate"] = rate
ns[name]["rate_limit_per"] = per
if name in self.cooldowns:
self.cooldowns[name].update(rate, per)
else:
from ..utils import CooldownBucket
self.cooldowns[name] = CooldownBucket(rate, per)
await ctx.send(embed=ok_embed(f"Rate limit: {rate} msgs per {per}s for `{name}`."))
await self._log(nd, info_embed(f"Rate limit set to **{rate}** msgs/**{per}**s by {ctx.author}", title="⚙️ Setting Changed"))
@WormholeBase.wh_set.command(name="slowmode")
@requires_role(Role.ADMIN)
async def wh_set_slowmode(self, ctx: commands.Context, name: str, seconds: int = 0) -> None:
"""Set slowmode (seconds between messages per user, 0 to disable)."""
nd = await self._net(name)
if not nd:
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
async with self.config.networks() as ns:
ns[name]["slowmode"] = max(0, seconds)
await ctx.send(embed=ok_embed(f"Slowmode set to {seconds}s for `{name}`."))
await self._log(nd, info_embed(f"Slowmode set to **{seconds}**s by {ctx.author}", title="⚙️ Setting Changed"))
# ── Log channel ────────────────────────────────────────────────────────
@WormholeBase.wh_set.command(name="logchannel", aliases=["log"])
@requires_role(Role.ADMIN)
async def wh_set_logchannel(self, ctx: commands.Context, name: str, channel: discord.TextChannel = None) -> None:
"""Set (or clear) the mod log channel."""
nd = await self._net(name)
if not nd:
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
async with self.config.networks() as ns:
ns[name]["log_channel"] = channel.id if channel else None
await ctx.send(embed=ok_embed(f"Log channel {'set to ' + channel.mention if channel else 'cleared'} for `{name}`."))
if channel:
await self._log(nd, info_embed(f"Log channel set to {channel.mention} by {ctx.author}", title="⚙️ Logging Configured"))
# ── Server nicknames ───────────────────────────────────────────────────
@WormholeBase.wh_set.command(name="servernick")
@requires_role(Role.ADMIN)
async def wh_set_servernick(self, ctx: commands.Context, name: str, guild_id: int, *, nick: str = None) -> None:
"""Set or clear a nickname for a server in relay messages."""
nd = await self._net(name)
if not nd:
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
async with self.config.networks() as ns:
nicks = ns[name].setdefault("server_nicknames", {})
if nick:
nicks[str(guild_id)] = nick
else:
nicks.pop(str(guild_id), None)
await ctx.send(embed=ok_embed(f"Server nickname {'set' if nick else 'cleared'}."))
await self._log(nd, info_embed(f"Server nickname **{'set to ' + nick if nick else 'cleared'}** by {ctx.author}", title="⚙️ Setting Changed"))
# ── Relay delay ────────────────────────────────────────────────────────
@WormholeBase.wh_set.command(name="delay")
@requires_role(Role.ADMIN)
async def wh_set_delay(self, ctx: commands.Context, name: str, seconds: int = 0) -> None:
"""Set relay delay (seconds before messages are relayed, 0 to disable)."""
nd = await self._net(name)
if not nd:
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
async with self.config.networks() as ns:
ns[name]["relay_delay"] = max(0, seconds)
await ctx.send(embed=ok_embed(f"Relay delay set to {seconds}s for `{name}`."))
await self._log(nd, info_embed(f"Relay delay set to **{seconds}**s by {ctx.author}", title="⚙️ Setting Changed"))
# ── Ephemeral auto-delete ──────────────────────────────────────────────
@WormholeBase.wh_set.command(name="ephemeral")
@requires_role(Role.ADMIN)
async def wh_set_ephemeral(self, ctx: commands.Context, name: str, seconds: int = 0) -> None:
"""Auto-delete relayed messages after N seconds (0 to disable)."""
nd = await self._net(name)
if not nd:
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
async with self.config.networks() as ns:
ns[name]["ephemeral_delay"] = max(0, seconds)
await ctx.send(embed=ok_embed(f"Ephemeral delay set to {seconds}s for `{name}`."))
await self._log(nd, info_embed(f"Ephemeral delay set to **{seconds}**s by {ctx.author}", title="⚙️ Setting Changed"))
# ── Discovery ──────────────────────────────────────────────────────────
@WormholeBase.wh_set.command(name="public")
@requires_role(Role.OWNER)
async def wh_set_public(self, ctx: commands.Context, name: str, enabled: bool) -> None:
"""Make network publicly discoverable."""
nd = await self._net(name)
if not nd:
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
async with self.config.networks() as ns:
ns[name]["public"] = enabled
await ctx.send(embed=ok_embed(f"`{name}` is now {'public' if enabled else 'private'}."))
await self._log(nd, info_embed(f"Network visibility set to **{'public' if enabled else 'private'}** by {ctx.author}", title="⚙️ Setting Changed"))
@WormholeBase.wh_set.command(name="tags")
@requires_role(Role.ADMIN)
async def wh_set_tags(self, ctx: commands.Context, name: str, *, tags: str) -> None:
"""Set discovery tags (comma-separated)."""
nd = await self._net(name)
if not nd:
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
tag_list = [t.strip() for t in tags.split(",") if t.strip()]
async with self.config.networks() as ns:
ns[name]["tags"] = tag_list
await ctx.send(embed=ok_embed(f"Tags set for `{name}`: {', '.join(tag_list)}"))
await self._log(nd, info_embed(f"Tags updated: **{', '.join(tag_list)}** by {ctx.author}", title="⚙️ Setting Changed"))
# ── Channel overrides ──────────────────────────────────────────────────
@WormholeBase.wh_set.command(name="override")
@requires_role(Role.ADMIN)
async def wh_set_override(self, ctx: commands.Context, name: str, key: str, value: str) -> None:
"""Set a per-channel config override for the current channel."""
nd = await self._net(name)
if not nd:
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
if ctx.channel.id not in nd.get("channels", []):
return await ctx.send(embed=err_embed("This channel isn't in the network."))
# Try to parse value
parsed = value
if value.lower() in ("true", "false"):
parsed = value.lower() == "true"
else:
try:
parsed = int(value)
except ValueError:
try:
parsed = float(value)
except ValueError:
pass
async with self.config.networks() as ns:
overrides = ns[name].setdefault("channel_overrides", {})
overrides.setdefault(str(ctx.channel.id), {})[key] = parsed
await ctx.send(embed=ok_embed(f"Override `{key} = {parsed}` set for this channel in `{name}`."))
await self._log(nd, info_embed(f"Channel override set: **{key} = {parsed}** by {ctx.author}", title="⚙️ Setting Changed"))
# ── View all settings ──────────────────────────────────────────────────
@WormholeBase.wh_set.command(name="show")
@requires_role(Role.HELPER)
async def wh_set_show(self, ctx: commands.Context, name: str) -> None:
"""Show current settings for a network."""
nd = await self._net(name)
if not nd:
return await ctx.send(embed=err_embed(f"Network `{name}` not found."))
em = discord.Embed(title=f"⚙️ Settings — {name}", colour=COLOUR_INFO)
em.add_field(name="Relay", value=(
f"Mode: {nd.get('relay_mode', 'webhook')}\n"
f"Image: {nd.get('image_mode', 'user')}\n"
f"Name: {nd.get('name_mode', 'both')}\n"
f"Frozen: {nd.get('frozen', False)}\n"
f"Anonymous: {nd.get('anonymous', False)}"
), inline=True)
em.add_field(name="Sync", value=(
f"Edits: {nd.get('sync_edits')}\n"
f"Deletes: {nd.get('sync_deletes')}\n"
f"Reactions: {nd.get('sync_reactions')}\n"
f"Replies: {nd.get('sync_replies')}\n"
f"Typing: {nd.get('sync_typing', False)}"
), inline=True)
em.add_field(name="Limits", value=(
f"Rate: {nd.get('rate_limit_rate', 5)}/{nd.get('rate_limit_per', 10)}s\n"
f"Slowmode: {nd.get('slowmode', 0)}s\n"
f"Delay: {nd.get('relay_delay', 0)}s\n"
f"Ephemeral: {nd.get('ephemeral_delay', 0)}s"
), inline=True)
am = nd.get("automod", {})
if am.get("enabled"):
active = [k.replace("anti_", "") for k, v in am.items() if k.startswith("anti_") and v]
em.add_field(name="Automod", value=", ".join(active) or "None active", inline=False)
await ctx.send(embed=em)