Remove dead modules: safety, rate_limiter, personality, webhook, web_status, announcements, log_setup

These modules were wired up but never actually functional in the
running bot. Strips all imports and usage from main.py and router.py.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ubuntu 2026-02-24 00:23:46 +00:00
commit 10bc94b273
9 changed files with 22 additions and 1234 deletions

View file

@ -1,109 +0,0 @@
"""Periodic announcements/broadcasts for MeshAI."""
import asyncio
import logging
import random
from typing import Awaitable, Callable, Optional
from .config import AnnouncementsConfig
logger = logging.getLogger(__name__)
class AnnouncementScheduler:
"""Scheduler for periodic announcements."""
def __init__(
self,
config: AnnouncementsConfig,
send_callback: Callable[[str, int], Awaitable[None]],
):
"""Initialize the announcement scheduler.
Args:
config: Announcements configuration
send_callback: Async callback to send messages: (text, channel) -> None
"""
self.config = config
self._send_callback = send_callback
self._task: Optional[asyncio.Task] = None
self._message_index = 0
self._running = False
async def start(self):
"""Start the announcement scheduler."""
if not self.config.enabled or not self.config.messages:
logger.debug("Announcements disabled or no messages configured")
return
self._running = True
self._task = asyncio.create_task(self._run_loop())
logger.info(
f"Announcement scheduler started (every {self.config.interval_hours}h)"
)
async def stop(self):
"""Stop the announcement scheduler."""
self._running = False
if self._task:
self._task.cancel()
try:
await self._task
except asyncio.CancelledError:
pass
self._task = None
logger.info("Announcement scheduler stopped")
async def _run_loop(self):
"""Main loop for sending periodic announcements."""
# Wait a bit before first announcement
await asyncio.sleep(60) # 1 minute initial delay
while self._running:
try:
# Get next message
message = self._get_next_message()
if message:
logger.info(f"Sending announcement to channel {self.config.channel}")
await self._send_callback(message, self.config.channel)
# Wait for next interval
await asyncio.sleep(self.config.interval_hours * 3600)
except asyncio.CancelledError:
break
except Exception as e:
logger.error(f"Error in announcement loop: {e}")
await asyncio.sleep(300) # Wait 5 min on error
def _get_next_message(self) -> Optional[str]:
"""Get the next announcement message."""
if not self.config.messages:
return None
if self.config.random_order:
return random.choice(self.config.messages)
else:
message = self.config.messages[self._message_index]
self._message_index = (self._message_index + 1) % len(self.config.messages)
return message
async def send_now(self, message: Optional[str] = None) -> bool:
"""Send an announcement immediately.
Args:
message: Optional specific message, or use next in rotation
Returns:
True if sent successfully
"""
msg = message or self._get_next_message()
if not msg:
return False
try:
await self._send_callback(msg, self.config.channel)
return True
except Exception as e:
logger.error(f"Failed to send announcement: {e}")
return False

View file

