Remove dead channel/mention code — DM-only bot cleanup

MeshAI is now DM-only. Removed all unreachable channel response
paths, @mention detection, ChannelsConfig, and channel TUI menu.
Fixed restart mechanism with integrated watcher and SIGKILL fallback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ubuntu 2026-02-24 08:43:25 +00:00
commit 1e033316fb
7 changed files with 81 additions and 199 deletions

View file

@ -6,9 +6,8 @@
# === BOT IDENTITY === # === BOT IDENTITY ===
bot: bot:
name: ai # Bot's trigger name (users say "@ai help") name: ai # Bot's display name
owner: "" # Owner's callsign (optional) owner: "" # Owner's callsign (optional)
respond_to_mentions: true # Respond when name is mentioned
respond_to_dms: true # Respond to direct messages respond_to_dms: true # Respond to direct messages
filter_bbs_protocols: true # Ignore advBBS sync/notification messages filter_bbs_protocols: true # Ignore advBBS sync/notification messages
@ -19,12 +18,6 @@ connection:
tcp_host: localhost # For TCP connection (meshtasticd) tcp_host: localhost # For TCP connection (meshtasticd)
tcp_port: 4403 tcp_port: 4403
# === CHANNEL FILTERING ===
channels:
mode: all # all | whitelist
whitelist: # Only respond on these channels (if mode=whitelist)
- 0
# === RESPONSE BEHAVIOR === # === RESPONSE BEHAVIOR ===
response: response:
delay_min: 2.2 # Min delay before responding (seconds) delay_min: 2.2 # Min delay before responding (seconds)

View file

@ -15,7 +15,6 @@ if [ ! -f "$MESHAI_CONFIG" ]; then
bot: bot:
name: ai name: ai
owner: "" owner: ""
respond_to_mentions: true
respond_to_dms: true respond_to_dms: true
filter_bbs_protocols: true filter_bbs_protocols: true
@ -25,11 +24,6 @@ connection:
tcp_host: localhost tcp_host: localhost
tcp_port: 4403 tcp_port: 4403
channels:
mode: all
whitelist:
- 0
response: response:
delay_min: 2.2 delay_min: 2.2
delay_max: 3.0 delay_max: 3.0
@ -74,38 +68,50 @@ ttyd -W -p 7682 \
/bin/bash -c 'while true; do python3 -m meshai --config-file "$MESHAI_CONFIG" --config; sleep 1; done' & /bin/bash -c 'while true; do python3 -m meshai --config-file "$MESHAI_CONFIG" --config; sleep 1; done' &
# Keep ttyd running even if bot fails # Keep ttyd running even if bot fails
trap "kill %1 2>/dev/null; kill %2 2>/dev/null" EXIT trap "kill %1 2>/dev/null" EXIT
# Restart watcher - monitors for restart signal from config tool # Kill bot gracefully with SIGKILL fallback
BOT_PID_FILE="/tmp/meshai_bot.pid" kill_bot() {
( local pid=$1
while true; do if ! kill -0 "$pid" 2>/dev/null; then
if [ -f /tmp/meshai_restart ]; then return
rm -f /tmp/meshai_restart fi
echo "Restart signal received, restarting bot..." kill "$pid" 2>/dev/null || true
# Kill bot using PID file echo "Sent SIGTERM to bot (PID $pid)"
if [ -f "$BOT_PID_FILE" ]; then # Wait up to 5 seconds for graceful shutdown
BOT_PID=$(cat "$BOT_PID_FILE") for i in 1 2 3 4 5; do
if kill -0 "$BOT_PID" 2>/dev/null; then kill -0 "$pid" 2>/dev/null || return
kill "$BOT_PID" 2>/dev/null || true sleep 1
echo "Sent TERM to bot (PID $BOT_PID)"
fi
fi
# Debounce - wait before checking for more signals
sleep 3
fi
sleep 2
done done
) & # Force kill if still alive
if kill -0 "$pid" 2>/dev/null; then
kill -9 "$pid" 2>/dev/null || true
echo "Sent SIGKILL to bot (PID $pid)"
fi
}
# Start the bot in a loop - retry on failure # Start the bot in a loop with integrated restart watcher
echo "Starting MeshAI..." echo "Starting MeshAI..."
rm -f /tmp/meshai_restart
while true; do while true; do
python -m meshai --config-file "$MESHAI_CONFIG" & python -m meshai --config-file "$MESHAI_CONFIG" &
BOT_PID=$! BOT_PID=$!
echo "$BOT_PID" > "$BOT_PID_FILE" echo "$BOT_PID" > /tmp/meshai.pid
wait $BOT_PID || true echo "Bot started (PID $BOT_PID)"
rm -f "$BOT_PID_FILE"
echo "Bot exited. Check config at http://localhost:7682. Retrying in 5s..." # Poll: wait for bot to exit OR restart signal
sleep 5 while kill -0 $BOT_PID 2>/dev/null; do
if [ -f /tmp/meshai_restart ]; then
rm -f /tmp/meshai_restart
echo "Restart signal received, restarting bot..."
kill_bot $BOT_PID
break
fi
sleep 1
done
wait $BOT_PID 2>/dev/null || true
rm -f /tmp/meshai.pid
echo "Bot exited. Restarting in 3s..."
sleep 3
done done

