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

137 lines
7.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`):
```python
# 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`):
```python
# 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`:
```ini
[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:
```python
# 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:
```python
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](service-plugins.md) — built-in services and how they are enabled.
- [Check-in API](checkin-api.md) — 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](checkin-api.md)** for the API contract if you run or build a server to receive submissions.