feat: voice profiles, rubberband FX, per-bot plugin filtering

- Add rubberband package to container for pitch-shifting FX
- Split FX chain: rubberband CLI for pitch, ffmpeg for filters
- Configurable voice profile (voice, fx, piper params) in [voice]
- Extra bots inherit voice config (minus trigger) for own TTS
- Greeting is voice-only, spoken directly by the greeting bot
- Per-bot only_plugins/except_plugins filtering on Mumble
- Alias plugin, core plugin tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
user
2026-02-22 11:41:00 +01:00
parent 3afeace6e7
commit e9d17e8b00
13 changed files with 1398 additions and 111 deletions

View File

@@ -36,6 +36,20 @@ class TestDecorators:
assert handler._derp_event == "PRIVMSG"
def test_command_decorator_aliases(self):
@command("skip", help="skip track", aliases=["next", "s"])
async def handler(bot, msg):
pass
assert handler._derp_aliases == ["next", "s"]
def test_command_decorator_aliases_default(self):
@command("ping", help="ping")
async def handler(bot, msg):
pass
assert handler._derp_aliases == []
def test_command_decorator_admin(self):
@command("secret", help="admin only", admin=True)
async def handler(bot, msg):
@@ -208,6 +222,46 @@ class TestRegistry:
assert registry.commands["secret"].admin is True
assert registry.commands["public"].admin is False
def test_load_plugin_aliases(self, tmp_path: Path):
plugin_file = tmp_path / "aliased.py"
plugin_file.write_text(textwrap.dedent("""\
from derp.plugin import command
@command("skip", help="Skip track", aliases=["next", "s"])
async def cmd_skip(bot, msg):
pass
"""))
registry = PluginRegistry()
count = registry.load_plugin(plugin_file)
assert count == 3 # primary + 2 aliases
assert "skip" in registry.commands
assert "next" in registry.commands
assert "s" in registry.commands
# Aliases point to the same callback
assert registry.commands["next"].callback is registry.commands["skip"].callback
assert registry.commands["s"].callback is registry.commands["skip"].callback
# Alias help text references the primary command
assert registry.commands["next"].help == "alias for !skip"
def test_unload_removes_aliases(self, tmp_path: Path):
plugin_file = tmp_path / "aliased.py"
plugin_file.write_text(textwrap.dedent("""\
from derp.plugin import command
@command("skip", help="Skip track", aliases=["next"])
async def cmd_skip(bot, msg):
pass
"""))
registry = PluginRegistry()
registry.load_plugin(plugin_file)
assert "next" in registry.commands
registry.unload_plugin("aliased")
assert "skip" not in registry.commands
assert "next" not in registry.commands
def test_load_plugin_stores_path(self, tmp_path: Path):
plugin_file = tmp_path / "pathed.py"
plugin_file.write_text(textwrap.dedent("""\
@@ -677,6 +731,71 @@ class TestChannelFilter:
assert bot._plugin_allowed("encode", "&local") is False
class TestAliasDispatch:
"""Test alias fallback in _dispatch_command."""
@staticmethod
def _make_bot_with_alias(alias_name: str, target_cmd: str) -> tuple[Bot, list]:
"""Create a Bot with a command and an alias pointing to it."""
config = {
"server": {"host": "localhost", "port": 6667, "tls": False,
"nick": "test", "user": "test", "realname": "test"},
"bot": {"prefix": "!", "channels": [], "plugins_dir": "plugins"},
}
registry = PluginRegistry()
called = []
async def _handler(bot, msg):
called.append(msg.text)
registry.register_command(target_cmd, _handler, plugin="test")
bot = Bot("test", config, registry)
bot.conn = _FakeConnection()
bot.state.set("alias", alias_name, target_cmd)
return bot, called
def test_alias_resolves_command(self):
"""An alias triggers the target command handler."""
bot, called = self._make_bot_with_alias("s", "skip")
msg = Message(raw="", prefix="nick!u@h", nick="nick",
command="PRIVMSG", params=["#ch", "!s"], tags={})
async def _run():
bot._dispatch_command(msg)
await asyncio.sleep(0.05) # let spawned task run
asyncio.run(_run())
assert len(called) == 1
def test_alias_ignored_when_command_exists(self):
"""Direct command match takes priority over alias."""
bot, called = self._make_bot_with_alias("skip", "stop")
# "skip" is both a real command and an alias to "stop"; real wins
msg = Message(raw="", prefix="nick!u@h", nick="nick",
command="PRIVMSG", params=["#ch", "!skip"], tags={})
async def _run():
bot._dispatch_command(msg)
await asyncio.sleep(0.05)
asyncio.run(_run())
assert len(called) == 1
# Handler was the "skip" handler, not "stop"
def test_no_alias_no_crash(self):
"""Unknown command with no alias silently returns."""
config = {
"server": {"host": "localhost", "port": 6667, "tls": False,
"nick": "test", "user": "test", "realname": "test"},
"bot": {"prefix": "!", "channels": [], "plugins_dir": "plugins"},
}
bot = Bot("test", config, PluginRegistry())
bot.conn = _FakeConnection()
msg = Message(raw="", prefix="nick!u@h", nick="nick",
command="PRIVMSG", params=["#ch", "!nonexistent"], tags={})
bot._dispatch_command(msg) # should not raise
class TestSplitUtf8:
"""Test UTF-8 safe message splitting."""