@ -1,122 +0,0 @@
"""Enhanced logging setup for MeshAI."""
import logging
import sys
from logging.handlers import RotatingFileHandler
from pathlib import Path
from typing import Optional
from .config import LoggingConfig
# Custom log levels for message tracking
MESSAGE_IN = 25 # Between INFO (20) and WARNING (30)
MESSAGE_OUT = 26
API_CALL = 15 # Between DEBUG (10) and INFO (20)
logging.addLevelName(MESSAGE_IN, "MSG_IN")
logging.addLevelName(MESSAGE_OUT, "MSG_OUT")
logging.addLevelName(API_CALL, "API")
class MeshAILogger(logging.Logger):
"""Custom logger with message tracking methods."""
def message_in(self, sender: str, text: str, channel: int = 0):
"""Log an incoming message."""
if self.isEnabledFor(MESSAGE_IN):
self._log(MESSAGE_IN, f"[CH{channel}] {sender}: {text}", ())
def message_out(self, recipient: str, text: str, channel: int = 0):
"""Log an outgoing message."""
if self.isEnabledFor(MESSAGE_OUT):
self._log(MESSAGE_OUT, f"[CH{channel}] -> {recipient}: {text}", ())
def api_call(self, backend: str, model: str, tokens: Optional[int] = None):
"""Log an API call."""
if self.isEnabledFor(API_CALL):
msg = f"API call to {backend}/{model}"
if tokens:
msg += f" ({tokens} tokens)"
self._log(API_CALL, msg, ())
# Set the custom logger class
logging.setLoggerClass(MeshAILogger)
def setup_logging(config: LoggingConfig, verbose: bool = False) -> logging.Logger:
"""Configure logging based on config.
Args:
config: Logging configuration
verbose: Override to enable DEBUG level
Returns:
The configured root logger
"""
# Determine log level
if verbose:
level = logging.DEBUG
else:
level_name = config.level.upper()
level = getattr(logging, level_name, logging.INFO)
# Create formatter
formatter = logging.Formatter(
fmt="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
# Get root logger
root_logger = logging.getLogger()
root_logger.setLevel(level)
# Clear existing handlers
root_logger.handlers.clear()
# Console handler (always)
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(level)
console_handler.setFormatter(formatter)
root_logger.addHandler(console_handler)
# File handler (if configured)
if config.file:
log_path = Path(config.file)
log_path.parent.mkdir(parents=True, exist_ok=True)
file_handler = RotatingFileHandler(
log_path,
maxBytes=config.max_size_mb * 1024 * 1024,
backupCount=config.backup_count,
)
file_handler.setLevel(level)
file_handler.setFormatter(formatter)
root_logger.addHandler(file_handler)
# Configure message logging levels based on config
meshai_logger = logging.getLogger("meshai")
if not config.log_messages:
# Disable message logging
meshai_logger.addFilter(lambda r: r.levelno not in (MESSAGE_IN, MESSAGE_OUT))
if not config.log_api_calls:
# Disable API call logging (it's DEBUG level anyway)
meshai_logger.addFilter(lambda r: r.levelno != API_CALL)
return root_logger
def get_logger(name: str = "meshai") -> MeshAILogger:
"""Get a MeshAI logger instance.
Args:
name: Logger name (will be prefixed with 'meshai.')
Returns:
Configured logger instance
"""
if not name.startswith("meshai"):
name = f"meshai.{name}"
return logging.getLogger(name)

View file

@ -11,8 +11,7 @@ from pathlib import Path
from typing import Optional from typing import Optional
from . import __version__ from . import __version__
from .announcements import AnnouncementScheduler from .backends import AnthropicBackend, GoogleBackend, LLMBackend, OpenAIBackend
from .backends import AnthropicBackend, FallbackBackend, GoogleBackend, LLMBackend, OpenAIBackend
from .cli import run_configurator from .cli import run_configurator
from .commands import CommandDispatcher from .commands import CommandDispatcher
from .commands.dispatcher import create_dispatcher from .commands.dispatcher import create_dispatcher
@ -21,13 +20,8 @@ from .config import Config, load_config
from .connector import MeshConnector, MeshMessage from .connector import MeshConnector, MeshMessage
from .history import ConversationHistory from .history import ConversationHistory
from .memory import ConversationSummary from .memory import ConversationSummary
from .personality import PersonalityManager
from .rate_limiter import RateLimiter
from .responder import Responder from .responder import Responder
from .router import MessageRouter, RouteType from .router import MessageRouter, RouteType
from .safety import SafetyFilter, UserFilter
from .web_status import WebStatusServer, get_status_data
from .webhook import WebhookClient
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -43,13 +37,6 @@ class MeshAI:
self.llm: Optional[LLMBackend] = None self.llm: Optional[LLMBackend] = None
self.router: Optional[MessageRouter] = None self.router: Optional[MessageRouter] = None
self.responder: Optional[Responder] = None self.responder: Optional[Responder] = None
self.personality: Optional[PersonalityManager] = None
self.safety_filter: Optional[SafetyFilter] = None
self.user_filter: Optional[UserFilter] = None
self.rate_limiter: Optional[RateLimiter] = None
self.webhook: Optional[WebhookClient] = None
self.web_status: Optional[WebStatusServer] = None
self.announcements: Optional[AnnouncementScheduler] = None
self._running = False self._running = False
self._loop: Optional[asyncio.AbstractEventLoop] = None self._loop: Optional[asyncio.AbstractEventLoop] = None
self._last_cleanup: float = 0.0 self._last_cleanup: float = 0.0
@ -70,14 +57,6 @@ class MeshAI:
self._loop = asyncio.get_event_loop() self._loop = asyncio.get_event_loop()
self._last_cleanup = time.time() self._last_cleanup = time.time()
# Start async services
await self.webhook.start()
await self.webhook.on_startup()
await self.announcements.start()
# Start sync services
self.web_status.start()
# Write PID file # Write PID file
self._write_pid() self._write_pid()
@ -97,16 +76,6 @@ class MeshAI:
logger.info("Stopping MeshAI...") logger.info("Stopping MeshAI...")
self._running = False self._running = False
if self.webhook:
await self.webhook.on_shutdown()
await self.webhook.stop()
if self.announcements:
await self.announcements.stop()
if self.web_status:
self.web_status.stop()
if self.connector: if self.connector:
self.connector.disconnect() self.connector.disconnect()
@ -125,30 +94,8 @@ class MeshAI:
self.history = ConversationHistory(self.config.history) self.history = ConversationHistory(self.config.history)
await self.history.initialize() await self.history.initialize()
# Command dispatcher (2h: pass config) # Command dispatcher
self.dispatcher = create_dispatcher( self.dispatcher = create_dispatcher()
prefix=self.config.commands.prefix,
disabled_commands=self.config.commands.disabled_commands,
custom_commands=self.config.commands.custom_commands,
)
# Safety and user filters (2a)
self.user_filter = UserFilter(
blocklist=self.config.users.blocklist,
allowlist=self.config.users.allowlist,
allowlist_only=self.config.users.allowlist_only,
admin_nodes=self.config.users.admin_nodes,
)
self.safety_filter = SafetyFilter(self.config.safety)
# Rate limiter (2b)
self.rate_limiter = RateLimiter(
self.config.rate_limits,
vip_nodes=self.config.users.vip_nodes,
)
# Personality manager (2c)
self.personality = PersonalityManager(self.config.personality)
# LLM backend # LLM backend
api_key = self.config.resolve_api_key() api_key = self.config.resolve_api_key()
@ -160,100 +107,52 @@ class MeshAI:
window_size = mem_cfg.window_size if mem_cfg.enabled else 0 window_size = mem_cfg.window_size if mem_cfg.enabled else 0
summarize_threshold = mem_cfg.summarize_threshold summarize_threshold = mem_cfg.summarize_threshold
# Create primary backend # Create backend
backend = self.config.llm.backend.lower() backend = self.config.llm.backend.lower()
if backend == "openai": if backend == "openai":
primary = OpenAIBackend( self.llm = OpenAIBackend(
self.config.llm, api_key, window_size, summarize_threshold self.config.llm, api_key, window_size, summarize_threshold
) )
elif backend == "anthropic": elif backend == "anthropic":
primary = AnthropicBackend( self.llm = AnthropicBackend(
self.config.llm, api_key, window_size, summarize_threshold self.config.llm, api_key, window_size, summarize_threshold
) )
elif backend == "google": elif backend == "google":
primary = GoogleBackend( self.llm = GoogleBackend(
self.config.llm, api_key, window_size, summarize_threshold self.config.llm, api_key, window_size, summarize_threshold
) )
else: else:
logger.warning(f"Unknown backend '{backend}', defaulting to OpenAI") logger.warning(f"Unknown backend '{backend}', defaulting to OpenAI")
primary = OpenAIBackend( self.llm = OpenAIBackend(
self.config.llm, api_key, window_size, summarize_threshold self.config.llm, api_key, window_size, summarize_threshold
) )
# Wrap in FallbackBackend if fallback is configured (2g)
if self.config.llm.fallback:
self.llm = FallbackBackend(
self.config.llm, api_key, window_size, summarize_threshold
)
else:
self.llm = primary
# Load persisted summaries into memory cache # Load persisted summaries into memory cache
await self._load_summaries() await self._load_summaries()
# Meshtastic connector # Meshtastic connector
self.connector = MeshConnector(self.config.connection) self.connector = MeshConnector(self.config.connection)
# Message router (pass personality manager) # Message router
self.router = MessageRouter( self.router = MessageRouter(
self.config, self.connector, self.history, self.dispatcher, self.llm, self.config, self.connector, self.history, self.dispatcher, self.llm,
personality=self.personality,
) )
# Responder # Responder
self.responder = Responder(self.config.response, self.connector) self.responder = Responder(self.config.response, self.connector)
# Webhook client (2d)
self.webhook = WebhookClient(self.config.integrations.webhook)
# Web status server (2e)
self.web_status = WebStatusServer(self.config.web_status)
# Announcement scheduler (2f)
async def _send_announcement(text: str, channel: int) -> None:
self.connector.send_message(text=text, channel=channel)
self.announcements = AnnouncementScheduler(
self.config.announcements,
send_callback=_send_announcement,
)
async def _on_message(self, message: MeshMessage) -> None: async def _on_message(self, message: MeshMessage) -> None:
"""Handle incoming message.""" """Handle incoming message."""
try: try:
# Check user filter (2a)
allowed, reason = self.user_filter.is_allowed(message.sender_id)
if not allowed:
logger.debug(f"Blocked message from {message.sender_id}: {reason}")
return
# Check if we should respond # Check if we should respond
if not self.router.should_respond(message): if not self.router.should_respond(message):
return return
# Check rate limiter (2b)
allowed, reason = self.rate_limiter.is_allowed(message.sender_id)
if not allowed:
logger.debug(f"Rate limited {message.sender_id}: {reason}")
return
logger.info( logger.info(
f"Processing message from {message.sender_name} ({message.sender_id}): " f"Processing message from {message.sender_name} ({message.sender_id}): "
f"{message.text[:50]}..." f"{message.text[:50]}..."
) )
# Record in web status (2e)
get_status_data().record_message(message.sender_id, message.sender_name)
# Send webhook event (2d)
await self.webhook.on_message_received(
sender_id=message.sender_id,
sender_name=message.sender_name,
text=message.text,
channel=message.channel,
is_dm=message.is_dm,
)
# Route the message # Route the message
result = await self.router.route(message) result = await self.router.route(message)
@ -271,10 +170,6 @@ class MeshAI:
if not response: if not response:
return return
# Apply safety filter to LLM responses (2a)
if result.route_type == RouteType.LLM:
response = self.safety_filter.filter_response(response)
# Send response # Send response
if message.is_dm: if message.is_dm:
await self.responder.send_response( await self.responder.send_response(
@ -292,21 +187,8 @@ class MeshAI:
channel=message.channel, channel=message.channel,
) )
# Record response in rate limiter and status (2b, 2e)
self.rate_limiter.record_message(message.sender_id)
get_status_data().record_response()
# Send webhook event (2d)
await self.webhook.on_response_sent(
recipient_id=message.sender_id if message.is_dm else None,
text=response,
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)
get_status_data().record_error(str(e))
await self.webhook.on_error(str(e))
async def _load_summaries(self) -> None: async def _load_summaries(self) -> None:
"""Load persisted summaries from database into memory cache.""" """Load persisted summaries from database into memory cache."""
@ -417,14 +299,6 @@ def main() -> None:
signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGTERM, signal_handler)
# Handle SIGHUP for config reload
def reload_handler(sig, frame):
logger.info("Received SIGHUP - reloading config")
# For now, just log - full reload would require more work
# Could reload config and reinitialize components
signal.signal(signal.SIGHUP, reload_handler)
try: try:
loop.run_until_complete(bot.start()) loop.run_until_complete(bot.start())
except KeyboardInterrupt: except KeyboardInterrupt:

View file

@ -1,119 +0,0 @@
"""Personality and prompt template handling for MeshAI."""
import re
from datetime import datetime
from typing import Optional
from .config import PersonalityConfig
class PersonalityManager:
"""Manages personality switching and prompt templating."""
def __init__(self, config: PersonalityConfig):
self.config = config
self._current_persona: Optional[str] = None
self._persona_prompts: dict[str, str] = {}
# Parse personas from config
for name, persona_data in config.personas.items():
if isinstance(persona_data, dict):
self._persona_prompts[name] = persona_data.get("prompt", "")
else:
self._persona_prompts[name] = str(persona_data)
def get_system_prompt(
self,
sender_name: str = "",
channel: int = 0,
extra_context: Optional[dict] = None,
) -> str:
"""Get the current system prompt with context injection.
Args:
sender_name: Name of the message sender
channel: Channel number
extra_context: Additional context variables
Returns:
Formatted system prompt
"""
# Start with base prompt or persona prompt
if self._current_persona and self._current_persona in self._persona_prompts:
base_prompt = self._persona_prompts[self._current_persona]
else:
base_prompt = self.config.system_prompt
# Apply context injection if configured
if self.config.context_injection:
context_vars = {
"time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"sender_name": sender_name,
"channel": str(channel),
"persona": self._current_persona or "default",
}
if extra_context:
context_vars.update(extra_context)
try:
context = self.config.context_injection.format(**context_vars)
base_prompt = f"{base_prompt}\n\n{context}"
except KeyError as e:
# Ignore missing context variables
pass
return base_prompt
def check_persona_trigger(self, text: str) -> Optional[str]:
"""Check if text contains a persona switch trigger.
Args:
text: Message text to check
Returns:
Persona name if triggered, None otherwise
"""
text_lower = text.lower().strip()
for name, persona_data in self.config.personas.items():
trigger = None
if isinstance(persona_data, dict):
trigger = persona_data.get("trigger", f"!{name}")
else:
trigger = f"!{name}"
if trigger and text_lower.startswith(trigger.lower()):
return name
return None
def switch_persona(self, persona_name: Optional[str]) -> bool:
"""Switch to a different persona.
Args:
persona_name: Name of persona to switch to, or None for default
Returns:
True if switch was successful
"""
if persona_name is None:
self._current_persona = None
return True
if persona_name in self._persona_prompts:
self._current_persona = persona_name
return True
return False
def get_current_persona(self) -> Optional[str]:
"""Get the name of the current persona."""
return self._current_persona
def list_personas(self) -> list[str]:
"""List available persona names."""
return list(self._persona_prompts.keys())
def reset(self) -> None:
"""Reset to default persona."""
self._current_persona = None