View file

@ -69,15 +69,14 @@ class Configurator:
disabled_count = len(self.config.commands.disabled_commands) disabled_count = len(self.config.commands.disabled_commands)
cmd_status = f"{disabled_count} disabled" if disabled_count else "all enabled" cmd_status = f"{disabled_count} disabled" if disabled_count else "all enabled"
table.add_row("1", "Bot Settings", f"@{self.config.bot.name}") table.add_row("1", "Bot Settings", self.config.bot.name)
table.add_row("2", "Connection", f"{self.config.connection.type}") table.add_row("2", "Connection", f"{self.config.connection.type}")
table.add_row("3", "LLM Backend", f"{self.config.llm.backend}/{self.config.llm.model}") table.add_row("3", "LLM Backend", f"{self.config.llm.backend}/{self.config.llm.model}")
table.add_row("4", "Response Settings", f"{self.config.response.max_length}ch max") table.add_row("4", "Response Settings", f"{self.config.response.max_length}ch max")
table.add_row("5", "Channels", f"{self.config.channels.mode}") table.add_row("5", "History & Memory", f"{self.config.history.max_messages_per_user} msgs")
table.add_row("6", "History & Memory", f"{self.config.history.max_messages_per_user} msgs") table.add_row("6", "Commands", cmd_status)
table.add_row("7", "Commands", cmd_status) table.add_row("7", "Weather", f"{self.config.weather.primary}")
table.add_row("8", "Weather", f"{self.config.weather.primary}") table.add_row("8", "Setup Wizard", "[dim]First-time setup[/dim]")
table.add_row("9", "Setup Wizard", "[dim]First-time setup[/dim]")
console.print(table) console.print(table)
console.print() console.print()
@ -86,13 +85,13 @@ class Configurator:
if self.modified: if self.modified:
console.print("[yellow]* Unsaved changes[/yellow]") console.print("[yellow]* Unsaved changes[/yellow]")
console.print() console.print()
console.print("[white]10. Save[/white] [dim]Save config, stay in menu[/dim]") console.print("[white] 9. Save[/white] [dim]Save config, stay in menu[/dim]")
console.print("[green]11. Save & Restart Bot[/green] [dim]Apply changes now[/dim]") console.print("[green]10. Save & Restart Bot[/green] [dim]Apply changes now[/dim]")
console.print("[white]12. Save & Exit[/white] [dim]Save, restart bot, exit[/dim]") console.print("[white]11. Save & Exit[/white] [dim]Save, restart bot, exit[/dim]")
console.print("[white]13. Exit without Saving[/white]") console.print("[white]12. Exit without Saving[/white]")
console.print() console.print()
choice = IntPrompt.ask("Select option", default=11) choice = IntPrompt.ask("Select option", default=10)
if choice == 1: if choice == 1:
self._bot_settings() self._bot_settings()
@ -103,23 +102,21 @@ class Configurator:
elif choice == 4: elif choice == 4:
self._response_settings() self._response_settings()
elif choice == 5: elif choice == 5:
self._channel_settings()
elif choice == 6:
self._history_settings() self._history_settings()
elif choice == 7: elif choice == 6:
self._command_settings() self._command_settings()
elif choice == 8: elif choice == 7:
self._weather_settings() self._weather_settings()
elif choice == 9: elif choice == 8:
self._setup_wizard() self._setup_wizard()
elif choice == 10: elif choice == 9:
self._save_only() self._save_only()
elif choice == 11: elif choice == 10:
self._save_and_restart() self._save_and_restart()
elif choice == 12: elif choice == 11:
self._save_restart_exit() self._save_restart_exit()
break break
elif choice == 13: elif choice == 12:
break break
def _show_header(self) -> None: def _show_header(self) -> None:
@ -148,18 +145,13 @@ class Configurator:
table.add_column("Setting", style="white") table.add_column("Setting", style="white")
table.add_column("Value", style="green") table.add_column("Value", style="green")
table.add_row("1", "Bot Name (@mention)", self.config.bot.name) table.add_row("1", "Bot Name", self.config.bot.name)
table.add_row("2", "Owner", self.config.bot.owner or "[dim]not set[/dim]") table.add_row("2", "Owner", self.config.bot.owner or "[dim]not set[/dim]")
table.add_row( table.add_row(
"3", "3", "Respond to DMs", self._status_icon(self.config.bot.respond_to_dms)
"Respond to @mentions",
self._status_icon(self.config.bot.respond_to_mentions),
) )
table.add_row( table.add_row(
"4", "Respond to DMs", self._status_icon(self.config.bot.respond_to_dms) "4", "Filter BBS Protocols", self._status_icon(self.config.bot.filter_bbs_protocols)
)
table.add_row(
"5", "Filter BBS Protocols", self._status_icon(self.config.bot.filter_bbs_protocols)
) )
table.add_row("0", "Back", "") table.add_row("0", "Back", "")
@ -181,18 +173,11 @@ class Configurator:
self.config.bot.owner = value self.config.bot.owner = value
self.modified = True self.modified = True
elif choice == 3: elif choice == 3:
value = Confirm.ask(
"Respond to @mentions?", default=self.config.bot.respond_to_mentions
)
if value != self.config.bot.respond_to_mentions:
self.config.bot.respond_to_mentions = value
self.modified = True
elif choice == 4:
value = Confirm.ask("Respond to DMs?", default=self.config.bot.respond_to_dms) value = Confirm.ask("Respond to DMs?", default=self.config.bot.respond_to_dms)
if value != self.config.bot.respond_to_dms: if value != self.config.bot.respond_to_dms:
self.config.bot.respond_to_dms = value self.config.bot.respond_to_dms = value
self.modified = True self.modified = True
elif choice == 5: elif choice == 4:
value = Confirm.ask("Filter BBS protocols?", default=self.config.bot.filter_bbs_protocols) value = Confirm.ask("Filter BBS protocols?", default=self.config.bot.filter_bbs_protocols)
if value != self.config.bot.filter_bbs_protocols: if value != self.config.bot.filter_bbs_protocols:
self.config.bot.filter_bbs_protocols = value self.config.bot.filter_bbs_protocols = value
@ -478,49 +463,6 @@ class Configurator:
self.config.response.max_messages = value self.config.response.max_messages = value
self.modified = True self.modified = True
def _channel_settings(self) -> None:
"""Channel filtering settings submenu."""
while True:
self._clear()
console.print("[bold]Channel Filtering[/bold]\n")
table = Table(box=box.ROUNDED)
table.add_column("Option", style="cyan", width=4)
table.add_column("Setting", style="white")
table.add_column("Value", style="green")
whitelist_str = ", ".join(str(c) for c in self.config.channels.whitelist)
table.add_row("1", "Mode", self.config.channels.mode)
table.add_row("2", "Whitelist Channels", whitelist_str or "[dim]none[/dim]")
table.add_row("0", "Back", "")
console.print(table)
console.print()
choice = IntPrompt.ask("Select option", default=0)
if choice == 0:
return
elif choice == 1:
console.print("\n[cyan]1.[/cyan] all - Respond on all channels")
console.print("[cyan]2.[/cyan] whitelist - Only respond on specific channels")
sel = IntPrompt.ask("Select", default=1 if self.config.channels.mode == "all" else 2)
value = "all" if sel == 1 else "whitelist"
if value != self.config.channels.mode:
self.config.channels.mode = value
self.modified = True
elif choice == 2:
value = Prompt.ask(
"Whitelist (comma-separated)", default=whitelist_str
)
try:
channels = [int(c.strip()) for c in value.split(",") if c.strip()]
if channels != self.config.channels.whitelist:
self.config.channels.whitelist = channels
self.modified = True
except ValueError:
console.print("[red]Invalid input. Use comma-separated numbers.[/red]")
def _history_settings(self) -> None: def _history_settings(self) -> None:
"""History settings submenu.""" """History settings submenu."""
while True: while True:
@ -603,7 +545,7 @@ class Configurator:
# Step 1: Bot identity # Step 1: Bot identity
console.print("[bold cyan]Step 1: Bot Identity[/bold cyan]") console.print("[bold cyan]Step 1: Bot Identity[/bold cyan]")
self.config.bot.name = Prompt.ask("Bot name (for @mentions)", default="ai") self.config.bot.name = Prompt.ask("Bot name", default="ai")
self.config.bot.owner = Prompt.ask("Your name/callsign", default="") self.config.bot.owner = Prompt.ask("Your name/callsign", default="")
console.print() console.print()

