From c69a3af4e6a3cd6e144aa40bb921ccec8fa1205a Mon Sep 17 00:00:00 2001 From: agessaman Date: Mon, 18 May 2026 21:06:28 -0700 Subject: [PATCH] refactor(core): extract admin server and default config from core.py Moves _BotAdminServer to modules/admin_server.py (renamed BotAdminServer) and the create_default_config method + 360-line template string to modules/default_config.py. core.py shrinks from 2,289 to ~1,870 lines; all 44 core tests and 2866 total tests still pass. Co-Authored-By: Claude Sonnet 4.6 --- modules/admin_server.py | 64 ++++++ modules/core.py | 429 +------------------------------------- modules/default_config.py | 372 +++++++++++++++++++++++++++++++++ 3 files changed, 441 insertions(+), 424 deletions(-) create mode 100644 modules/admin_server.py create mode 100644 modules/default_config.py diff --git a/modules/admin_server.py b/modules/admin_server.py new file mode 100644 index 0000000..f124636 --- /dev/null +++ b/modules/admin_server.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +""" +Minimal Flask HTTP admin server for the MeshCore Bot. + +Runs in a daemon thread alongside the bot's asyncio loop. +Configured via the ``[Admin]`` section in config.ini: + + [Admin] + enabled = true + port = 5001 + token = ; required; requests without matching Bearer token are rejected +""" + +import threading +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from .core import MeshCoreBot + + +class BotAdminServer(threading.Thread): + """Minimal Flask HTTP server exposing bot admin endpoints.""" + + def __init__(self, bot: "MeshCoreBot", port: int, token: str) -> None: + super().__init__(daemon=True, name="BotAdminServer") + self._bot = bot + self._port = port + self._token = token + + def run(self) -> None: + try: + from flask import Flask, Response, jsonify + from flask import request as flask_request + + app = Flask("bot_admin") + # Suppress Flask startup banner and request logs + import logging as _logging + _logging.getLogger("werkzeug").setLevel(_logging.ERROR) + + def _check_auth() -> "Response | None": + auth = flask_request.headers.get("Authorization", "") + if not auth.startswith("Bearer ") or auth[7:] != self._token: + return jsonify({"error": "unauthorized"}), 401 + return None + + @app.post("/api/admin/reload") + def reload_config(): # type: ignore[no-untyped-def] + denied = _check_auth() + if denied is not None: + return denied + success, msg = self._bot.reload_config() + status = 200 if success else 409 + return jsonify({"success": success, "message": msg}), status + + @app.get("/api/admin/health") + def health(): # type: ignore[no-untyped-def] + denied = _check_auth() + if denied is not None: + return denied + return jsonify({"status": "ok"}) + + app.run(host="127.0.0.1", port=self._port, threaded=True) + except Exception as exc: # noqa: BLE001 + self._bot.logger.error("BotAdminServer failed to start: %s", exc) diff --git a/modules/core.py b/modules/core.py index 6c84966..584c5dc 100644 --- a/modules/core.py +++ b/modules/core.py @@ -25,9 +25,11 @@ import colorlog import meshcore from meshcore import EventType +from .admin_server import BotAdminServer from .channel_manager import ChannelManager from .command_manager import CommandManager from shared.db_manager import AsyncDBManager, DBManager +from .default_config import create_default_config from .feed_manager import FeedManager from .i18n import Translator from .message_handler import MessageHandler @@ -62,61 +64,6 @@ class _JsonFormatter(logging.Formatter): return json.dumps(obj, ensure_ascii=False) -class _BotAdminServer(threading.Thread): - """Minimal Flask HTTP server exposing bot admin endpoints. - - Runs in a daemon thread alongside the bot's asyncio loop. - Configured via ``[Admin]`` section in config.ini: - - [Admin] - enabled = true - port = 5001 - token = ; required; requests without matching Bearer token are rejected - """ - - def __init__(self, bot: "MeshCoreBot", port: int, token: str) -> None: - super().__init__(daemon=True, name="BotAdminServer") - self._bot = bot - self._port = port - self._token = token - - def run(self) -> None: - try: - from flask import Flask, Response, jsonify - from flask import request as flask_request - - app = Flask("bot_admin") - # Suppress Flask startup banner and request logs - import logging as _logging - _logging.getLogger("werkzeug").setLevel(_logging.ERROR) - - def _check_auth() -> "Response | None": - auth = flask_request.headers.get("Authorization", "") - if not auth.startswith("Bearer ") or auth[7:] != self._token: - return jsonify({"error": "unauthorized"}), 401 - return None - - @app.post("/api/admin/reload") - def reload_config(): # type: ignore[no-untyped-def] - denied = _check_auth() - if denied is not None: - return denied - success, msg = self._bot.reload_config() - status = 200 if success else 409 - return jsonify({"success": success, "message": msg}), status - - @app.get("/api/admin/health") - def health(): # type: ignore[no-untyped-def] - denied = _check_auth() - if denied is not None: - return denied - return jsonify({"status": "ok"}) - - app.run(host="127.0.0.1", port=self._port, threaded=True) - except Exception as exc: # noqa: BLE001 - self._bot.logger.error("BotAdminServer failed to start: %s", exc) - - class MeshCoreBot: """MeshCore Bot using official meshcore package. @@ -195,12 +142,12 @@ class MeshCoreBot: self.web_viewer_integration = None # Admin HTTP server (optional — [Admin] section) - self._admin_server: _BotAdminServer | None = None + self._admin_server: BotAdminServer | None = None if self.config.getboolean('Admin', 'enabled', fallback=False): admin_port = self.config.getint('Admin', 'port', fallback=5001) admin_token = self.config.get('Admin', 'token', fallback='') if admin_token: - self._admin_server = _BotAdminServer(self, admin_port, admin_token) + self._admin_server = BotAdminServer(self, admin_port, admin_token) else: self.logger.warning("Admin server enabled but no token configured — skipping") @@ -445,7 +392,7 @@ class MeshCoreBot: does not exist, a default configuration is created first. """ if not Path(self.config_file).exists(): - self.create_default_config() + create_default_config(self.config_file) # Force UTF-8 so emoji and non-ASCII characters in config.ini parse on Windows. self.config.read(self.config_file, encoding="utf-8") @@ -626,372 +573,6 @@ class MeshCoreBot: self.logger.error(traceback.format_exc()) return (False, error_msg) - def create_default_config(self) -> None: - """Create default configuration file. - - Writes a default 'config.ini' file to disk with standard settings - and comments explaining each option. - """ - default_config = """[Connection] -# Connection type: serial, ble, or tcp -# serial: Connect via USB serial port -# ble: Connect via Bluetooth Low Energy -# tcp: Connect via TCP/IP -connection_type = serial - -# Serial port (for serial connection) -# Common ports: /dev/ttyUSB0, /dev/tty.usbserial-*, COM3 (Windows) -serial_port = /dev/ttyUSB0 - -# BLE device name (for BLE connection) -# Leave commented out for auto-detection, or specify exact device name -#ble_device_name = MeshCore - -# TCP hostname or IP address (for TCP connection) -#hostname = 192.168.1.60 -# TCP port (for TCP connection) -#tcp_port = 5000 - -# Connection timeout in seconds -timeout = 30 - -[Bot] -# Bot name for identification and logging -bot_name = MeshCoreBot - -# RF Data Correlation Settings -# Time window for correlating RF data with messages (seconds) -rf_data_timeout = 15.0 - -# Time to wait for RF data correlation (seconds) -message_correlation_timeout = 10.0 - -# Enable enhanced correlation strategies -enable_enhanced_correlation = true - -# Bot node ID (leave empty for auto-assignment) -node_id = - -# Enable/disable bot responses -# true: Bot will respond to keywords and commands -# false: Bot will only listen and log messages -enabled = true - -# Passive mode (only listen, don't respond) -# true: Bot will not send any messages -# false: Bot will respond normally -passive_mode = false - -# Rate limiting in seconds between messages -# Prevents spam by limiting how often the bot can send messages -rate_limit_seconds = 2 - -# Bot transmission rate limit in seconds between bot messages -# Prevents bot from overwhelming the mesh network -bot_tx_rate_limit_seconds = 1.0 - -# Transmission delay in milliseconds before sending messages -# Helps prevent message collisions on the mesh network -# Recommended: 100-500ms for busy networks, 0 for quiet networks -tx_delay_ms = 250 - -# DM retry settings for improved reliability (meshcore-2.1.6+) -# Maximum number of retry attempts for failed DM sends -dm_max_retries = 3 - -# Maximum flood attempts (when path reset is needed) -dm_max_flood_attempts = 2 - -# Number of attempts before switching to flood mode -dm_flood_after = 2 - -# Timezone for bot operations -# Use standard timezone names (e.g., "America/New_York", "Europe/London", "UTC") -# Leave empty to use system timezone -timezone = - -# Bot location for geographic proximity calculations and astronomical data -# Default latitude for bot location (decimal degrees) -# Example: 40.7128 for New York City, 48.50 for Victoria BC -bot_latitude = 40.7128 - -# Default longitude for bot location (decimal degrees) -# Example: -74.0060 for New York City, -123.00 for Victoria BC -bot_longitude = -74.0060 - -# Interval-based advertising settings -# Send periodic flood adverts at specified intervals -# 0: Disabled (default) -# >0: Send flood advert every N hours -advert_interval_hours = 0 - -# Send startup advert when bot finishes initializing -# false: No startup advert (default) -# zero-hop: Send local broadcast advert -# flood: Send network-wide flood advert -startup_advert = false - -# Auto-manage contact list when new contacts are discovered -# device: Device handles auto-addition using standard auto-discovery mode, bot manages contact list capacity (purge old contacts when near limits) -# bot: Bot automatically adds new companion contacts to device, bot manages contact list capacity (purge old contacts when near limits) -# false: Manual mode - no automatic actions, use !repeater commands to manage contacts (default) -auto_manage_contacts = false - -[Admin_ACL] -# Admin Access Control List (ACL) for restricted commands -# Only users with public keys listed here can execute admin commands -# Format: comma-separated list of public keys (without spaces) -# Example: f5d2b56d19b24412756933e917d4632e088cdd5daeadc9002feca73bf5d2b56d,another_key_here -admin_pubkeys = - -# Commands that require admin access (comma-separated) -# These commands will only work for users in the admin_pubkeys list -admin_commands = repeater - -[Keywords] -# Keyword-response pairs (keyword = response format) -# Available fields: {sender}, {connection_info}, {snr}, {rssi}, {timestamp}, {path}, {path_distance}, {firstlast_distance} -# {sender}: Name/ID of message sender -# {connection_info}: Path info, SNR, and RSSI combined (e.g., "01,5f (2 hops) | SNR: 15 dB | RSSI: -120 dBm") -# {snr}: Signal-to-noise ratio in dB -# {rssi}: Received signal strength indicator in dBm -# {timestamp}: Message timestamp in HH:MM:SS format -# {path}: Message routing path (e.g., "01,5f (2 hops)") -# {hops}: Total hop count only (e.g., "2" or "0"); same value as in path/connection_info -# {hops_label}: Same as hops with "hop"/"hops" and pluralization (e.g., "1 hop", "2 hops") -# {path_distance}: Total distance between all hops in path with locations (e.g., "123.4km (3 segs, 1 no-loc)") -# {firstlast_distance}: Distance between first and last repeater in path (e.g., "45.6km" or empty if locations missing) -test = "ack [@{sender}]{phrase_part} | {connection_info} | Received at: {timestamp}" -ping = "Pong!" -pong = "Ping!" -help = "Bot Help: test, ping, help, hello, cmd, advert, t phrase, @string, wx, aqi, sun, moon, solar, hfcond, satpass | Use 'help ' for details" -cmd = "Available commands: test, ping, help, hello, cmd, advert, t phrase, @string, wx, aqi, sun, moon, solar, hfcond, satpass" - -[Channels] -# Channels to monitor (comma-separated) -# Bot will only respond to messages on these channels -# Use exact channel names as configured on your MeshCore node -monitor_channels = general,test,emergency - -# Enable DM responses -# true: Bot will respond to direct messages -# false: Bot will ignore direct messages -respond_to_dms = true - -[Banned_Users] -# List of banned sender names (comma-separated). Matching is prefix (starts-with): -# "Awful Username" also matches "Awful Username 🍆". No bot responses in channels or DMs. -banned_users = - -[Feed_Manager] -# Enable or disable RSS/API feed subscriptions -# true: Feed manager polls configured feeds and sends updates to channels -# false: Feed manager disabled (default) -feed_manager_enabled = false - -[Scheduled_Messages] -# Scheduled message format: HHMM = channel:message -# Time format: HHMM (24-hour, no colon) -# Bot will send these messages at the specified times daily -0800 = general:Good morning! Bot is online and ready. -1200 = general:Midday status check - all systems operational. -1800 = general:Evening update - bot status: Good - -[Logging] -# Log level: DEBUG, INFO, WARNING, ERROR, CRITICAL -# DEBUG: Most verbose, shows all details -# INFO: Standard logging level -# WARNING: Only warnings and errors -# ERROR: Only errors -# CRITICAL: Only critical errors -log_level = INFO - -# Log file path (leave empty for console only) -# Bot will write logs to this file in addition to console -# Use absolute path for Docker compatibility (e.g., /data/logs/meshcore_bot.log) -# Relative paths will resolve relative to the config file directory -log_file = meshcore_bot.log - -# Enable colored console output -# true: Use colors in console output -# false: Plain text output -colored_output = true - -# MeshCore library log level (separate from bot log level) -# Controls debug output from the meshcore library itself -# Options: DEBUG, INFO, WARNING, ERROR, CRITICAL -meshcore_log_level = INFO - -[Custom_Syntax] -# Custom syntax patterns for special message formats -# Format: pattern = "response_format" -# Available fields: {sender}, {phrase}, {connection_info}, {snr}, {timestamp}, {path} -# {phrase}: The text after the trigger (for t_phrase syntax) -# -# Special syntax: Messages starting with "t " or "T " followed by a phrase -# Example: "t hello world" -> "ack {sender}: hello world | {connection_info}" -t_phrase = "ack {sender}: {phrase} | {connection_info}" - - -[External_Data] -# Weather API key (future feature) -weather_api_key = - -# Weather update interval in seconds (future feature) -weather_update_interval = 3600 - -# Tide API key (future feature) -tide_api_key = - -# Tide update interval in seconds (future feature) -tide_update_interval = 1800 - -# N2YO API key for satellite pass information -# Get free key at: https://www.n2yo.com/login/ -n2yo_api_key = - -# AirNow API key for AQI data -# Get free key at: https://docs.airnowapi.org/ -airnow_api_key = - -# Repeater prefix API URL for prefix command -# Leave empty to disable prefix command functionality -# Configure your own regional API endpoint -repeater_prefix_api_url = - -# Repeater prefix cache duration in hours -# How long to cache prefix data before refreshing from API -# Recommended: 1-6 hours (data doesn't change frequently) -repeater_prefix_cache_hours = 1 - -[Prefix_Command] -# Enable or disable repeater geolocation in prefix command -# true: Show city names with repeaters when location data is available -# false: Show only repeater names without location information -show_repeater_locations = true - -# Use reverse geocoding for coordinates without city names -# true: Automatically look up city names from GPS coordinates -# false: Only show coordinates if no city name is available -use_reverse_geocoding = true - -# Hide prefix source information -# true: Hide "Source: domain.com" line from prefix command output -# false: Show source information (default) -hide_source = false - -# Prefix heard time window (days) -# Number of days to look back when showing prefix results (default command behavior) -# Only repeaters heard within this window will be shown by default -# Use "prefix XX all" to show all repeaters regardless of time -prefix_heard_days = 7 - -# Prefix free time window (days) -# Number of days to look back when determining which prefixes are "free" -# Only repeaters heard within this window will be considered as using a prefix -# Repeaters not heard in this window will be excluded from used prefixes list -prefix_free_days = 30 - -[Weather] -# Default state for city name disambiguation -# When users type "wx seattle", it will search for "seattle, WA, USA" -# Use 2-letter state abbreviation (e.g., WA, CA, NY, TX) -default_state = WA - -# Default country for city name disambiguation (for international weather plugin) -# Use 2-letter country code (e.g., US, CA, GB, AU) -default_country = US - -# Temperature unit for weather display -# Options: fahrenheit, celsius -# Default: fahrenheit -temperature_unit = fahrenheit - -# Wind speed unit for weather display -# Options: mph, kmh, ms (meters per second) -# Default: mph -wind_speed_unit = mph - -# Precipitation unit for weather display -# Options: inch, mm -# Default: inch -precipitation_unit = inch - -[Path_Command] -# Optional prefix on path command replies: {sender}, {connection_info}, {path}, {timestamp}, {snr}, {rssi} -# reply_prefix = -# Bytes per hop before repeater name lookup (0/1 = always; 2/3 = gate to hex + tip if shorter) -# minimum_path_bytes = 0 -# Geographic proximity calculation method -# simple: Use proximity to bot location (default) -# path: Use proximity to previous/next nodes in the path for more realistic routing -proximity_method = simple - -# Enable path proximity fallback -# When path proximity can't be calculated (missing location data), fall back to simple proximity -# true: Fall back to bot location proximity when path data unavailable -# false: Show collision warning when path proximity unavailable -path_proximity_fallback = true - -# Maximum range for geographic proximity guessing (kilometers) -# Repeaters beyond this distance will have reduced confidence or be rejected -# Set to 0 to disable range limiting -max_proximity_range = 200 - -# Maximum age for repeater data in path matching (days) -# Only include repeaters that have been heard within this many days -# Helps filter out stale or inactive repeaters from path decoding -# Set to 0 to disable age filtering -max_repeater_age_days = 14 - -# Confidence indicator symbols for path command -# High confidence (>= 0.9): Shows when path decoding is very reliable -high_confidence_symbol = 🎯 - -# Medium confidence (>= 0.8): Shows when path decoding is reasonably reliable -medium_confidence_symbol = 📍 - -# Low confidence (< 0.8): Shows when path decoding has uncertainty -low_confidence_symbol = ❓ - -[Solar_Config] -# URL timeout for external API calls (seconds) -url_timeout = 10 - -# Use Zulu/UTC time for astronomical data -# true: Use 24-hour UTC format -# false: Use 12-hour local format -use_zulu_time = false - -[Joke_Command] -# Enable or disable the joke command (true/false) -enabled = true - -# Enable seasonal joke defaults (October: spooky, December: Christmas) -# true: Seasonal defaults are applied (default) -# false: No seasonal defaults (always random) -seasonal_jokes = true - -# Handle long jokes (over 130 characters) -# false: Fetch new jokes until we get a short one (default) -# true: Split long jokes into multiple messages -long_jokes = false - -[DadJoke_Command] -# Enable or disable the dad joke command (true/false) -enabled = true - -# Handle long jokes (over 130 characters) -# false: Fetch new jokes until we get a short one (default) -# true: Split long jokes into multiple messages -long_jokes = false - -""" - with open(self.config_file, 'w') as f: - f.write(default_config) - # Note: Using print here since logger may not be initialized yet - print(f"Created default config file: {self.config_file}") - def setup_logging(self) -> None: """Setup logging configuration. diff --git a/modules/default_config.py b/modules/default_config.py new file mode 100644 index 0000000..2fc2206 --- /dev/null +++ b/modules/default_config.py @@ -0,0 +1,372 @@ +#!/usr/bin/env python3 +""" +Default configuration template for MeshCore Bot. + +Call ``create_default_config(path)`` to write config.ini when none exists. +""" + +DEFAULT_CONFIG = """[Connection] +# Connection type: serial, ble, or tcp +# serial: Connect via USB serial port +# ble: Connect via Bluetooth Low Energy +# tcp: Connect via TCP/IP +connection_type = serial + +# Serial port (for serial connection) +# Common ports: /dev/ttyUSB0, /dev/tty.usbserial-*, COM3 (Windows) +serial_port = /dev/ttyUSB0 + +# BLE device name (for BLE connection) +# Leave commented out for auto-detection, or specify exact device name +#ble_device_name = MeshCore + +# TCP hostname or IP address (for TCP connection) +#hostname = 192.168.1.60 +# TCP port (for TCP connection) +#tcp_port = 5000 + +# Connection timeout in seconds +timeout = 30 + +[Bot] +# Bot name for identification and logging +bot_name = MeshCoreBot + +# RF Data Correlation Settings +# Time window for correlating RF data with messages (seconds) +rf_data_timeout = 15.0 + +# Time to wait for RF data correlation (seconds) +message_correlation_timeout = 10.0 + +# Enable enhanced correlation strategies +enable_enhanced_correlation = true + +# Bot node ID (leave empty for auto-assignment) +node_id = + +# Enable/disable bot responses +# true: Bot will respond to keywords and commands +# false: Bot will only listen and log messages +enabled = true + +# Passive mode (only listen, don't respond) +# true: Bot will not send any messages +# false: Bot will respond normally +passive_mode = false + +# Rate limiting in seconds between messages +# Prevents spam by limiting how often the bot can send messages +rate_limit_seconds = 2 + +# Bot transmission rate limit in seconds between bot messages +# Prevents bot from overwhelming the mesh network +bot_tx_rate_limit_seconds = 1.0 + +# Transmission delay in milliseconds before sending messages +# Helps prevent message collisions on the mesh network +# Recommended: 100-500ms for busy networks, 0 for quiet networks +tx_delay_ms = 250 + +# DM retry settings for improved reliability (meshcore-2.1.6+) +# Maximum number of retry attempts for failed DM sends +dm_max_retries = 3 + +# Maximum flood attempts (when path reset is needed) +dm_max_flood_attempts = 2 + +# Number of attempts before switching to flood mode +dm_flood_after = 2 + +# Timezone for bot operations +# Use standard timezone names (e.g., "America/New_York", "Europe/London", "UTC") +# Leave empty to use system timezone +timezone = + +# Bot location for geographic proximity calculations and astronomical data +# Default latitude for bot location (decimal degrees) +# Example: 40.7128 for New York City, 48.50 for Victoria BC +bot_latitude = 40.7128 + +# Default longitude for bot location (decimal degrees) +# Example: -74.0060 for New York City, -123.00 for Victoria BC +bot_longitude = -74.0060 + +# Interval-based advertising settings +# Send periodic flood adverts at specified intervals +# 0: Disabled (default) +# >0: Send flood advert every N hours +advert_interval_hours = 0 + +# Send startup advert when bot finishes initializing +# false: No startup advert (default) +# zero-hop: Send local broadcast advert +# flood: Send network-wide flood advert +startup_advert = false + +# Auto-manage contact list when new contacts are discovered +# device: Device handles auto-addition using standard auto-discovery mode, bot manages contact list capacity (purge old contacts when near limits) +# bot: Bot automatically adds new companion contacts to device, bot manages contact list capacity (purge old contacts when near limits) +# false: Manual mode - no automatic actions, use !repeater commands to manage contacts (default) +auto_manage_contacts = false + +[Admin_ACL] +# Admin Access Control List (ACL) for restricted commands +# Only users with public keys listed here can execute admin commands +# Format: comma-separated list of public keys (without spaces) +# Example: f5d2b56d19b24412756933e917d4632e088cdd5daeadc9002feca73bf5d2b56d,another_key_here +admin_pubkeys = + +# Commands that require admin access (comma-separated) +# These commands will only work for users in the admin_pubkeys list +admin_commands = repeater + +[Keywords] +# Keyword-response pairs (keyword = response format) +# Available fields: {sender}, {connection_info}, {snr}, {rssi}, {timestamp}, {path}, {path_distance}, {firstlast_distance} +# {sender}: Name/ID of message sender +# {connection_info}: Path info, SNR, and RSSI combined (e.g., "01,5f (2 hops) | SNR: 15 dB | RSSI: -120 dBm") +# {snr}: Signal-to-noise ratio in dB +# {rssi}: Received signal strength indicator in dBm +# {timestamp}: Message timestamp in HH:MM:SS format +# {path}: Message routing path (e.g., "01,5f (2 hops)") +# {hops}: Total hop count only (e.g., "2" or "0"); same value as in path/connection_info +# {hops_label}: Same as hops with "hop"/"hops" and pluralization (e.g., "1 hop", "2 hops") +# {path_distance}: Total distance between all hops in path with locations (e.g., "123.4km (3 segs, 1 no-loc)") +# {firstlast_distance}: Distance between first and last repeater in path (e.g., "45.6km" or empty if locations missing) +test = "ack [@{sender}]{phrase_part} | {connection_info} | Received at: {timestamp}" +ping = "Pong!" +pong = "Ping!" +help = "Bot Help: test, ping, help, hello, cmd, advert, t phrase, @string, wx, aqi, sun, moon, solar, hfcond, satpass | Use 'help ' for details" +cmd = "Available commands: test, ping, help, hello, cmd, advert, t phrase, @string, wx, aqi, sun, moon, solar, hfcond, satpass" + +[Channels] +# Channels to monitor (comma-separated) +# Bot will only respond to messages on these channels +# Use exact channel names as configured on your MeshCore node +monitor_channels = general,test,emergency + +# Enable DM responses +# true: Bot will respond to direct messages +# false: Bot will ignore direct messages +respond_to_dms = true + +[Banned_Users] +# List of banned sender names (comma-separated). Matching is prefix (starts-with): +# "Awful Username" also matches "Awful Username 🍆". No bot responses in channels or DMs. +banned_users = + +[Feed_Manager] +# Enable or disable RSS/API feed subscriptions +# true: Feed manager polls configured feeds and sends updates to channels +# false: Feed manager disabled (default) +feed_manager_enabled = false + +[Scheduled_Messages] +# Scheduled message format: HHMM = channel:message +# Time format: HHMM (24-hour, no colon) +# Bot will send these messages at the specified times daily +0800 = general:Good morning! Bot is online and ready. +1200 = general:Midday status check - all systems operational. +1800 = general:Evening update - bot status: Good + +[Logging] +# Log level: DEBUG, INFO, WARNING, ERROR, CRITICAL +# DEBUG: Most verbose, shows all details +# INFO: Standard logging level +# WARNING: Only warnings and errors +# ERROR: Only errors +# CRITICAL: Only critical errors +log_level = INFO + +# Log file path (leave empty for console only) +# Bot will write logs to this file in addition to console +# Use absolute path for Docker compatibility (e.g., /data/logs/meshcore_bot.log) +# Relative paths will resolve relative to the config file directory +log_file = meshcore_bot.log + +# Enable colored console output +# true: Use colors in console output +# false: Plain text output +colored_output = true + +# MeshCore library log level (separate from bot log level) +# Controls debug output from the meshcore library itself +# Options: DEBUG, INFO, WARNING, ERROR, CRITICAL +meshcore_log_level = INFO + +[Custom_Syntax] +# Custom syntax patterns for special message formats +# Format: pattern = "response_format" +# Available fields: {sender}, {phrase}, {connection_info}, {snr}, {timestamp}, {path} +# {phrase}: The text after the trigger (for t_phrase syntax) +# +# Special syntax: Messages starting with "t " or "T " followed by a phrase +# Example: "t hello world" -> "ack {sender}: hello world | {connection_info}" +t_phrase = "ack {sender}: {phrase} | {connection_info}" + + +[External_Data] +# Weather API key (future feature) +weather_api_key = + +# Weather update interval in seconds (future feature) +weather_update_interval = 3600 + +# Tide API key (future feature) +tide_api_key = + +# Tide update interval in seconds (future feature) +tide_update_interval = 1800 + +# N2YO API key for satellite pass information +# Get free key at: https://www.n2yo.com/login/ +n2yo_api_key = + +# AirNow API key for AQI data +# Get free key at: https://docs.airnowapi.org/ +airnow_api_key = + +# Repeater prefix API URL for prefix command +# Leave empty to disable prefix command functionality +# Configure your own regional API endpoint +repeater_prefix_api_url = + +# Repeater prefix cache duration in hours +# How long to cache prefix data before refreshing from API +# Recommended: 1-6 hours (data doesn't change frequently) +repeater_prefix_cache_hours = 1 + +[Prefix_Command] +# Enable or disable repeater geolocation in prefix command +# true: Show city names with repeaters when location data is available +# false: Show only repeater names without location information +show_repeater_locations = true + +# Use reverse geocoding for coordinates without city names +# true: Automatically look up city names from GPS coordinates +# false: Only show coordinates if no city name is available +use_reverse_geocoding = true + +# Hide prefix source information +# true: Hide "Source: domain.com" line from prefix command output +# false: Show source information (default) +hide_source = false + +# Prefix heard time window (days) +# Number of days to look back when showing prefix results (default command behavior) +# Only repeaters heard within this window will be shown by default +# Use "prefix XX all" to show all repeaters regardless of time +prefix_heard_days = 7 + +# Prefix free time window (days) +# Number of days to look back when determining which prefixes are "free" +# Only repeaters heard within this window will be considered as using a prefix +# Repeaters not heard in this window will be excluded from used prefixes list +prefix_free_days = 30 + +[Weather] +# Default state for city name disambiguation +# When users type "wx seattle", it will search for "seattle, WA, USA" +# Use 2-letter state abbreviation (e.g., WA, CA, NY, TX) +default_state = WA + +# Default country for city name disambiguation (for international weather plugin) +# Use 2-letter country code (e.g., US, CA, GB, AU) +default_country = US + +# Temperature unit for weather display +# Options: fahrenheit, celsius +# Default: fahrenheit +temperature_unit = fahrenheit + +# Wind speed unit for weather display +# Options: mph, kmh, ms (meters per second) +# Default: mph +wind_speed_unit = mph + +# Precipitation unit for weather display +# Options: inch, mm +# Default: inch +precipitation_unit = inch + +[Path_Command] +# Optional prefix on path command replies: {sender}, {connection_info}, {path}, {timestamp}, {snr}, {rssi} +# reply_prefix = +# Bytes per hop before repeater name lookup (0/1 = always; 2/3 = gate to hex + tip if shorter) +# minimum_path_bytes = 0 +# Geographic proximity calculation method +# simple: Use proximity to bot location (default) +# path: Use proximity to previous/next nodes in the path for more realistic routing +proximity_method = simple + +# Enable path proximity fallback +# When path proximity can't be calculated (missing location data), fall back to simple proximity +# true: Fall back to bot location proximity when path data unavailable +# false: Show collision warning when path proximity unavailable +path_proximity_fallback = true + +# Maximum range for geographic proximity guessing (kilometers) +# Repeaters beyond this distance will have reduced confidence or be rejected +# Set to 0 to disable range limiting +max_proximity_range = 200 + +# Maximum age for repeater data in path matching (days) +# Only include repeaters that have been heard within this many days +# Helps filter out stale or inactive repeaters from path decoding +# Set to 0 to disable age filtering +max_repeater_age_days = 14 + +# Confidence indicator symbols for path command +# High confidence (>= 0.9): Shows when path decoding is very reliable +high_confidence_symbol = 🎯 + +# Medium confidence (>= 0.8): Shows when path decoding is reasonably reliable +medium_confidence_symbol = 📍 + +# Low confidence (< 0.8): Shows when path decoding has uncertainty +low_confidence_symbol = ❓ + +[Solar_Config] +# URL timeout for external API calls (seconds) +url_timeout = 10 + +# Use Zulu/UTC time for astronomical data +# true: Use 24-hour UTC format +# false: Use 12-hour local format +use_zulu_time = false + +[Joke_Command] +# Enable or disable the joke command (true/false) +enabled = true + +# Enable seasonal joke defaults (October: spooky, December: Christmas) +# true: Seasonal defaults are applied (default) +# false: No seasonal defaults (always random) +seasonal_jokes = true + +# Handle long jokes (over 130 characters) +# false: Fetch new jokes until we get a short one (default) +# true: Split long jokes into multiple messages +long_jokes = false + +[DadJoke_Command] +# Enable or disable the dad joke command (true/false) +enabled = true + +# Handle long jokes (over 130 characters) +# false: Fetch new jokes until we get a short one (default) +# true: Split long jokes into multiple messages +long_jokes = false + +""" + + +def create_default_config(config_file: str) -> None: + """Write DEFAULT_CONFIG to ``config_file``. + + Uses print() instead of logger since this is called before logging is set up. + """ + with open(config_file, 'w') as f: + f.write(DEFAULT_CONFIG) + print(f"Created default config file: {config_file}")