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 <noreply@anthropic.com>
This commit is contained in:
agessaman
2026-05-18 21:06:28 -07:00
parent 8f04d8ceb6
commit c69a3af4e6
3 changed files with 441 additions and 424 deletions
+64
View File
@@ -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 = <secret> ; 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)
+5 -424
View File
@@ -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 = <secret> ; 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 <command>' 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.
+372
View File
@@ -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 <command>' 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}")