mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-10-23 14:22:22 +00:00
Add support for local plugins configuration and directory handling
- Updated `config.ini.example` and `config.ini.minimal-example` to include optional `local_dir_path` for specifying a custom local plugins directory. - Enhanced `install-service.sh` to preserve the `local/` directory during installation and create its structure if it doesn't exist. - Modified `CommandManager` and `MeshCoreBot` to resolve the local commands and services directory based on the new configuration. - Added validation in `config_validation.py` to check the existence and readability of the specified local plugins path. - Improved documentation in `local-plugins.md` to clarify the usage of local plugins and the new configuration options.
This commit is contained in:
1 parent
e9f17ec48e
commit
f86fe0b140
7 files changed
+74
-6
No files matched your search
@@ -130,6 +130,12 @@ auto_manage_contacts = bot
|
||||
# Default: meshcore_bot.db
|
||||
db_path = meshcore_bot.db
|
||||
|
||||
# Local plugins directory (optional)
|
||||
# Directory containing commands/, service_plugins/, and optional config.ini.
|
||||
# Relative paths are resolved from the bot root; absolute paths used as-is.
|
||||
# Default: local (i.e. bot_root/local). Changing this requires a bot restart.
|
||||
#local_dir_path = local
|
||||
|
||||
# Seconds to wait after a failed service restart before retrying (default: 300)
|
||||
service_restart_backoff_seconds = 300
|
||||
|
||||
|
||||
@@ -143,6 +143,9 @@ auto_manage_contacts = false
|
||||
# Default: meshcore_bot.db
|
||||
db_path = meshcore_bot.db
|
||||
|
||||
# Local plugins directory (optional). Default: local. See config.ini.example.
|
||||
#local_dir_path = local
|
||||
|
||||
[Localization]
|
||||
# Language code for bot responses (en, es, es-MX, es-ES, fr, de, ja, etc.)
|
||||
# Default: en (English)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Local plugins and services
|
||||
|
||||
You can add your own **command plugins** and **service plugins** without modifying the bot’s code by placing them in the **`local/`** directory. Their configuration can live in **`local/config.ini`** so it stays separate from the main `config.ini`.
|
||||
You can add your own **command plugins** and **service plugins** without modifying the bot’s code by placing them in a **local** directory. By default that directory is **`local/`** next to your main config.ini; you can change it with **`[Bot]` `local_dir_path`** (see below). Their configuration can live in **`local/config.ini`** so it stays separate from the main `config.ini`.
|
||||
|
||||
## Directories
|
||||
|
||||
@@ -12,6 +12,8 @@ You can add your own **command plugins** and **service plugins** without modifyi
|
||||
|
||||
Local plugins are **additive**: they are loaded after built-in (and alternative) plugins. If a local plugin or service has the same logical **name** as one already loaded, it is **skipped** and a warning is logged. There is no override-by-name for local code.
|
||||
|
||||
**Custom local path:** In `config.ini`, under `[Bot]`, you can set **`local_dir_path`** to a directory that contains `commands/`, `service_plugins/`, and optional `config.ini`. Use a relative path (resolved from the bot root) or an absolute path so the local folder can live outside the install directory (e.g. `/home/user/meshcore-local`). Changing `local_dir_path` requires a **bot restart**.
|
||||
|
||||
## Minimal command plugin
|
||||
|
||||
Create a file in **local/commands/** (e.g. `local/commands/hello_local.py`):
|
||||
|
||||
@@ -308,6 +308,12 @@ copy_files_smart() {
|
||||
print_info "Preserving existing alternative commands (only updating if source is newer)"
|
||||
fi
|
||||
|
||||
# Preserve install dir's local/ entirely (user custom commands and service plugins)
|
||||
# Never overwrite or delete anything under $dest_dir/local/
|
||||
if [ -d "$dest_dir/local" ]; then
|
||||
print_info "Preserving existing local/ directory (not overwriting)"
|
||||
fi
|
||||
|
||||
# Exclude patterns
|
||||
rsync -a --update --exclude='.git' \
|
||||
--exclude='__pycache__' \
|
||||
@@ -320,10 +326,20 @@ copy_files_smart() {
|
||||
--exclude='*.db-wal' \
|
||||
--exclude='*.log' \
|
||||
--exclude='backups' \
|
||||
--exclude='local/' \
|
||||
$preserve_config \
|
||||
"$source_dir/" "$dest_dir/" 2>/dev/null || {
|
||||
print_warning "rsync had some issues, falling back to manual copy"
|
||||
}
|
||||
# If install dir has no local/, create minimal structure from source so service has valid layout
|
||||
if [ ! -d "$dest_dir/local" ]; then
|
||||
print_info "Creating local/ directory structure (first-time install)"
|
||||
mkdir -p "$dest_dir/local/commands" "$dest_dir/local/service_plugins"
|
||||
[ -f "$source_dir/local/README.md" ] && cp "$source_dir/local/README.md" "$dest_dir/local/" || true
|
||||
[ -f "$source_dir/local/__init__.py" ] && cp "$source_dir/local/__init__.py" "$dest_dir/local/" || true
|
||||
[ -f "$source_dir/local/commands/.gitkeep" ] && cp "$source_dir/local/commands/.gitkeep" "$dest_dir/local/commands/" || true
|
||||
[ -f "$source_dir/local/service_plugins/.gitkeep" ] && cp "$source_dir/local/service_plugins/.gitkeep" "$dest_dir/local/service_plugins/" || true
|
||||
fi
|
||||
print_success "Files synchronized using rsync"
|
||||
return 0
|
||||
fi
|
||||
@@ -336,6 +352,11 @@ copy_files_smart() {
|
||||
print_info "Preserving existing alternative commands (not overwriting)"
|
||||
fi
|
||||
|
||||
# Preserve install dir's local/ entirely - never overwrite when it exists
|
||||
if [ -d "$dest_dir/local" ]; then
|
||||
print_info "Preserving existing local/ directory (not overwriting)"
|
||||
fi
|
||||
|
||||
# Copy files, preserving config.ini if it exists
|
||||
while IFS= read -r file; do
|
||||
local rel_path="${file#$source_dir/}"
|
||||
@@ -355,6 +376,12 @@ copy_files_smart() {
|
||||
[[ "$rel_path" == *".log" ]] && continue
|
||||
[[ "$rel_path" == *"/backups/"* ]] && continue
|
||||
|
||||
# Preserve install dir's local/ entirely - skip all files under local/ when dest has local/
|
||||
if [[ "$rel_path" == "local/"* ]] && [ -d "$dest_dir/local" ]; then
|
||||
files_skipped=$((files_skipped + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Preserve alternatives directory - only update if source file is newer
|
||||
if [[ "$rel_path" == "modules/commands/alternatives/"* ]] && [ -d "$dest_dir/modules/commands/alternatives" ]; then
|
||||
if [ -f "$dest_file" ]; then
|
||||
|
||||
@@ -89,9 +89,12 @@ class CommandManager:
|
||||
self.command_prefix = self.load_command_prefix()
|
||||
|
||||
# Initialize plugin loader and load all plugins
|
||||
self.plugin_loader = PluginLoader(
|
||||
bot, local_commands_dir=str(bot.bot_root / "local" / "commands")
|
||||
local_commands_dir = (
|
||||
str(bot._local_root / "commands")
|
||||
if getattr(bot, "_local_root", None) is not None
|
||||
else str(bot.bot_root / "local" / "commands")
|
||||
)
|
||||
self.plugin_loader = PluginLoader(bot, local_commands_dir=local_commands_dir)
|
||||
self.commands = self.plugin_loader.load_all_plugins()
|
||||
|
||||
# Cache for internet connectivity status to avoid checking on every command
|
||||
|
||||
@@ -198,6 +198,26 @@ def validate_config(config_path: str) -> List[Tuple[str, str]]:
|
||||
msg = _check_path_writable(db_path, bot_root, "Database path")
|
||||
if msg:
|
||||
results.append((SEVERITY_WARNING, msg))
|
||||
# Optional: local_dir_path should exist and be readable when set
|
||||
local_dir = config.get("Bot", "local_dir_path", fallback="").strip()
|
||||
if local_dir:
|
||||
try:
|
||||
resolved = _resolve_path(local_dir, bot_root)
|
||||
if not resolved.exists():
|
||||
results.append((
|
||||
SEVERITY_WARNING,
|
||||
f"Local plugins path '{resolved}' does not exist; local commands/plugins will not load.",
|
||||
))
|
||||
elif not os.access(str(resolved), os.R_OK):
|
||||
results.append((
|
||||
SEVERITY_WARNING,
|
||||
f"Local plugins path '{resolved}' is not readable.",
|
||||
))
|
||||
except (OSError, RuntimeError):
|
||||
results.append((
|
||||
SEVERITY_WARNING,
|
||||
f"Local plugins path: cannot resolve '{local_dir}'.",
|
||||
))
|
||||
if config.has_section("Logging"):
|
||||
log_file = config.get("Logging", "log_file", fallback="").strip()
|
||||
if log_file:
|
||||
|
||||
+10
-3
@@ -214,7 +214,7 @@ class MeshCoreBot:
|
||||
self.logger.info("Initializing service plugin loader")
|
||||
try:
|
||||
self.service_loader = ServicePluginLoader(
|
||||
self, local_services_dir=str(self.bot_root / "local" / "service_plugins")
|
||||
self, local_services_dir=str(self._local_root / "service_plugins")
|
||||
)
|
||||
self.services = self.service_loader.load_all_services()
|
||||
self.logger.info(f"Service plugin loader initialized with {len(self.services)} service(s)")
|
||||
@@ -269,8 +269,15 @@ class MeshCoreBot:
|
||||
|
||||
# Force UTF-8 so emoji and non-ASCII characters in config.ini parse on Windows.
|
||||
self.config.read(self.config_file, encoding="utf-8")
|
||||
# Resolve local plugins directory from [Bot] local_dir_path (default: local)
|
||||
self._local_root = Path(
|
||||
resolve_path(
|
||||
self.config.get("Bot", "local_dir_path", fallback="local"),
|
||||
self.bot_root,
|
||||
)
|
||||
)
|
||||
# Merge local config if present (user plugins/services settings)
|
||||
local_config = self.bot_root / "local" / "config.ini"
|
||||
local_config = self._local_root / "config.ini"
|
||||
if local_config.exists():
|
||||
self.config.read(local_config, encoding="utf-8")
|
||||
|
||||
@@ -335,7 +342,7 @@ class MeshCoreBot:
|
||||
|
||||
# Reload the config
|
||||
self.config.read(self.config_file, encoding="utf-8")
|
||||
local_config = self.bot_root / "local" / "config.ini"
|
||||
local_config = self._local_root / "config.ini"
|
||||
if local_config.exists():
|
||||
self.config.read(local_config, encoding="utf-8")
|
||||
|
||||
|
||||
Reference in new issue
Block a user