View file

@ -1,115 +0,0 @@
"""Rate limiting for MeshAI."""
import time
from collections import defaultdict
from dataclasses import dataclass, field
from typing import Optional
from .config import RateLimitsConfig
@dataclass
class UserRateState:
"""Rate limit state for a single user."""
message_times: list[float] = field(default_factory=list)
last_response_time: float = 0.0
burst_count: int = 0
class RateLimiter:
"""Rate limiter for message processing."""
def __init__(self, config: RateLimitsConfig, vip_nodes: Optional[list[str]] = None):
self.config = config
self.vip_nodes = set(vip_nodes or [])
self._user_states: dict[str, UserRateState] = defaultdict(UserRateState)
self._global_times: list[float] = []
def is_allowed(self, user_id: str) -> tuple[bool, Optional[str]]:
"""Check if a message from user is allowed.
Args:
user_id: The user's node ID
Returns:
Tuple of (allowed, reason). If not allowed, reason explains why.
"""
# VIP users bypass rate limits
if user_id in self.vip_nodes:
return True, None
now = time.time()
state = self._user_states[user_id]
# Clean old timestamps (older than 1 minute)
cutoff = now - 60.0
state.message_times = [t for t in state.message_times if t > cutoff]
self._global_times = [t for t in self._global_times if t > cutoff]
# Check cooldown (minimum time between responses to same user)
if state.last_response_time > 0:
elapsed = now - state.last_response_time
if elapsed < self.config.cooldown_seconds:
remaining = self.config.cooldown_seconds - elapsed
return False, f"Cooldown: wait {remaining:.1f}s"
# Check per-user rate limit
if len(state.message_times) >= self.config.messages_per_minute:
# Check burst allowance
if state.burst_count >= self.config.burst_allowance:
return False, "Rate limit exceeded (per-user)"
state.burst_count += 1
else:
state.burst_count = 0
# Check global rate limit
if len(self._global_times) >= self.config.global_messages_per_minute:
return False, "Rate limit exceeded (global)"
return True, None
def record_message(self, user_id: str) -> None:
"""Record that a message was processed for a user."""
now = time.time()
state = self._user_states[user_id]
state.message_times.append(now)
state.last_response_time = now
self._global_times.append(now)
def get_user_stats(self, user_id: str) -> dict:
"""Get rate limit stats for a user."""
now = time.time()
state = self._user_states[user_id]
cutoff = now - 60.0
recent_count = len([t for t in state.message_times if t > cutoff])
return {
"messages_last_minute": recent_count,
"limit": self.config.messages_per_minute,
"remaining": max(0, self.config.messages_per_minute - recent_count),
"is_vip": user_id in self.vip_nodes,
}
def get_global_stats(self) -> dict:
"""Get global rate limit stats."""
now = time.time()
cutoff = now - 60.0
recent_count = len([t for t in self._global_times if t > cutoff])
return {
"messages_last_minute": recent_count,
"limit": self.config.global_messages_per_minute,
"remaining": max(0, self.config.global_messages_per_minute - recent_count),
}
def reset_user(self, user_id: str) -> None:
"""Reset rate limit state for a user."""
if user_id in self._user_states:
del self._user_states[user_id]
def reset_all(self) -> None:
"""Reset all rate limit state."""
self._user_states.clear()
self._global_times.clear()

