From 1fbb2657fdfccf3e3a26bbccb8a477b1a1559d23 Mon Sep 17 00:00:00 2001 From: Rosie Val Date: Tue, 21 Jul 2026 22:34:31 -0400 Subject: [PATCH] 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 --- wormhole/commands/settings.py | 110 +++++++++++++++++++++++++++++++++- 1 file changed, 108 insertions(+), 2 deletions(-) diff --git a/wormhole/commands/settings.py b/wormhole/commands/settings.py index 889cd96..accad60 100644 --- a/wormhole/commands/settings.py +++ b/wormhole/commands/settings.py @@ -21,23 +21,31 @@ class SettingsCommands(WormholeBase): @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 ─────────────────────────────────────────────────────────── + # ── 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 ────────────────────────────────────────────────────── @@ -45,6 +53,9 @@ class SettingsCommands(WormholeBase): @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", @@ -57,49 +68,69 @@ class SettingsCommands(WormholeBase): 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 ─────────────────────────────────────────────────────────── + # ── 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) @@ -110,6 +141,7 @@ class SettingsCommands(WormholeBase): 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 ───────────────────────────────────────── @@ -117,25 +149,37 @@ class SettingsCommands(WormholeBase): @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 ────────────────────────────────────────────────── @@ -143,62 +187,90 @@ class SettingsCommands(WormholeBase): @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 ────────────────────────────────────────────────────── @@ -206,6 +278,9 @@ class SettingsCommands(WormholeBase): @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 @@ -215,14 +290,19 @@ class SettingsCommands(WormholeBase): 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 ──────────────────────────────────────────────────────── @@ -230,9 +310,14 @@ class SettingsCommands(WormholeBase): @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 ─────────────────────────────────────────────────── @@ -240,6 +325,9 @@ class SettingsCommands(WormholeBase): @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: @@ -247,6 +335,7 @@ class SettingsCommands(WormholeBase): 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 ──────────────────────────────────────────────────────── @@ -254,9 +343,13 @@ class SettingsCommands(WormholeBase): @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 ────────────────────────────────────────────── @@ -264,9 +357,13 @@ class SettingsCommands(WormholeBase): @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 ────────────────────────────────────────────────────────── @@ -274,18 +371,26 @@ class SettingsCommands(WormholeBase): @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 ────────────────────────────────────────────────── @@ -314,6 +419,7 @@ class SettingsCommands(WormholeBase): 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 ──────────────────────────────────────────────────