View file

@ -17,7 +17,6 @@ class BotConfig:
name: str = "ai" name: str = "ai"
owner: str = "" owner: str = ""
respond_to_mentions: bool = True
respond_to_dms: bool = True respond_to_dms: bool = True
filter_bbs_protocols: bool = True filter_bbs_protocols: bool = True
@ -32,14 +31,6 @@ class ConnectionConfig:
tcp_port: int = 4403 tcp_port: int = 4403
@dataclass
class ChannelsConfig:
"""Channel filtering settings."""
mode: str = "all" # all or whitelist
whitelist: list[int] = field(default_factory=lambda: [0])
@dataclass @dataclass
class ResponseConfig: class ResponseConfig:
"""Response behavior settings.""" """Response behavior settings."""
@ -134,7 +125,6 @@ class Config:
bot: BotConfig = field(default_factory=BotConfig) bot: BotConfig = field(default_factory=BotConfig)
connection: ConnectionConfig = field(default_factory=ConnectionConfig) connection: ConnectionConfig = field(default_factory=ConnectionConfig)
channels: ChannelsConfig = field(default_factory=ChannelsConfig)
response: ResponseConfig = field(default_factory=ResponseConfig) response: ResponseConfig = field(default_factory=ResponseConfig)
history: HistoryConfig = field(default_factory=HistoryConfig) history: HistoryConfig = field(default_factory=HistoryConfig)
memory: MemoryConfig = field(default_factory=MemoryConfig) memory: MemoryConfig = field(default_factory=MemoryConfig)

