Add comprehensive config options matching fq51bbs

New features:
- Rate limiting (per-user and global)
- Enhanced logging with file rotation
- LLM fallback backend support
- Safety filtering (profanity, blocked phrases, emergency keywords)
- User management (blocklist, allowlist, admin/VIP nodes)
- Custom commands with static responses
- Personality/prompt templates with persona switching
- Web status page with JSON API
- Periodic announcements/broadcasts
- Webhook integrations

New modules:
- rate_limiter.py - Per-user and global rate limiting
- safety.py - Response filtering and user access control
- personality.py - Prompt templates and persona management
- web_status.py - Simple web status dashboard
- announcements.py - Periodic broadcast scheduler
- webhook.py - Webhook notification client
- log_setup.py - Enhanced logging configuration
- backends/fallback.py - LLM fallback wrapper

Config expanded from ~50 to ~200 lines with full documentation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Matt 2025-12-15 13:10:02 -07:00
commit 165da72d8d
13 changed files with 1796 additions and 48 deletions

View file

@ -8,11 +8,38 @@ from .base import CommandContext, CommandHandler
logger = logging.getLogger(__name__)
class CustomCommandHandler(CommandHandler):
"""Handler for user-defined static response commands."""
def __init__(self, name: str, response: str, description: str = "Custom command"):
self._name = name
self._response = response
self._description = description
@property
def name(self) -> str:
return self._name
@property
def description(self) -> str:
return self._description
@property
def usage(self) -> str:
return f"!{self._name}"
async def execute(self, args: str, context: CommandContext) -> str:
return self._response
class CommandDispatcher:
"""Registry and dispatcher for bang commands."""
def __init__(self):
def __init__(self, prefix: str = "!", disabled_commands: Optional[list[str]] = None):
self._commands: dict[str, CommandHandler] = {}
self._custom_commands: dict[str, str] = {}
self.prefix = prefix
self.disabled_commands = set(c.upper() for c in (disabled_commands or []))
def register(self, handler: CommandHandler) -> None:
"""Register a command handler.
@ -21,9 +48,40 @@ class CommandDispatcher:
handler: CommandHandler instance to register
"""
name = handler.name.upper()
if name in self.disabled_commands:
logger.debug(f"Skipping disabled command: !{handler.name}")
return
self._commands[name] = handler
logger.debug(f"Registered command: !{handler.name}")
def register_custom(self, name: str, response: str, description: str = "Custom command") -> None:
"""Register a custom static response command.
Args:
name: Command name (without prefix)
response: Static response text
description: Command description for help
"""
handler = CustomCommandHandler(name, response, description)
self.register(handler)
self._custom_commands[name.upper()] = response
def unregister(self, name: str) -> bool:
"""Unregister a command.
Args:
name: Command name to remove
Returns:
True if command was removed, False if not found
"""
name = name.upper()
if name in self._commands:
del self._commands[name]
self._custom_commands.pop(name, None)
return True
return False
def get_commands(self) -> list[CommandHandler]:
"""Get all registered command handlers."""
return list(self._commands.values())
@ -35,25 +93,25 @@ class CommandDispatcher:
text: Message text to check
Returns:
True if text starts with !
True if text starts with command prefix
"""
return text.strip().startswith("!")
return text.strip().startswith(self.prefix)
def parse(self, text: str) -> tuple[Optional[str], str]:
"""Parse command and arguments from text.
Args:
text: Message text starting with !
text: Message text starting with command prefix
Returns:
Tuple of (command_name, arguments) or (None, "") if invalid
"""
text = text.strip()
if not text.startswith("!"):
if not text.startswith(self.prefix):
return None, ""
# Remove ! prefix
text = text[1:]
# Remove prefix
text = text[len(self.prefix):]
# Split into command and args
parts = text.split(maxsplit=1)
@ -96,21 +154,48 @@ class CommandDispatcher:
return f"Error: {str(e)[:100]}"
def create_dispatcher() -> CommandDispatcher:
"""Create and populate command dispatcher with default commands."""
def create_dispatcher(
prefix: str = "!",
disabled_commands: Optional[list[str]] = None,
custom_commands: Optional[dict] = None,
) -> CommandDispatcher:
"""Create and populate command dispatcher with default commands.
Args:
prefix: Command prefix (default: "!")
disabled_commands: List of command names to disable
custom_commands: Dict of name -> response for custom commands
Returns:
Configured CommandDispatcher
"""
from .help import HelpCommand
from .ping import PingCommand
from .reset import ResetCommand
from .status import StatusCommand
from .weather import WeatherCommand
dispatcher = CommandDispatcher()
dispatcher = CommandDispatcher(prefix=prefix, disabled_commands=disabled_commands)
# Register all commands
# Register all built-in commands
dispatcher.register(HelpCommand(dispatcher))
dispatcher.register(PingCommand())
dispatcher.register(ResetCommand())
dispatcher.register(StatusCommand())
dispatcher.register(WeatherCommand())
# Register custom commands
if custom_commands:
for name, response in custom_commands.items():
if isinstance(response, dict):
# Support dict format: {response: "...", description: "..."}
dispatcher.register_custom(
name,
response.get("response", ""),
response.get("description", "Custom command"),
)
else:
# Simple string response
dispatcher.register_custom(name, str(response))
return dispatcher