diff --git a/config.ini.example b/config.ini.example index 2b10613..4d154a6 100644 --- a/config.ini.example +++ b/config.ini.example @@ -311,6 +311,9 @@ help = "Bot Help: test (or t), ping, help, hello, cmd, advert, @string, wx, aqi, # Time format: HHMM (24-hour, no colon) # Bot will send these messages at the specified times daily # Example: 0800 = general:Good morning! Weather update coming soon. +# +# Newlines: use \n in the message for a line break (e.g. general:Line one\nLine two). +# Literal backslash: use \\n for backslash+n; \\t for tab. # # Available placeholders for mesh network information: # diff --git a/modules/command_manager.py b/modules/command_manager.py index ac1765a..4b13beb 100644 --- a/modules/command_manager.py +++ b/modules/command_manager.py @@ -16,7 +16,7 @@ from meshcore import EventType from .models import MeshMessage from .plugin_loader import PluginLoader from .commands.base_command import BaseCommand -from .utils import check_internet_connectivity_async, format_keyword_response_with_placeholders +from .utils import check_internet_connectivity_async, decode_escape_sequences, format_keyword_response_with_placeholders @dataclass @@ -342,32 +342,6 @@ class CommandManager: self.bot.bot_tx_rate_limiter.record_tx() return True - def _decode_escape_sequences(self, text: str) -> str: - """Decode escape sequences in config strings. - - Processes common escape sequences like \\n (newline), \\t (tab), \\\\ (backslash). - This allows users to add newlines in keyword responses using \n (single backslash). - - Behavior: - - \n in config file → newline character - - \\n in config file → literal backslash + n - - Args: - text: The text string to process. - - Returns: - str: The text with escape sequences decoded. - """ - # Replace escape sequences - # Order matters: \\ must be processed first to avoid double-processing - # This preserves literal backslashes (\\n becomes \n, not a newline) - text = text.replace('\\\\', '\x00') # Temporary placeholder for backslash - text = text.replace('\\n', '\n') # Newline - text = text.replace('\\t', '\t') # Tab - text = text.replace('\\r', '\r') # Carriage return - text = text.replace('\x00', '\\') # Restore backslash - return text - def load_keywords(self) -> Dict[str, str]: """Load keywords from config. @@ -380,8 +354,8 @@ class CommandManager: # Strip quotes from the response if present if response.startswith('"') and response.endswith('"'): response = response[1:-1] - # Decode escape sequences (e.g., \\n for newlines) - response = self._decode_escape_sequences(response) + # Decode escape sequences (e.g., \n for newlines) + response = decode_escape_sequences(response) keywords[keyword.lower()] = response return keywords @@ -393,8 +367,8 @@ class CommandManager: # Strip quotes from the response format if present if response_format.startswith('"') and response_format.endswith('"'): response_format = response_format[1:-1] - # Decode escape sequences (e.g., \\n for newlines) - response_format = self._decode_escape_sequences(response_format) + # Decode escape sequences (e.g., \n for newlines) + response_format = decode_escape_sequences(response_format) syntax_patterns[pattern] = response_format return syntax_patterns diff --git a/modules/commands/greeter_command.py b/modules/commands/greeter_command.py index 7063fec..9410a64 100644 --- a/modules/commands/greeter_command.py +++ b/modules/commands/greeter_command.py @@ -11,6 +11,7 @@ from datetime import datetime, timedelta from typing import Optional, Dict, Any, List, Tuple from .base_command import BaseCommand from ..models import MeshMessage +from ..utils import decode_escape_sequences class GreeterCommand(BaseCommand): @@ -119,39 +120,13 @@ class GreeterCommand(BaseCommand): import traceback self.logger.error(traceback.format_exc()) - def _decode_escape_sequences(self, text: str) -> str: - """Decode escape sequences in config strings. - - Processes common escape sequences like \\n (newline), \\t (tab), \\\\ (backslash). - This allows users to add newlines in greeting messages using \n (single backslash). - - Behavior: - - \n in config file → newline character - - \\n in config file → literal backslash + n - - Args: - text: The text string to process. - - Returns: - str: The text with escape sequences decoded. - """ - # Replace escape sequences - # Order matters: \\ must be processed first to avoid double-processing - # This preserves literal backslashes (\\n becomes \n, not a newline) - text = text.replace('\\\\', '\x00') # Temporary placeholder for backslash - text = text.replace('\\n', '\n') # Newline - text = text.replace('\\t', '\t') # Tab - text = text.replace('\\r', '\r') # Carriage return - text = text.replace('\x00', '\\') # Restore backslash - return text - def _load_config(self) -> None: """Load configuration for greeter command.""" self.enabled = self.get_config_value('Greeter_Command', 'enabled', fallback=False, value_type='bool') self.greeting_message = self.get_config_value('Greeter_Command', 'greeting_message', fallback='Welcome to the mesh, {sender}!') - # Decode escape sequences (e.g., \\n for newlines) - self.greeting_message = self._decode_escape_sequences(self.greeting_message) + # Decode escape sequences (e.g., \n for newlines) + self.greeting_message = decode_escape_sequences(self.greeting_message) self.rollout_days = self.get_config_value('Greeter_Command', 'rollout_days', fallback=7, value_type='int') self.include_mesh_info = self.get_config_value('Greeter_Command', 'include_mesh_info', @@ -159,7 +134,7 @@ class GreeterCommand(BaseCommand): self.mesh_info_format = self.get_config_value('Greeter_Command', 'mesh_info_format', fallback='\n\nMesh Info: {total_contacts} contacts, {repeaters} repeaters') # Decode escape sequences (e.g., \n for newlines) - self.mesh_info_format = self._decode_escape_sequences(self.mesh_info_format) + self.mesh_info_format = decode_escape_sequences(self.mesh_info_format) # Log configuration for debugging self.logger.debug(f"Greeter config loaded: include_mesh_info={self.include_mesh_info}, " @@ -199,8 +174,8 @@ class GreeterCommand(BaseCommand): channel_name, greeting = entry.split(':', 1) channel_name = channel_name.strip() greeting = greeting.strip() - # Decode escape sequences (e.g., \\n for newlines) - greeting = self._decode_escape_sequences(greeting) + # Decode escape sequences (e.g., \n for newlines) + greeting = decode_escape_sequences(greeting) # Store both original and lowercase channel name for case-insensitive matching self.channel_greetings[channel_name.lower()] = { 'channel': channel_name, diff --git a/modules/scheduler.py b/modules/scheduler.py index 18b9975..807b539 100644 --- a/modules/scheduler.py +++ b/modules/scheduler.py @@ -14,7 +14,7 @@ import json import os from typing import Dict, Tuple, Any from pathlib import Path -from .utils import format_keyword_response_with_placeholders +from .utils import decode_escape_sequences, format_keyword_response_with_placeholders class MessageScheduler: @@ -57,15 +57,17 @@ class MessageScheduler: continue channel, message = message_info.split(':', 1) + channel = channel.strip() + message = decode_escape_sequences(message.strip()) # Convert HHMM to HH:MM for scheduler hour = int(time_str[:2]) minute = int(time_str[2:]) schedule_time = f"{hour:02d}:{minute:02d}" schedule.every().day.at(schedule_time).do( - self.send_scheduled_message, channel.strip(), message.strip() + self.send_scheduled_message, channel, message ) - self.scheduled_messages[time_str] = (channel.strip(), message.strip()) + self.scheduled_messages[time_str] = (channel, message) self.logger.info(f"Scheduled message: {schedule_time} -> {channel}: {message}") except ValueError: self.logger.warning(f"Invalid scheduled message format: {message_info}") diff --git a/modules/utils.py b/modules/utils.py index 0b71f34..14f0463 100644 --- a/modules/utils.py +++ b/modules/utils.py @@ -133,6 +133,28 @@ def truncate_string(text: str, max_length: int, ellipsis: str = '...') -> str: return text[:max_length - len(ellipsis)] + ellipsis +def decode_escape_sequences(text: str) -> str: + """Decode escape sequences in config strings (e.g. Keywords, Scheduled_Messages). + + Processes \\n (newline), \\t (tab), \\r (carriage return), \\\\ (literal backslash). + Use a single backslash in config: \\n for newline; \\\\n for literal backslash + n. + + Args: + text: The text string to process. + + Returns: + str: The text with escape sequences decoded. + """ + if not text: + return text + text = text.replace('\\\\', '\x00') # Temporary placeholder for backslash + text = text.replace('\\n', '\n') # Newline + text = text.replace('\\t', '\t') # Tab + text = text.replace('\\r', '\r') # Carriage return + text = text.replace('\x00', '\\') # Restore backslash + return text + + def format_location_for_display(city: Optional[str], state: Optional[str] = None, country: Optional[str] = None, max_length: int = 20) -> Optional[str]: """Format location data for display with intelligent abbreviation.