- Changed `meshcore` dependency version in `requirements.txt` from `>=2.2.14` to `==2.2.14` for consistency. - Added detailed instructions in `SERVICE-INSTALLATION.md` regarding Python version compatibility and potential f-string issues with Python 3.11. - Introduced a new section in `local-plugins.md` explaining how to handle message chunking and bot rate limits, providing code examples for better clarity.
6.8 KiB
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.
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.
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:
[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.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
- Use
skip_user_rate_limit=Truefor every chunk. 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.