refactor: Consolidate escape sequence decoding across modules

- Removed the custom `_decode_escape_sequences` method from multiple classes and replaced it with a centralized `decode_escape_sequences` function in the utils module.
- Updated all relevant modules (CommandManager, MessageScheduler, GreeterCommand) to utilize the new function for decoding escape sequences in configuration strings, improving code maintainability and consistency.
- Enhanced the config example to clarify the usage of escape sequences in messages.
This commit is contained in:
agessaman
2026-02-02 16:18:14 -08:00
parent 248a8da568
commit e6bec91c2b
5 changed files with 41 additions and 65 deletions
+3
View File
@@ -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:
#
+5 -31
View File
@@ -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
+6 -31
View File
@@ -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,
+5 -3
View File
@@ -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}")
+22
View File
@@ -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.