-
Notifications
You must be signed in to change notification settings - Fork 11
Community Plugin Development Guide
Welcome to the MMRelay plugin development guide! This document will get you started with writing plugins for the relay system. It covers setting up a development environment, understanding the BasePlugin
class, and creating your first plugin. This guide is here to help you extend the relay's functionality with custom plugins tailored to your specific needs.
To develop plugins for MMRelay, you'll need:
- Python 3.8+
- A working installation of the MMRelay application.
- Familiarity with Python and some experience with asynchronous programming (if you're new to these, this is a good way to learn!).
- A text editor or IDE (e.g., VS Code, PyCharm. I prefer VSCodium).
Plugins in MMRelay are Python classes that extend what the relay can do. All plugins inherit from a shared base class called BasePlugin
. This class provides essential methods and utilities for message handling, logging, and data persistence. By using BasePlugin
as your starting point, you can create plugins that interact with either the Meshtastic meshnet, Matrix rooms, or both.
The BasePlugin
is designed to provide a consistent interface for all plugins. Here's a quick look at some of its important features:
-
Logging: Each plugin has its own logger (
self.logger
) to help with tracking actions and debugging. -
Data Storage: Methods like
store_node_data()
,get_node_data()
, anddelete_node_data()
let plugins persistently store data specific to nodes. - Message Handling: Plugins can react to incoming messages from Meshtastic or Matrix by implementing specific methods.
-
Configuration Options: Plugins can access configuration options like
channels
from theconfig.yaml
file. Note that theplugin_response_delay
is now configured globally under themeshtastic
section.
Each plugin must implement two key methods:
handle_meshtastic_message(packet, formatted_message, longname, meshnet_name)
handle_room_message(room, event, full_message)
These functions define how your plugin will handle incoming messages from Meshtastic nodes and Matrix rooms, respectively.
Let's start with a simple example to get your feet wet. This plugin will respond to a specific command without handling channels or DMs, making it easier to understand the basics.
Plugins should be in their own project repositories. Create a new project repository on GitHub, GitLab, Codeberg, etc. Clone your new repo to your local machine and open it in your editor. Inside your cloned project, create a new file for the plugin.
Note: For an easy start, you can fork this template repository mmr-plugin-template and add your own code.
For this example, create a file named simple_responder.py
in your project repository.
Every plugin must inherit from BasePlugin
and set its unique plugin_name
. Here's the complete code for the simple_responder
plugin:
from plugins.base_plugin import BasePlugin
from meshtastic_utils import connect_meshtastic
from matrix_utils import bot_command
class Plugin(BasePlugin):
plugin_name = "simple_responder"
async def handle_meshtastic_message(self, packet, formatted_message, longname, meshnet_name):
if "decoded" in packet and "text" in packet["decoded"]:
message = packet["decoded"]["text"].strip()
if message == "!hello":
meshtastic_client = connect_meshtastic()
# Respond with a greeting
meshtastic_client.sendText(text="Hello from Meshtastic!", channelIndex=0)
return True # Indicate that we handled the message
return False # Indicate that we did not handle the message
async def handle_room_message(self, room, event, full_message):
if bot_command("hello", event):
await self.send_matrix_message(room.room_id, "Hello from Matrix!")
return True # Indicate that we handled the message
return False # Indicate that we did not handle the message
This example avoids complexities of channel and DM handling. It uses the bot_command
function from matrix_utils.py
to check if the message is a command directed at the bot, and connect_meshtastic
from meshtastic_utils.py
to send a message to the mesh network.
If your plugin is hosted in a repository and you want the relay to clone it automatically, specify the repository and tag:
community-plugins:
simple_responder:
active: true
repository: https://github.com/YourUsername/SimpleResponder.git
tag: main
After adding the plugin to your configuration, restart the relay. You should see a line in your log indicating that the simple_responder plugin has started:
DEBUG:Plugin:simple_responder:Started with priority=10
You can then send messages via Meshtastic or Matrix to verify that the plugin is working as expected.
Now, let's create a HelloWorld plugin. This plugin will simply log "Hello world" when it receives a message from either Meshtastic or Matrix, but won't send any responses.
Create a file named hello_world.py
and add the following code:
from plugins.base_plugin import BasePlugin
class Plugin(BasePlugin):
plugin_name = "hello_world"
async def handle_meshtastic_message(self, packet, formatted_message, longname, meshnet_name):
self.logger.debug("Hello world, Meshtastic")
return False # Indicate that we did not handle the message
async def handle_room_message(self, room, event, full_message):
self.logger.debug("Hello world, Matrix")
return False # Indicate that we did not handle the message
Activate this plugin in your config.yaml
just like you did with the simple_responder
plugin.
Besides the methods in BasePlugin
, there are several functions in the relay's codebase you can use to avoid reinventing the wheel:
-
Matrix Utilities:
-
bot_command(command, event)
: A function frommatrix_utils.py
that helps determine if a Matrix message is a command directed at the bot.
Example Usage:
from matrix_utils import bot_command async def handle_room_message(self, room, event, full_message): if bot_command("status", event): # Handle the 'status' command await self.send_matrix_message(room.room_id, "System is running smoothly.") return True # Indicate that we handled the message return False
-
-
Meshtastic Utilities:
-
connect_meshtastic()
: A function frommeshtastic_utils.py
that returns the Meshtastic client interface. Useful for sending messages or accessing node information.
from meshtastic_utils import connect_meshtastic meshtastic_client = connect_meshtastic()
-
-
Database Utilities:
-
For storing plugin-specific data, use the data persistence methods provided by
BasePlugin
. If you need to access node information like long names or short names, you can use functions fromdb_utils.py
.-
get_longname(meshtastic_id)
: Retrieve the longname for a given Meshtastic ID. -
get_shortname(meshtastic_id)
: Retrieve the shortname for a given Meshtastic ID.
-
from db_utils import get_longname, get_shortname longname = get_longname(sender_id) shortname = get_shortname(sender_id)
-
As you get more comfortable with plugin development, you can explore advanced features to enhance your plugins.
Plugins can respond differently to DMs and messages in specific channels. This is controlled by the channels
setting in your plugin's configuration.
To make your plugin respond only to DMs and ignore all channel messages, set the channels
list to be empty:
plugins:
my_plugin:
active: true
channels: [] # Empty list: plugin only responds to DMs
In your plugin, check whether to respond based on the channel and whether it's a DM using is_channel_enabled
:
if not self.is_channel_enabled(channel, is_direct_message=is_direct_message):
self.logger.debug(f"Channel {channel} not enabled for plugin '{self.plugin_name}'")
return False
To make your plugin respond to all mapped channels (defined in matrix_rooms
), either omit the channels
setting or comment it out:
plugins:
my_plugin:
active: true
# channels: [0,1,3,4] # Commented out or omitted; plugin responds to all mapped channels
By default, if channels
is not specified, the plugin responds to all mapped channels. The is_channel_enabled
method handles this logic internally.
To make your plugin respond only to specific channels, list them in the channels
setting:
plugins:
my_plugin:
active: true
channels: [0, 1, 3, 4] # List of Meshtastic channels to respond to
By default, plugins always respond to DMs if active, regardless of channels
configuration. The is_channel_enabled
method in BasePlugin
ensures DMs are always enabled:
def is_channel_enabled(self, channel, is_direct_message=False):
if is_direct_message:
return True # Always respond to DMs if the plugin is active
else:
return channel in self.channels
When processing a message, determine if it's a DM and pass is_direct_message
:
# Determine if the message is a direct message
toId = packet.get("to")
myId = meshtastic_client.myInfo.my_node_num # Relay's own node number
if toId == myId:
is_direct_message = True
else:
is_direct_message = False
if not self.is_channel_enabled(channel, is_direct_message=is_direct_message):
return False
This ensures your plugin responds appropriately to DMs and channel messages based on your configuration.
BasePlugin
provides easy-to-use methods for saving and retrieving plugin-specific data. For instance, if your plugin needs to track statistics for each node:
# Store node data
self.store_node_data(meshtastic_id, node_data)
# Retrieve node data
node_data = self.get_node_data(meshtastic_id)
If your plugin needs to perform periodic tasks, use the built-in scheduling capabilities from BasePlugin
. The start()
method lets you set up recurring background jobs:
def start(self):
schedule.every(5).minutes.do(self.background_job)
You can create specific commands for handling messages from the Meshtastic network. Modify handle_meshtastic_message()
to parse commands from Meshtastic nodes.
Example Using Existing Functions:
from meshtastic_utils import connect_meshtastic
async def handle_meshtastic_message(self, packet, formatted_message, longname, meshnet_name):
if "decoded" in packet and "text" in packet["decoded"]:
message = packet["decoded"]["text"].strip()
channel = packet.get("channel", 0)
meshtastic_client = connect_meshtastic()
# Determine if the message is a direct message
toId = packet.get("to")
myId = meshtastic_client.myInfo.my_node_num # Relay's own node number
if toId == myId:
is_direct_message = True
else:
is_direct_message = False
if not self.is_channel_enabled(channel, is_direct_message=is_direct_message):
return False
if message == "!ping":
# Wait for the response delay
await asyncio.sleep(self.get_response_delay())
fromId = packet.get("fromId")
if is_direct_message:
# Respond via DM
meshtastic_client.sendText(
text="pong",
destinationId=fromId,
)
else:
# Respond in the same channel
meshtastic_client.sendText(
text="pong",
channelIndex=channel,
)
return True # Indicate that we handled the message
return False # Indicate that we did not handle the message
Note on Response Delay: If your plugin automatically responds to mesh commands, respect the plugin_response_delay
configuration option. It's set globally under meshtastic
in config.yaml
. Retrieve it using self.get_response_delay()
and apply it before sending your response, as shown above. This helps manage network traffic and prevents overwhelming the mesh network.
Example config.yaml
Setting:
meshtastic:
plugin_response_delay: 5 # Delay in seconds before plugin responses; defaults to 3
The bot_command
function is a helper utility located in matrix_utils.py
. It's designed to figure out if an incoming Matrix message is a command directed at your bot. It handles various ways different Matrix clients format messages and mentions.
Note: You don't need to modify this function. It's already included in the matrix_utils.py
file. You can simply import and use it in your plugins as shown in the examples.
For reference, here's the implementation of bot_command()
:
def bot_command(command, event):
"""
Checks if the given command is directed at the bot,
accounting for variations in different Matrix clients.
"""
full_message = event.body.strip()
content = event.source.get("content", {})
formatted_body = content.get("formatted_body", "")
# Remove HTML tags and extract the text content
text_content = re.sub(r"<[^>]+>", "", formatted_body).strip()
# Check if the message starts with bot_user_id or bot_user_name
if full_message.startswith(bot_user_id) or text_content.startswith(bot_user_id):
# Construct a regex pattern to match variations of bot mention and command
pattern = rf"^(?:{re.escape(bot_user_id)}|{re.escape(bot_user_name)}|[#@].+?)[,:;]?\s*!{command}$"
return bool(re.match(pattern, full_message)) or bool(re.match(pattern, text_content))
elif full_message.startswith(bot_user_name) or text_content.startswith(bot_user_name):
# Construct a regex pattern to match variations of bot mention and command
pattern = rf"^(?:{re.escape(bot_user_id)}|{re.escape(bot_user_name)}|[#@].+?)[,:;]?\s*!{command}$"
return bool(re.match(pattern, full_message)) or bool(re.match(pattern, text_content))
else:
return False
-
Use Logging Liberally: Use
self.logger
to record key events, errors, or other relevant info for debugging and monitoring. - Keep Plugins Modular: Each plugin should handle a distinct piece of functionality for easier maintenance and debugging.
- Avoid Blocking Operations: The relay is asynchronous, so be careful not to use blocking calls that could delay message handling.
-
Respect Plugin Priorities: Set plugin priorities appropriately by defining the
priority
attribute within your plugin class to control the order of message processing. -
Handle Response Delays: If your plugin sends automatic responses, use
await asyncio.sleep(self.get_response_delay())
to respect the globally configured response delay. -
Manage Channel Responses: Use
self.is_channel_enabled(channel, is_direct_message=is_direct_message)
to control where your plugin responds and ensure it handles DMs appropriately. -
Leverage Existing Functions: Utilize functions from
matrix_utils.py
,meshtastic_utils.py
, anddb_utils.py
to simplify your plugin code and maintain consistency.
Now that you know the basics, consider adding more complex features to your plugin. You could interact with external APIs, respond to specific commands, or handle more detailed data from the Meshtastic network. Check out existing plugins like nodes_plugin.py
or weather_plugin.py
for more ideas and examples.
If you have questions or run into issues, feel free to ask in the project's Matrix room #mmrelay:meshnet.club.
Happy coding!