View file

@ -11,7 +11,6 @@ from .commands import CommandContext, CommandDispatcher
from .config import Config from .config import Config
from .connector import MeshConnector, MeshMessage from .connector import MeshConnector, MeshMessage
from .history import ConversationHistory from .history import ConversationHistory
from .personality import PersonalityManager
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -62,14 +61,12 @@ class MessageRouter:
history: ConversationHistory, history: ConversationHistory,
dispatcher: CommandDispatcher, dispatcher: CommandDispatcher,
llm_backend: LLMBackend, llm_backend: LLMBackend,
personality: Optional[PersonalityManager] = None,
): ):
self.config = config self.config = config
self.connector = connector self.connector = connector
self.history = history self.history = history
self.dispatcher = dispatcher self.dispatcher = dispatcher
self.llm = llm_backend self.llm = llm_backend
self.personality = personality
# Compile mention pattern # Compile mention pattern
bot_name = re.escape(config.bot.name) bot_name = re.escape(config.bot.name)
@ -160,16 +157,10 @@ class MessageRouter:
# Get conversation history # Get conversation history
history = await self.history.get_history_for_llm(message.sender_id) history = await self.history.get_history_for_llm(message.sender_id)
# Get system prompt from personality manager or config # Get system prompt from config
system_prompt = "" system_prompt = ""
if getattr(self.config.llm, 'use_system_prompt', True): if getattr(self.config.llm, 'use_system_prompt', True):
if self.personality: system_prompt = self.config.llm.system_prompt
system_prompt = self.personality.get_system_prompt(
sender_name=message.sender_name,
channel=message.channel,
)
else:
system_prompt = self.config.llm.system_prompt
try: try:
response = await self.llm.generate( response = await self.llm.generate(
@ -217,19 +208,17 @@ class MessageRouter:
cleaned = " ".join(cleaned.split()) cleaned = " ".join(cleaned.split())
cleaned = cleaned.strip() cleaned = cleaned.strip()
# Check for prompt injection if guard is enabled # Check for prompt injection
if self.config.safety.prompt_injection_guard: for pattern in _INJECTION_PATTERNS:
for pattern in _INJECTION_PATTERNS: if pattern.search(cleaned):
if pattern.search(cleaned): logger.warning(
logger.warning( f"Possible prompt injection detected: {cleaned[:80]}..."
f"Possible prompt injection detected: {cleaned[:80]}..." )
) match = pattern.search(cleaned)
# Truncate to just the part before the injection pattern cleaned = cleaned[:match.start()].strip()
match = pattern.search(cleaned) if not cleaned:
cleaned = cleaned[:match.start()].strip() cleaned = "Hello"
if not cleaned: break
cleaned = "Hello"
break
return cleaned return cleaned

View file

@ -1,142 +0,0 @@
"""Response filtering and safety for MeshAI."""
import re
from typing import Optional
from .config import SafetyConfig
# Basic profanity list (expand as needed)
PROFANITY_PATTERNS = [
r"\bf+u+c+k+\w*\b",
r"\bs+h+i+t+\w*\b",
r"\ba+s+s+h+o+l+e+\w*\b",
r"\bb+i+t+c+h+\w*\b",
r"\bc+u+n+t+\w*\b",
r"\bd+a+m+n+\w*\b",
]
class SafetyFilter:
"""Filter for response safety and content moderation."""
def __init__(self, config: SafetyConfig):
self.config = config
self._profanity_regex = None
if config.filter_profanity:
self._profanity_regex = re.compile(
"|".join(PROFANITY_PATTERNS), re.IGNORECASE
)
def filter_response(self, text: str) -> str:
"""Filter a response for safety.
Args:
text: The response text to filter
Returns:
Filtered text
"""
# Truncate to max length
if len(text) > self.config.max_response_length:
text = text[: self.config.max_response_length - 3] + "..."
# Filter profanity
if self._profanity_regex:
text = self._profanity_regex.sub("***", text)
# Filter blocked phrases
for phrase in self.config.blocked_phrases:
text = text.replace(phrase, "[filtered]")
text = text.replace(phrase.lower(), "[filtered]")
text = text.replace(phrase.upper(), "[filtered]")
return text
def should_respond(
self,
text: str,
sender_id: str,
own_id: str,
is_mentioned: bool,
is_dm: bool,
) -> tuple[bool, Optional[str]]:
"""Check if we should respond to this message.
Args:
text: Message text
sender_id: Sender's node ID
own_id: Our own node ID
is_mentioned: Whether our name is mentioned
is_dm: Whether this is a direct message
Returns:
Tuple of (should_respond, reason). Reason is None if we should respond.
"""
# Never respond to self
if self.config.ignore_self and sender_id == own_id:
return False, "Self message"
# Check for emergency keywords (always respond)
text_lower = text.lower()
for keyword in self.config.emergency_keywords:
if keyword.lower() in text_lower:
return True, None
# Check mention requirement
if self.config.require_mention and not is_mentioned and not is_dm:
return False, "Not mentioned"
return True, None
def contains_emergency(self, text: str) -> bool:
"""Check if text contains emergency keywords."""
text_lower = text.lower()
return any(kw.lower() in text_lower for kw in self.config.emergency_keywords)
class UserFilter:
"""Filter for user access control."""
def __init__(
self,
blocklist: list[str],
allowlist: list[str],
allowlist_only: bool,
admin_nodes: list[str],
):
self.blocklist = set(blocklist)
self.allowlist = set(allowlist)
self.allowlist_only = allowlist_only
self.admin_nodes = set(admin_nodes)
def is_allowed(self, user_id: str) -> tuple[bool, Optional[str]]:
"""Check if user is allowed to interact.
Args:
user_id: The user's node ID
Returns:
Tuple of (allowed, reason)
"""
# Check blocklist first
if user_id in self.blocklist:
return False, "User is blocked"
# If allowlist_only mode, check allowlist
if self.allowlist_only:
if user_id not in self.allowlist:
return False, "User not in allowlist"
return True, None
def is_admin(self, user_id: str) -> bool:
"""Check if user is an admin."""
return user_id in self.admin_nodes
def add_to_blocklist(self, user_id: str) -> None:
"""Add a user to the blocklist."""
self.blocklist.add(user_id)
def remove_from_blocklist(self, user_id: str) -> None:
"""Remove a user from the blocklist."""
self.blocklist.discard(user_id)

View file

@ -1,292 +0,0 @@
"""Simple web status page for MeshAI."""
import asyncio
import html as html_module
import json
import logging
import threading
import time
from datetime import datetime
from http.server import BaseHTTPRequestHandler, HTTPServer
from threading import Thread
from typing import Callable, Optional
from .config import WebStatusConfig
logger = logging.getLogger(__name__)
class StatusData:
"""Container for status information."""
def __init__(self):
self._lock = threading.Lock()
self.start_time = time.time()
self.message_count = 0
self.response_count = 0
self.error_count = 0
self.connected_nodes: set[str] = set()
self.recent_activity: list[dict] = []
self.last_message_time: Optional[float] = None
self.using_fallback = False
def record_message(self, sender_id: str, sender_name: str):
"""Record an incoming message."""
with self._lock:
self.message_count += 1
self.last_message_time = time.time()
self.connected_nodes.add(sender_id)
self.recent_activity.append({
"type": "message",
"time": datetime.now().isoformat(),
"sender": sender_name,
})
# Keep only last 20 activities
self.recent_activity = self.recent_activity[-20:]
def record_response(self):
"""Record an outgoing response."""
with self._lock:
self.response_count += 1
def record_error(self, error: str):
"""Record an error."""
with self._lock:
self.error_count += 1
self.recent_activity.append({
"type": "error",
"time": datetime.now().isoformat(),
"error": error[:100],
})
self.recent_activity = self.recent_activity[-20:]
def get_uptime(self) -> str:
"""Get formatted uptime string."""
elapsed = int(time.time() - self.start_time)
days, remainder = divmod(elapsed, 86400)
hours, remainder = divmod(remainder, 3600)
minutes, seconds = divmod(remainder, 60)
parts = []
if days:
parts.append(f"{days}d")
if hours:
parts.append(f"{hours}h")
if minutes:
parts.append(f"{minutes}m")
parts.append(f"{seconds}s")
return " ".join(parts)
def to_dict(self, include_activity: bool = False) -> dict:
"""Convert to dictionary for JSON response."""
with self._lock:
data = {
"status": "online",
"uptime": self.get_uptime(),
"uptime_seconds": int(time.time() - self.start_time),
"messages_received": self.message_count,
"responses_sent": self.response_count,
"errors": self.error_count,
"connected_nodes": len(self.connected_nodes),
"using_fallback": self.using_fallback,
}
if self.last_message_time:
data["last_message_ago"] = int(time.time() - self.last_message_time)
if include_activity:
data["recent_activity"] = list(self.recent_activity)
return data
# Global status data instance
_status_data = StatusData()
def get_status_data() -> StatusData:
"""Get the global status data instance."""
return _status_data
class StatusRequestHandler(BaseHTTPRequestHandler):
"""HTTP request handler for status page."""
config: WebStatusConfig = None
def log_message(self, format, *args):
"""Suppress default logging."""
pass
def do_GET(self):
"""Handle GET requests."""
if self.path == "/" or self.path == "/status":
self._serve_status_page()
elif self.path == "/api/status":
self._serve_json_status()
elif self.path == "/health":
self._serve_health()
else:
self.send_error(404)
def _check_auth(self) -> bool:
"""Check authentication if required."""
if not self.config or not self.config.require_auth:
return True
auth_header = self.headers.get("Authorization", "")
if auth_header.startswith("Basic "):
import base64
try:
decoded = base64.b64decode(auth_header[6:]).decode()
_, password = decoded.split(":", 1)
return password == self.config.auth_password
except Exception:
pass
return False
def _serve_status_page(self):
"""Serve HTML status page."""
if not self._check_auth():
self.send_response(401)
self.send_header("WWW-Authenticate", 'Basic realm="MeshAI Status"')
self.end_headers()
return
status = _status_data.to_dict(
include_activity=self.config.show_recent_activity if self.config else False
)
esc = html_module.escape
# Build optional stat rows
rows = ""
if self.config and self.config.show_uptime:
rows += (
'<div class="stat"><span class="stat-label">Uptime</span>'
f'<span class="stat-value">{esc(str(status["uptime"]))}</span></div>'
)
if self.config and self.config.show_message_count:
rows += (
'<div class="stat"><span class="stat-label">Messages</span>'
f'<span class="stat-value">{esc(str(status["messages_received"]))}</span></div>'
'<div class="stat"><span class="stat-label">Responses</span>'
f'<span class="stat-value">{esc(str(status["responses_sent"]))}</span></div>'
)
if self.config and self.config.show_connected_nodes:
rows += (
'<div class="stat"><span class="stat-label">Connected Nodes</span>'
f'<span class="stat-value">{esc(str(status["connected_nodes"]))}</span></div>'
)
status_class = "status-fallback" if status.get("using_fallback") else "status-online"
status_text = "ONLINE (Fallback)" if status.get("using_fallback") else "ONLINE"
html = f"""<!DOCTYPE html>
<html>
<head>
<title>MeshAI Status</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {{
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace;
background: #0d1117;
color: #c9d1d9;
margin: 0;
padding: 20px;
}}
.container {{ max-width: 600px; margin: 0 auto; }}
h1 {{ color: #58a6ff; border-bottom: 1px solid #30363d; padding-bottom: 10px; }}
.stat {{
display: flex;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid #21262d;
}}
.stat-label {{ color: #8b949e; }}
.stat-value {{ color: #58a6ff; font-weight: bold; }}
.status-online {{ color: #3fb950; }}
.status-fallback {{ color: #d29922; }}
.footer {{ margin-top: 20px; color: #484f58; font-size: 12px; }}
</style>
</head>
<body>
<div class="container">
<h1>MeshAI Status</h1>
<div class="stat">
<span class="stat-label">Status</span>
<span class="stat-value {esc(status_class)}">{esc(status_text)}</span>
</div>
{rows}
<div class="stat">
<span class="stat-label">Errors</span>
<span class="stat-value">{esc(str(status["errors"]))}</span>
</div>
<div class="footer">Auto-refresh in 30s</div>
</div>
<script>setTimeout(() => location.reload(), 30000);</script>
</body>
</html>"""
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(html.encode())
def _serve_json_status(self):
"""Serve JSON status."""
if not self._check_auth():
self.send_response(401)
self.end_headers()
return
status = _status_data.to_dict(
include_activity=self.config.show_recent_activity if self.config else False
)
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(json.dumps(status, indent=2).encode())
def _serve_health(self):
"""Serve simple health check."""
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(b"OK")
class WebStatusServer:
"""Web status server manager."""
def __init__(self, config: WebStatusConfig):
self.config = config
self._server: Optional[HTTPServer] = None
self._thread: Optional[Thread] = None
def start(self):
"""Start the web status server."""
if not self.config.enabled:
return
StatusRequestHandler.config = self.config
try:
self._server = HTTPServer(("0.0.0.0", self.config.port), StatusRequestHandler)
self._thread = Thread(target=self._server.serve_forever, daemon=True)
self._thread.start()
logger.info(f"Web status server started on port {self.config.port}")
except Exception as e:
logger.error(f"Failed to start web status server: {e}")
def stop(self):
"""Stop the web status server."""
if self._server:
self._server.shutdown()
self._server = None
self._thread = None
logger.info("Web status server stopped")

View file

@ -1,176 +0,0 @@
"""Webhook integration for MeshAI."""
import asyncio
import json
import logging
from datetime import datetime
from typing import Any, Optional
import httpx
from .config import WebhookConfig
logger = logging.getLogger(__name__)
class WebhookClient:
"""Client for sending webhook notifications."""
def __init__(self, config: WebhookConfig):
self.config = config
self._client: Optional[httpx.AsyncClient] = None
self._queue: asyncio.Queue = asyncio.Queue()
self._task: Optional[asyncio.Task] = None
async def start(self):
"""Start the webhook client."""
if not self.config.enabled or not self.config.url:
logger.debug("Webhooks disabled or no URL configured")
return
self._client = httpx.AsyncClient(timeout=10.0)
self._task = asyncio.create_task(self._process_queue())
logger.info(f"Webhook client started: {self.config.url}")
async def stop(self):
"""Stop the webhook client."""
if self._task:
self._task.cancel()
try:
await self._task
except asyncio.CancelledError:
pass
self._task = None
if self._client:
await self._client.aclose()
self._client = None
logger.info("Webhook client stopped")
def _should_send(self, event_type: str) -> bool:
"""Check if this event type should be sent."""
if not self.config.enabled:
return False
return event_type in self.config.events
async def send_event(
self,
event_type: str,
data: dict[str, Any],
immediate: bool = False,
):
"""Send a webhook event.
Args:
event_type: Type of event (message_received, response_sent, error)
data: Event data
immediate: If True, send immediately instead of queuing
"""
if not self._should_send(event_type):
return
payload = {
"event": event_type,
"timestamp": datetime.utcnow().isoformat() + "Z",
"data": data,
}
if immediate:
await self._send_payload(payload)
else:
await self._queue.put(payload)
async def _process_queue(self):
"""Process queued webhook payloads."""
while True:
try:
payload = await self._queue.get()
await self._send_payload(payload)
self._queue.task_done()
except asyncio.CancelledError:
break
except Exception as e:
logger.error(f"Error processing webhook queue: {e}")
async def _send_payload(self, payload: dict):
"""Send a webhook payload."""
if not self._client:
return
try:
response = await self._client.post(
self.config.url,
json=payload,
headers={"Content-Type": "application/json"},
)
if response.status_code >= 400:
logger.warning(
f"Webhook returned {response.status_code}: {response.text[:100]}"
)
except Exception as e:
logger.error(f"Failed to send webhook: {e}")
# Convenience methods for common events
async def on_message_received(
self,
sender_id: str,
sender_name: str,
text: str,
channel: int,
is_dm: bool,
):
"""Send message_received event."""
await self.send_event(
"message_received",
{
"sender_id": sender_id,
"sender_name": sender_name,
"text": text,
"channel": channel,
"is_dm": is_dm,
},
)
async def on_response_sent(
self,
recipient_id: Optional[str],
text: str,
channel: int,
):
"""Send response_sent event."""
await self.send_event(
"response_sent",
{
"recipient_id": recipient_id,
"text": text,
"channel": channel,
},
)
async def on_error(self, error: str, context: Optional[dict] = None):
"""Send error event."""
await self.send_event(
"error",
{
"error": error,
"context": context or {},
},
)
async def on_startup(self):
"""Send startup event."""
await self.send_event(
"startup",
{"message": "MeshAI started"},
immediate=True,
)
async def on_shutdown(self):
"""Send shutdown event."""
await self.send_event(
"shutdown",
{"message": "MeshAI stopping"},
immediate=True,
)