From 04bd004f36fac44adc33008d0d732b6f7e1f679f Mon Sep 17 00:00:00 2001 From: agessaman Date: Sun, 15 Feb 2026 21:02:48 -0800 Subject: [PATCH] Improve monitor channel handling by adding support for quoted values - Introduced `strip_optional_quotes` function to handle monitor channel values with optional surrounding quotes. - Updated `load_monitor_channels` method in `CommandManager` to utilize the new function, ensuring compatibility with quoted and unquoted channel configurations. - Modified `generate_samples` function to apply the same logic for consistency. - Added tests for `strip_optional_quotes` to validate behavior with various input cases. --- generate_website.py | 9 +++++---- modules/command_manager.py | 8 ++++++-- modules/config_validation.py | 15 +++++++++++++++ tests/test_command_manager.py | 6 ++++++ tests/test_config_validation.py | 26 ++++++++++++++++++++++++++ 5 files changed, 58 insertions(+), 6 deletions(-) diff --git a/generate_website.py b/generate_website.py index 52ea432..5053842 100755 --- a/generate_website.py +++ b/generate_website.py @@ -23,6 +23,7 @@ try: from modules.plugin_loader import PluginLoader from modules.db_manager import DBManager from modules.utils import resolve_path + from modules.config_validation import strip_optional_quotes except ImportError as e: print("Error: Missing required dependencies.") print(f"Details: {e}") @@ -2449,8 +2450,8 @@ def generate_samples(config_file): title = get_website_title(config) introduction = get_website_intro(config) - # Get monitor channels - monitor_channels_str = config.get('Channels', 'monitor_channels', fallback='') + # Get monitor channels (quoted or unquoted) + monitor_channels_str = strip_optional_quotes(config.get('Channels', 'monitor_channels', fallback='')) monitor_channels = [ch.strip() for ch in monitor_channels_str.split(',') if ch.strip()] # Load channels from config @@ -2678,8 +2679,8 @@ def main(): introduction = get_website_intro(config) title = get_website_title(config) - # Get monitor channels for channel restriction display - monitor_channels_str = config.get('Channels', 'monitor_channels', fallback='') + # Get monitor channels for channel restriction display (quoted or unquoted) + monitor_channels_str = strip_optional_quotes(config.get('Channels', 'monitor_channels', fallback='')) monitor_channels = [ch.strip() for ch in monitor_channels_str.split(',') if ch.strip()] # Load channels from Channels_List section diff --git a/modules/command_manager.py b/modules/command_manager.py index efea3ed..b4b2557 100644 --- a/modules/command_manager.py +++ b/modules/command_manager.py @@ -17,6 +17,7 @@ from .models import MeshMessage from .plugin_loader import PluginLoader from .commands.base_command import BaseCommand from .utils import check_internet_connectivity_async, decode_escape_sequences, format_keyword_response_with_placeholders +from .config_validation import strip_optional_quotes @dataclass @@ -423,8 +424,11 @@ class CommandManager: return any(sender_id.startswith(entry) for entry in self.banned_users) def load_monitor_channels(self) -> List[str]: - """Load monitored channels from config""" - channels = self.bot.config.get('Channels', 'monitor_channels', fallback='') + """Load monitored channels from config. + Values may be quoted, e.g. \"#bot,#bot-everett,#bots\" or unquoted. + """ + raw = self.bot.config.get('Channels', 'monitor_channels', fallback='') + channels = strip_optional_quotes(raw) return [channel.strip() for channel in channels.split(',') if channel.strip()] def load_channel_keywords(self) -> Optional[List[str]]: diff --git a/modules/config_validation.py b/modules/config_validation.py index 6cc8330..5c81c91 100644 --- a/modules/config_validation.py +++ b/modules/config_validation.py @@ -56,6 +56,21 @@ ADMIN_ACL_SECTION = "Admin_ACL" # admin commands disabled BANNED_USERS_SECTION = "Banned_Users" # empty banned list LOCALIZATION_SECTION = "Localization" # language=en, translation_path=translations/ +def strip_optional_quotes(s: str) -> str: + """Strip one layer of surrounding double or single quotes if present. + + Allows config values like monitor_channels to be written as + "#bot,#bot-everett,#bots" so the list does not look like comments. + Backward compatible: unquoted values are returned unchanged. + """ + if not isinstance(s, str): + return s + s = s.strip() + if len(s) >= 2 and s[0] == s[-1] and s[0] in '"\'': + return s[1:-1] + return s + + # Non-standard section name -> suggested canonical name (exact match) SECTION_TYPO_MAP = { "WebViewer": "Web_Viewer", diff --git a/tests/test_command_manager.py b/tests/test_command_manager.py index d9200c5..f9284fc 100644 --- a/tests/test_command_manager.py +++ b/tests/test_command_manager.py @@ -163,6 +163,12 @@ class TestLoadMonitorChannels: manager = make_manager(cm_bot) assert manager.monitor_channels == [] + def test_load_monitor_channels_quoted(self, cm_bot): + """Quoted monitor_channels (e.g. \"#bot,#bot-everett,#bots\") is supported.""" + cm_bot.config.set("Channels", "monitor_channels", '"#bot,#bot-everett,#bots"') + manager = make_manager(cm_bot) + assert manager.monitor_channels == ["#bot", "#bot-everett", "#bots"] + class TestLoadChannelKeywords: """Tests for channel keyword whitelist loading.""" diff --git a/tests/test_config_validation.py b/tests/test_config_validation.py index 8e35e2a..53f1a75 100644 --- a/tests/test_config_validation.py +++ b/tests/test_config_validation.py @@ -7,6 +7,7 @@ from modules.config_validation import ( SEVERITY_ERROR, SEVERITY_INFO, SEVERITY_WARNING, + strip_optional_quotes, validate_config, _resolve_path, _check_path_writable, @@ -15,6 +16,31 @@ from modules.config_validation import ( ) +class TestStripOptionalQuotes: + """Tests for strip_optional_quotes (monitor_channels and similar config values).""" + + def test_unquoted_unchanged(self): + assert strip_optional_quotes("#bot,#bot-everett,#bots") == "#bot,#bot-everett,#bots" + assert strip_optional_quotes("general,test") == "general,test" + + def test_double_quoted_stripped(self): + assert strip_optional_quotes('"#bot,#bot-everett,#bots"') == "#bot,#bot-everett,#bots" + + def test_single_quoted_stripped(self): + assert strip_optional_quotes("'#bot,#bot-everett,#bots'") == "#bot,#bot-everett,#bots" + + def test_empty_and_whitespace(self): + assert strip_optional_quotes("") == "" + assert strip_optional_quotes(" ") == "" + + def test_mismatched_quotes_not_stripped(self): + assert strip_optional_quotes('"#bot,#bots\'') == '"#bot,#bots\'' + assert strip_optional_quotes('\'#bot,#bots"') == '\'#bot,#bots"' + + def test_single_char_quoted_stripped(self): + assert strip_optional_quotes('"a"') == "a" + + class TestValidateConfig: """Tests for validate_config()."""