Local plugins and services
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
| 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
.pyfiles inlocal/commands/(except__init__.py). - Each file must define exactly one class that inherits from
BaseCommandand is not the base class itself. - Use
bot.configfor options; you can put your section in local/config.ini (e.g.[HelloLocal_Command]) and read withself.get_config_value('HelloLocal_Command', 'enabled', fallback=True, value_type='bool')orself.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
.pyfiles inlocal/service_plugins/(excluding__init__.py,base_service.py, and*_utils.py). - The class must inherit from
BaseServicePluginand implementstart()andstop(). - To enable it, add a section in local/config.ini (or main config) with
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.configcontains both; later file wins on overlapping sections/keys. - Put options for your local plugins in local/config.ini to keep main
config.iniclean. Use the same section naming as built-in plugins (e.g.[MyCommand_Command]for a command, or aconfig_sectionfor a service). - After a config reload (e.g. via the
reloadcommand), both main config andlocal/config.iniare 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 bot’s rate limiters can block the second and later sends. You’ll see a warning like “Rate limited. Wait X seconds.”
What’s going on
- Global rate limit (
[Bot]rate_limit_seconds, default 10): minimum time between any two bot replies. If you don’t 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:
- Use
skip_user_rate_limit=Truefor 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 aren’t blocked by the “10 second” global window. - 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 1–1.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.