From 277491f5352db95473cc6335a271ac63f97b684f Mon Sep 17 00:00:00 2001 From: Chris Wiegand Date: Sat, 11 Apr 2026 15:34:00 -0600 Subject: [PATCH 1/3] create common base function to match replies of bot name even when command class is inherited, patch all overriding implementations to use this --- .../commands/alternatives/wx_international.py | 6 +-- modules/commands/base_command.py | 54 ++++++++++++------- modules/commands/channels_command.py | 6 +-- modules/commands/dadjoke_command.py | 6 +-- modules/commands/dice_command.py | 13 ++--- modules/commands/hacker_command.py | 7 +-- modules/commands/joke_command.py | 6 +-- modules/commands/multitest_command.py | 12 ++--- modules/commands/path_command.py | 12 ++--- modules/commands/prefix_command.py | 10 +--- modules/commands/repeater_command.py | 9 +--- modules/commands/roll_command.py | 16 +++--- modules/commands/sports_command.py | 9 ++-- modules/commands/trace_command.py | 10 ++-- modules/commands/webviewer_command.py | 9 +--- modules/commands/wx_command.py | 6 +-- 16 files changed, 75 insertions(+), 116 deletions(-) diff --git a/modules/commands/alternatives/wx_international.py b/modules/commands/alternatives/wx_international.py index fca8ef8..3d91470 100644 --- a/modules/commands/alternatives/wx_international.py +++ b/modules/commands/alternatives/wx_international.py @@ -104,10 +104,8 @@ class GlobalWxCommand(BaseCommand): Returns: bool: True if message matches a keyword, False otherwise. """ - content = message.content.strip() - if content.startswith('!'): - content = content[1:].strip() - content_lower = content.lower() + content_lower = self.cleanup_message_for_matching(message) + for keyword in self.keywords: if content_lower.startswith(keyword + ' ') or content_lower == keyword: return True diff --git a/modules/commands/base_command.py b/modules/commands/base_command.py index 15a2443..f27d986 100644 --- a/modules/commands/base_command.py +++ b/modules/commands/base_command.py @@ -5,6 +5,7 @@ Provides common functionality and interface for command implementations """ from abc import ABC, abstractmethod +from email.mime import message from typing import Optional, List, Dict, Any, Tuple from datetime import datetime import pytz @@ -747,6 +748,37 @@ class BaseCommand(ABC): cleaned = re.sub(r'\s+', ' ', cleaned).strip() return cleaned + def cleanup_message_for_matching(self, message: MeshMessage) -> str: + """Clean up message text before keyword checking. + + This includes stripping mentions and command prefix. + + Args: + text: The original message text. + """ + content = message.content.strip() + + if self._command_prefix: + # If prefix is configured, message must start with it + if not content.startswith(self._command_prefix): + return "" + # Strip the prefix + content = content[len(self._command_prefix):].strip() + else: + # Handle command-style messages (legacy "!" prefix) for backward compatibility + if content.startswith('!'): + content = content[1:].strip() + + # Check if mentions are valid (bot must be mentioned if any mentions exist) + if not self._check_mentions_ok(content): + return "" + + # Strip @[username] mentions before checking keywords + content = self._strip_mentions(content) + + content_lower = content.lower() + return content_lower + def matches_keyword(self, message: MeshMessage) -> bool: """Check if this command matches the message content based on keywords. @@ -764,28 +796,10 @@ class BaseCommand(ABC): if not self.keywords: return False - content = message.content.strip() - - # Check for command prefix if configured - if self._command_prefix: - # If prefix is configured, message must start with it - if not content.startswith(self._command_prefix): - return False - # Strip the prefix - content = content[len(self._command_prefix):].strip() - else: - # If no prefix configured, strip legacy "!" prefix for backward compatibility - if content.startswith('!'): - content = content[1:].strip() - - # Check if mentions are valid (bot must be mentioned if any mentions exist) - if not self._check_mentions_ok(content): + content_lower = self.cleanup_message_for_matching(message) + if not content_lower: return False - # Strip @[username] mentions before checking keywords - content = self._strip_mentions(content) - content_lower = content.lower() - for keyword in self.keywords: keyword_lower = keyword.lower() diff --git a/modules/commands/channels_command.py b/modules/commands/channels_command.py index b10eff1..3018c56 100644 --- a/modules/commands/channels_command.py +++ b/modules/commands/channels_command.py @@ -71,11 +71,7 @@ class ChannelsCommand(BaseCommand): if not self.keywords: return False - # Strip exclamation mark if present (for command-style messages) - content = message.content.strip() - if content.startswith('!'): - content = content[1:].strip() - content_lower = content.lower() + content_lower = self.cleanup_message_for_matching(message) # Don't match if this looks like a subcommand of another command # (e.g., "stats channels" should not match "channels" command) diff --git a/modules/commands/dadjoke_command.py b/modules/commands/dadjoke_command.py index 7a0220a..c0bd49c 100644 --- a/modules/commands/dadjoke_command.py +++ b/modules/commands/dadjoke_command.py @@ -64,10 +64,8 @@ class DadJokeCommand(BaseCommand): Returns: bool: True if message matches a keyword, False otherwise. """ - content = message.content.strip() - if content.startswith('!'): - content = content[1:].strip() - content_lower = content.lower() + content_lower = self.cleanup_message_for_matching(message) + for keyword in self.keywords: # Match if keyword is at start followed by space or end of message if content_lower == keyword or content_lower.startswith(keyword + ' '): diff --git a/modules/commands/dice_command.py b/modules/commands/dice_command.py index abcfef2..7d2f44e 100644 --- a/modules/commands/dice_command.py +++ b/modules/commands/dice_command.py @@ -82,20 +82,17 @@ class DiceCommand(BaseCommand): Returns: bool: True if message is a dice command, False otherwise. """ - content = message.content.strip().lower() - - # Handle command-style messages - if content.startswith('!'): - content = content[1:].strip().lower() + + content_lower = self.cleanup_message_for_matching(message) # Check for exact "dice" match - if content == "dice": + if content_lower == "dice": return True # Check for dice with parameters (dice d20, dice 20, dice d6, etc.) # Match any message starting with "dice " - validation happens in execute() - if content.startswith("dice "): - words = content.split() + if content_lower.startswith("dice "): + words = content_lower.split() if len(words) >= 2 and words[0] == "dice": return True # Match any dice command, validation in execute() diff --git a/modules/commands/hacker_command.py b/modules/commands/hacker_command.py index a3c186d..b4d5f77 100644 --- a/modules/commands/hacker_command.py +++ b/modules/commands/hacker_command.py @@ -483,11 +483,8 @@ class HackerCommand(BaseCommand): if not self.enabled: return False - content = message.content.strip() - if content.startswith('!'): - content = content[1:].strip() - content_lower = content.lower() - + content_lower = self.cleanup_message_for_matching(message) + # Commands that should match exactly (no arguments) exact_match_commands = ['ls -l', 'ls -la', 'echo $PATH', 'df -h', 'whoami', 'history', 'top', 'htop', 'free', 'uname -a'] diff --git a/modules/commands/joke_command.py b/modules/commands/joke_command.py index 82381f5..f2f1de4 100644 --- a/modules/commands/joke_command.py +++ b/modules/commands/joke_command.py @@ -84,10 +84,8 @@ class JokeCommand(BaseCommand): Returns: bool: True if a joke keyword matches, False otherwise. """ - content = message.content.strip() - if content.startswith('!'): - content = content[1:].strip() - content_lower = content.lower() + content_lower = self.cleanup_message_for_matching(message) + for keyword in self.keywords: # Match if keyword is at start followed by space or end of message if content_lower == keyword or content_lower.startswith(keyword + ' '): diff --git a/modules/commands/multitest_command.py b/modules/commands/multitest_command.py index 1454f52..bf655b0 100644 --- a/modules/commands/multitest_command.py +++ b/modules/commands/multitest_command.py @@ -89,15 +89,9 @@ class MultitestCommand(BaseCommand): return self.translate('commands.multitest.help', fallback="Listens for 6 seconds and collects all unique paths from incoming messages") def matches_keyword(self, message: MeshMessage) -> bool: - """Check if message matches multitest keyword""" - content = message.content.strip() - - # Handle exclamation prefix - if content.startswith('!'): - content = content[1:].strip() - - content_lower = content.lower() - + """Check if message matches multitest keyword""" + content_lower = self.cleanup_message_for_matching(message) + # Check for exact match or keyword followed by space for keyword in self.keywords: if content_lower == keyword or content_lower.startswith(keyword + ' '): diff --git a/modules/commands/path_command.py b/modules/commands/path_command.py index 2c0b52d..db4df3a 100644 --- a/modules/commands/path_command.py +++ b/modules/commands/path_command.py @@ -171,19 +171,13 @@ class PathCommand(BaseCommand): def matches_keyword(self, message: MeshMessage) -> bool: """Check if message starts with 'path' keyword or 'p' shortcut (if enabled)""" - content = message.content.strip() - - # Handle exclamation prefix - if content.startswith('!'): - content = content[1:].strip() - - content_lower = content.lower() - + content_lower = self.cleanup_message_for_matching(message) + # Handle "p" shortcut if enabled if self.enable_p_shortcut: if content_lower == "p": return True # Just "p" by itself - elif (content.startswith('p ') or content.startswith('P ')) and len(content) > 2: + elif content_lower.startswith('p ') and len(content_lower) > 2: return True # "p " followed by path data # Check if message starts with any of our keywords diff --git a/modules/commands/prefix_command.py b/modules/commands/prefix_command.py index f780ff3..b8eec7e 100644 --- a/modules/commands/prefix_command.py +++ b/modules/commands/prefix_command.py @@ -135,14 +135,8 @@ class PrefixCommand(BaseCommand): def matches_keyword(self, message: MeshMessage) -> bool: """Check if message starts with 'prefix' keyword""" - content = message.content.strip() - - # Handle exclamation prefix - if content.startswith('!'): - content = content[1:].strip() - - # Check if message starts with 'prefix' (with or without space) - content_lower = content.lower() + content_lower = self.cleanup_message_for_matching(message) + return content_lower == 'prefix' or content_lower.startswith('prefix ') async def _parse_location_to_lat_lon(self, location: str) -> Tuple[Optional[float], Optional[float], Optional[str]]: diff --git a/modules/commands/repeater_command.py b/modules/commands/repeater_command.py index 0217f20..bab7b32 100644 --- a/modules/commands/repeater_command.py +++ b/modules/commands/repeater_command.py @@ -87,14 +87,9 @@ class RepeaterCommand(BaseCommand): Returns: bool: True if the message starts with any of the command keywords. """ - content = message.content.strip() - - # Handle exclamation prefix - if content.startswith('!'): - content = content[1:].strip() - + content_lower = self.cleanup_message_for_matching(message) + # Check if message starts with any of our keywords - content_lower = content.lower() for keyword in self.keywords: if content_lower.startswith(keyword + ' ') or content_lower == keyword: return True diff --git a/modules/commands/roll_command.py b/modules/commands/roll_command.py index ed692b7..eb16065 100644 --- a/modules/commands/roll_command.py +++ b/modules/commands/roll_command.py @@ -73,22 +73,18 @@ class RollCommand(BaseCommand): Returns: bool: True if the message matches the roll command syntax, False otherwise. """ - content = message.content.strip().lower() - - # Handle command-style messages - if content.startswith('!'): - content = content[1:].strip().lower() - + content_lower = self.cleanup_message_for_matching(message) + # Check for exact "roll" match - if content == "roll": + if content_lower == "roll": return True # Check for roll with parameters (roll 50, roll 1000, etc.) # Ensure "roll" is the first word and followed by valid number - if content.startswith("roll "): - words = content.split() + if content_lower.startswith("roll "): + words = content_lower.split() if len(words) >= 2 and words[0] == "roll": - roll_part = content[5:].strip() # Get everything after "roll " + roll_part = content_lower[5:].strip() # Get everything after "roll " # Check if the roll part is valid number notation (not just any word) max_num = self.parse_roll_notation(roll_part) return max_num is not None # Only match if it's valid number notation diff --git a/modules/commands/sports_command.py b/modules/commands/sports_command.py index 152f92c..6b5d423 100644 --- a/modules/commands/sports_command.py +++ b/modules/commands/sports_command.py @@ -113,13 +113,10 @@ class SportsCommand(BaseCommand): if not self.keywords: return False - # Strip exclamation mark if present (for command-style messages) - content = message.content.strip() - if content.startswith('!'): - content = content[1:].strip() - + content_lower = self.cleanup_message_for_matching(message) + # Split into words and check if first word matches any keyword - words = content.split() + words = content_lower.split() if not words: return False diff --git a/modules/commands/trace_command.py b/modules/commands/trace_command.py index f0c5f4d..72cf29b 100644 --- a/modules/commands/trace_command.py +++ b/modules/commands/trace_command.py @@ -60,13 +60,11 @@ class TraceCommand(BaseCommand): ) def matches_keyword(self, message: MeshMessage) -> bool: - content = message.content.strip() - if content.startswith("!"): - content = content[1:].strip() - c = content.lower() - if c == "trace" or c == "tracer": + content_lower = self.cleanup_message_for_matching(message) + + if content_lower == "trace" or content_lower == "tracer": return True - if c.startswith("trace ") or c.startswith("tracer "): + if content_lower.startswith("trace ") or content_lower.startswith("tracer "): return True return False diff --git a/modules/commands/webviewer_command.py b/modules/commands/webviewer_command.py index 6d08250..9e8f666 100644 --- a/modules/commands/webviewer_command.py +++ b/modules/commands/webviewer_command.py @@ -58,14 +58,9 @@ class WebViewerCommand(BaseCommand): Returns: bool: True if matches, False otherwise. """ - content = message.content.strip() - - # Handle exclamation prefix - if content.startswith('!'): - content = content[1:].strip() - + content_lower = self.cleanup_message_for_matching(message) + # Check if message starts with any of our keywords - content_lower = content.lower() for keyword in self.keywords: if content_lower.startswith(keyword + ' ') or content_lower == keyword: return True diff --git a/modules/commands/wx_command.py b/modules/commands/wx_command.py index f6b6ead..a513680 100644 --- a/modules/commands/wx_command.py +++ b/modules/commands/wx_command.py @@ -144,10 +144,8 @@ class WxCommand(BaseCommand): if self.delegate_command: return self.delegate_command.matches_keyword(message) - content = message.content.strip() - if content.startswith('!'): - content = content[1:].strip() - content_lower = content.lower() + content_lower = self.cleanup_message_for_matching(message) + for keyword in self.keywords: if content_lower.startswith(keyword + ' ') or content_lower == keyword: return True From 56be1e778400d7a748744143be0dc61b7e1347ac Mon Sep 17 00:00:00 2001 From: Chris Wiegand Date: Sat, 11 Apr 2026 15:49:41 -0600 Subject: [PATCH 2/3] actually fix the message's copy of content since some implementations pull further arguments from it --- modules/commands/base_command.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/commands/base_command.py b/modules/commands/base_command.py index f27d986..693a5e7 100644 --- a/modules/commands/base_command.py +++ b/modules/commands/base_command.py @@ -774,10 +774,11 @@ class BaseCommand(ABC): return "" # Strip @[username] mentions before checking keywords - content = self._strip_mentions(content) + # also puts back the message with the prefix or mentions removed for evaluating further + message.content = self._strip_mentions(content) + message.content_lower = message.content.lower() - content_lower = content.lower() - return content_lower + return message.content_lower def matches_keyword(self, message: MeshMessage) -> bool: """Check if this command matches the message content based on keywords. From 9d4b142071605775d09512beba88eba0cd809bbe Mon Sep 17 00:00:00 2001 From: Chris Wiegand Date: Sat, 11 Apr 2026 15:49:49 -0600 Subject: [PATCH 3/3] log from user, and if in channel or DM --- modules/message_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/message_handler.py b/modules/message_handler.py index 1aa4cfa..e2f2d37 100644 --- a/modules/message_handler.py +++ b/modules/message_handler.py @@ -2657,7 +2657,7 @@ class MessageHandler: if not self.should_process_message(message): return - self.logger.info(f"Processing message: {message.content}") + self.logger.info(f"Processing message: '{message.content}' from {message.sender_id} in {'DM' if message.is_dm else message.channel}") # Check for advert command (DM only) if message.is_dm and message.content.strip().lower() == "advert":