View file

@ -174,22 +174,12 @@ class MeshAI:
if not response: if not response:
return return
# Send response # Send DM response
if message.is_dm: await self.responder.send_response(
await self.responder.send_response( text=response,
text=response, destination=message.sender_id,
destination=message.sender_id, channel=message.channel,
channel=message.channel, )
)
else:
formatted = self.responder.format_channel_response(
response, message.sender_name, mention_sender=True
)
await self.responder.send_response(
text=formatted,
destination=None,
channel=message.channel,
)
except Exception as e: except Exception as e:
logger.error(f"Error handling message: {e}", exc_info=True) logger.error(f"Error handling message: {e}", exc_info=True)

View file

@ -137,24 +137,3 @@ class Responder:
return pos return pos
return 0 return 0
def format_channel_response(
self, text: str, sender_name: str, mention_sender: bool = False
) -> str:
"""Format response for channel context.
Args:
text: Response text
sender_name: Name of sender being replied to
mention_sender: Whether to prefix with sender's name
Returns:
Formatted response
"""
if mention_sender:
# Check if adding prefix would exceed max length
prefix = f"@{sender_name}: "
if len(prefix) + len(text) <= self.config.max_length * self.config.max_messages:
return prefix + text
return text

View file

@ -68,13 +68,13 @@ class MessageRouter:
self.dispatcher = dispatcher self.dispatcher = dispatcher
self.llm = llm_backend self.llm = llm_backend
# Compile mention pattern
bot_name = re.escape(config.bot.name)
self._mention_pattern = re.compile(rf"@{bot_name}\b", re.IGNORECASE)
def should_respond(self, message: MeshMessage) -> bool: def should_respond(self, message: MeshMessage) -> bool:
"""Determine if we should respond to this message. """Determine if we should respond to this message.
DM-only bot: ignores all public channel messages.
Commands and conversational LLM responses both work in DMs.
Args: Args:
message: Incoming message message: Incoming message
@ -85,35 +85,20 @@ class MessageRouter:
if message.sender_id == self.connector.my_node_id: if message.sender_id == self.connector.my_node_id:
return False return False
# Only respond to DMs
if not message.is_dm:
return False
if not self.config.bot.respond_to_dms:
return False
# Ignore advBBS protocol and notification messages # Ignore advBBS protocol and notification messages
if self.config.bot.filter_bbs_protocols: if self.config.bot.filter_bbs_protocols:
if any(message.text.startswith(p) for p in ADVBBS_PREFIXES): if any(message.text.startswith(p) for p in ADVBBS_PREFIXES):
logger.debug(f"Ignoring advBBS message from {message.sender_id}: {message.text[:40]}...") logger.debug(f"Ignoring advBBS message from {message.sender_id}: {message.text[:40]}...")
return False return False
# Check if DM — conversational mode only, skip !commands return True
# (let MeshMonitor or other bots handle bang commands in DMs)
if message.is_dm:
if self.dispatcher.is_command(message.text):
return False
return self.config.bot.respond_to_dms
# Check channel filtering
if self.config.channels.mode == "whitelist":
if message.channel not in self.config.channels.whitelist:
return False
# Check for @mention
if self.config.bot.respond_to_mentions:
if self._mention_pattern.search(message.text):
return True
# Check for bang command (always respond to commands)
if self.dispatcher.is_command(message.text):
return True
# Not a DM, no mention, no command - ignore
return False
async def route(self, message: MeshMessage) -> RouteResult: async def route(self, message: MeshMessage) -> RouteResult:
"""Route a message and generate response. """Route a message and generate response.
@ -200,11 +185,8 @@ class MessageRouter:
logger.debug(f"Persisted summary for {user_id}") logger.debug(f"Persisted summary for {user_id}")
def _clean_query(self, text: str) -> str: def _clean_query(self, text: str) -> str:
"""Remove @mention and check for prompt injection.""" """Clean up query text and check for prompt injection."""
# Remove @botname mention cleaned = " ".join(text.split())
cleaned = self._mention_pattern.sub("", text)
# Clean up extra whitespace
cleaned = " ".join(cleaned.split())
cleaned = cleaned.strip() cleaned = cleaned.strip()
# Check for prompt injection # Check for prompt injection