feat: add per-channel plugin filtering

Channels with a [channels."#name"] section and `plugins` list only run
those plugins. Unconfigured channels run everything. Core is always
active. PMs are unrestricted. Denied commands are silently ignored.
This commit is contained in:
user
2026-02-15 04:16:41 +01:00
parent ee68e77157
commit 7bbfa9b345
2 changed files with 23 additions and 0 deletions

View File

@@ -281,8 +281,11 @@ class Bot:
return
# Dispatch to event handlers (fire-and-forget)
channel = msg.target if msg.is_channel else None
event_type = msg.command
for handler in self.registry.events.get(event_type, []):
if not self._plugin_allowed(handler.plugin, channel):
continue
self._spawn(handler.callback(self, msg), name=f"event:{handler.name}")
# Dispatch to command handlers (PRIVMSG only)
@@ -312,6 +315,20 @@ class Bot:
await self.conn.send(format_msg("NOTICE", msg.nick, reply))
log.debug("CTCP %s reply to %s", ctcp_cmd, msg.nick)
def _plugin_allowed(self, plugin_name: str, channel: str | None) -> bool:
"""Check if a plugin is allowed in a channel."""
if plugin_name == "core":
return True
if not channel or not channel.startswith(("#", "&")):
return True # PMs are unrestricted
chan_cfg = self.config.get("channels", {}).get(channel)
if not chan_cfg:
return True # unconfigured channels run everything
allowed = chan_cfg.get("plugins")
if allowed is None:
return True
return plugin_name in allowed
def _is_admin(self, msg: Message) -> bool:
"""Check if the message sender is a bot admin.
@@ -345,6 +362,10 @@ class Bot:
name=f"cmd:{cmd_name}:ambiguous")
return
channel = msg.target if msg.is_channel else None
if not self._plugin_allowed(handler.plugin, channel):
return
if handler.admin and not self._is_admin(msg):
deny = f"Permission denied: {self.prefix}{cmd_name} requires admin"
self._spawn(self.reply(msg, deny), name=f"cmd:{cmd_name}:denied")

View File

@@ -29,8 +29,10 @@ DEFAULTS: dict = {
"rate_burst": 5,
"admins": [],
},
"channels": {},
"logging": {
"level": "info",
"format": "text",
},
}