Files
meshcore-bot/docs/local-plugins.md
agessaman f86fe0b140 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.
2026-03-07 12:31:54 -08:00

7.8 KiB
Raw Permalink Blame History

Local plugins and services

You can add your own command plugins and service plugins without modifying the bots 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

Path Purpose
local/commands/ One Python file per command plugin (subclass of BaseCommand).
local/service_plugins/ One Python file per service plugin (subclass of BaseServicePlugin).
local/config.ini Optional. Merged with main config; use it for your plugins sections.

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):

# local/commands/hello_local.py
from modules.commands.base_command import BaseCommand
from modules.models import MeshMessage


class HelloLocalCommand(BaseCommand):
    name = "hellolocal"
    keywords = ["hellolocal", "hi local"]
    description = "A local greeting command"

    async def execute(self, message: MeshMessage) -> bool:
        return await self.handle_keyword_match(message)
  • The bot discovers all .py files in local/commands/ (except __init__.py).
  • Each file must define exactly one class that inherits from BaseCommand and is not the base class itself.
  • Use bot.config for options; you can put your section in local/config.ini (e.g. [HelloLocal_Command]) and read with self.get_config_value('HelloLocal_Command', 'enabled', fallback=True, value_type='bool') or self.bot.config.get(...).

Restart the bot (or ensure the directory exists and the file is in place before starting). The command will be registered like any other.

Minimal service plugin

Create a file in local/service_plugins/ (e.g. local/service_plugins/my_background_service.py):

# local/service_plugins/my_background_service.py
from modules.service_plugins.base_service import BaseServicePlugin


class MyBackgroundService(BaseServicePlugin):
    config_section = "MyBackground"
    description = "A local background service"

    async def start(self) -> None:
        self._running = True
        self.logger.info("MyBackground service started")

    async def stop(self) -> None:
        self._running = False
        self.logger.info("MyBackground service stopped")
  • The bot discovers all .py files in local/service_plugins/ (excluding __init__.py, base_service.py, and *_utils.py).
  • The class must inherit from BaseServicePlugin and implement start() and stop().
  • To enable it, add a section in local/config.ini (or main config) with enabled = true:
[MyBackground]
enabled = true

Restart the bot so the service is loaded and started.

Configuration

  • Main config is read first, then local/config.ini if it exists. So bot.config contains both; later file wins on overlapping sections/keys.
  • Put options for your local plugins in local/config.ini to keep main config.ini clean. Use the same section naming as built-in plugins (e.g. [MyCommand_Command] for a command, or a config_section for a service).
  • After a config reload (e.g. via the reload command), both main config and local/config.ini are re-read, so on-demand config in your plugins will see updates. Plugin/service instances are not reloaded; only config values.

Duplicate names

If a local command or service has the same name as an already-loaded plugin or service (e.g. you add local/commands/ping.py with name = "ping"), the local one is skipped and a warning is logged. Choose a different name (e.g. pinglocal) to avoid the conflict.

Sending multiple messages (chunking)

When a service plugin sends a long message by splitting it into chunks and calling send_channel_message multiple times, the bots rate limiters can block the second and later sends. Youll see a warning like “Rate limited. Wait X seconds.”

Whats going on

  • Global rate limit ([Bot] rate_limit_seconds, default 10): minimum time between any two bot replies. If you dont skip it, the first send uses the “slot” and the next send within that window is blocked.
  • Bot TX rate limit (bot_tx_rate_limit_seconds, default 1.0): minimum time between bot transmissions on the mesh. This is always enforced.

What to do

Easiest: use the built-in chunked helper so spacing and rate limits are handled for you:

# Service plugin: send long text to your channel in chunks (no manual delay loop)
await self.bot.command_manager.send_channel_messages_chunked(self.channel, chunks)

Commands that reply to a message (channel or DM) can use await self.send_response_chunked(message, chunks).

Alternatively, implement the spacing yourself:

  1. Use skip_user_rate_limit=True for every chunk. That skips the global (and per-user) limits so automated service messages are not blocked by the 10 second global window. That skips the global (and per-user) limits so automated service messages arent blocked by the “10 second” global window.
  2. Space chunks in time so the bot TX limit is satisfied: before each chunk after the first, wait for the bot TX rate limiter and then sleep. Same pattern as the greeter and other multi-part senders:
import asyncio

# chunks = ["first part...", "second part...", ...]
for i, chunk in enumerate(chunks):
    if i > 0:
        await self.bot.bot_tx_rate_limiter.wait_for_tx()
        rate_limit = self.bot.config.getfloat('Bot', 'bot_tx_rate_limit_seconds', fallback=1.0)
        sleep_time = max(rate_limit + 0.5, 1.0)
        await asyncio.sleep(sleep_time)
    await self.bot.command_manager.send_channel_message(
        self.channel, chunk, skip_user_rate_limit=True
    )

So you are allowed to send multiple messages in sequence; you do not need 10 seconds between chunks. Use skip_user_rate_limit=True and about 11.5 seconds (or your configured bot_tx_rate_limit_seconds + buffer) between chunks.

References

  • Service plugins — built-in services and how they are enabled.
  • Check-in API — contract for the optional check-in submission API (local check-in service).
  • Built-in command plugins live in modules/commands/ and modules/commands/alternatives/; you can use them as examples for BaseCommand, get_config_value, handle_keyword_match, etc.
  • Base classes: modules/commands/base_command.py (BaseCommand), modules/service_plugins/base_service.py (BaseServicePlugin).

Check-in service (local)

The repo includes a local service plugin local/service_plugins/checkin_service.py that collects check-ins from a channel (default #meshmonday) on a chosen day (Monday only or daily). You can require a specific phrase (e.g. "check in") or count any message. Optionally it submits check-in data (packet hash, username, message) to a web API secured with an API key. Configuration belongs in local/config.ini under [CheckIn]. See config.ini.example for a commented [CheckIn] block and Check-in API for the API contract if you run or build a server to receive submissions.