mmrelay 1.0.8__py3-none-any.whl → 1.0.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mmrelay might be problematic. Click here for more details.

mmrelay/__init__.py CHANGED
@@ -3,16 +3,15 @@ Meshtastic Matrix Relay - Bridge between Meshtastic mesh networks and Matrix cha
3
3
  """
4
4
 
5
5
  import os
6
-
7
- import pkg_resources
6
+ from importlib.metadata import PackageNotFoundError, version
8
7
 
9
8
  # First try to get version from environment variable (GitHub tag)
10
9
  if "GITHUB_REF_NAME" in os.environ:
11
10
  __version__ = os.environ.get("GITHUB_REF_NAME")
12
11
  else:
13
- # Fall back to setup.cfg metadata using pkg_resources (compatible with PyInstaller)
12
+ # Fall back to package metadata using importlib.metadata (modern replacement for pkg_resources)
14
13
  try:
15
- __version__ = pkg_resources.get_distribution("mmrelay").version
16
- except pkg_resources.DistributionNotFound:
14
+ __version__ = version("mmrelay")
15
+ except PackageNotFoundError:
17
16
  # If all else fails, use hardcoded version
18
- __version__ = "1.0.8"
17
+ __version__ = "1.0.10"
mmrelay/main.py CHANGED
@@ -23,7 +23,7 @@ from mmrelay.db_utils import (
23
23
  from mmrelay.log_utils import get_logger
24
24
  from mmrelay.matrix_utils import connect_matrix, join_matrix_room
25
25
  from mmrelay.matrix_utils import logger as matrix_logger
26
- from mmrelay.matrix_utils import on_room_message, on_room_member
26
+ from mmrelay.matrix_utils import on_room_member, on_room_message
27
27
  from mmrelay.meshtastic_utils import connect_meshtastic
28
28
  from mmrelay.meshtastic_utils import logger as meshtastic_logger
29
29
  from mmrelay.plugin_loader import load_plugins
mmrelay/matrix_utils.py CHANGED
@@ -524,7 +524,9 @@ async def on_room_message(
524
524
  full_display_name = room_display_name
525
525
  else:
526
526
  # Fallback to global display name if room-specific name is not available
527
- display_name_response = await matrix_client.get_displayname(event.sender)
527
+ display_name_response = await matrix_client.get_displayname(
528
+ event.sender
529
+ )
528
530
  full_display_name = display_name_response.displayname or event.sender
529
531
 
530
532
  # If not from a remote meshnet, proceed as normal to relay back to the originating meshnet
@@ -771,32 +773,12 @@ async def on_room_member(room: MatrixRoom, event: RoomMemberEvent) -> None:
771
773
  """
772
774
  Callback to handle room member events, specifically tracking room-specific display name changes.
773
775
  This ensures we detect when users update their display names in specific rooms.
774
- """
775
- # Only track updates from active members
776
- if event.membership != "join":
777
- return
778
-
779
- new_displayname = event.content.get("displayname")
780
- old_displayname = event.prev_content.get("displayname") if event.prev_content else None
781
- user_id = event.state_key
782
- room_id = room.room_id
783
-
784
- # Log display name changes for debugging
785
- if new_displayname != old_displayname:
786
- if new_displayname and old_displayname:
787
- logger.info(
788
- f"[Matrix] {user_id} updated room display name in {room_id}: "
789
- f"'{old_displayname}' → '{new_displayname}'"
790
- )
791
- elif new_displayname and not old_displayname:
792
- logger.info(
793
- f"[Matrix] {user_id} set room display name in {room_id}: '{new_displayname}'"
794
- )
795
- elif not new_displayname and old_displayname:
796
- logger.info(
797
- f"[Matrix] {user_id} removed room display name in {room_id}: '{old_displayname}' → (global name)"
798
- )
799
776
 
800
- # Note: We don't need to maintain a separate cache here since matrix-nio
801
- # automatically updates the room state and room.user_name() will return
802
- # the updated room-specific display name immediately after this event.
777
+ Note: This callback doesn't need to do any explicit processing since matrix-nio
778
+ automatically updates the room state and room.user_name() will return the
779
+ updated room-specific display name immediately after this event.
780
+ """
781
+ # The callback is registered to ensure matrix-nio processes the event,
782
+ # but no explicit action is needed since room.user_name() automatically
783
+ # handles room-specific display names after the room state is updated.
784
+ pass
mmrelay/plugin_loader.py CHANGED
@@ -57,6 +57,28 @@ def get_community_plugin_dirs():
57
57
 
58
58
 
59
59
  def clone_or_update_repo(repo_url, ref, plugins_dir):
60
+ """Clone or update a Git repository for community plugins.
61
+
62
+ Args:
63
+ repo_url (str): Git repository URL to clone/update
64
+ ref (dict): Reference specification with keys:
65
+ - type: "tag" or "branch"
66
+ - value: tag name or branch name
67
+ plugins_dir (str): Directory where the repository should be cloned
68
+
69
+ Returns:
70
+ bool: True if successful, False if clone/update failed
71
+
72
+ Handles complex Git operations including:
73
+ - Cloning new repositories with specific tags/branches
74
+ - Updating existing repositories and switching refs
75
+ - Installing requirements.txt dependencies via pip or pipx
76
+ - Fallback to default branches (main/master) when specified ref fails
77
+ - Robust error handling and logging
78
+
79
+ The function automatically installs Python dependencies if a requirements.txt
80
+ file is found in the repository root.
81
+ """
60
82
  # Extract the repository name from the URL
61
83
  repo_name = os.path.splitext(os.path.basename(repo_url.rstrip("/")))[0]
62
84
  repo_path = os.path.join(plugins_dir, repo_name)
@@ -497,6 +519,27 @@ def clone_or_update_repo(repo_url, ref, plugins_dir):
497
519
 
498
520
 
499
521
  def load_plugins_from_directory(directory, recursive=False):
522
+ """Load plugin classes from Python files in a directory.
523
+
524
+ Args:
525
+ directory (str): Directory path to search for plugin files
526
+ recursive (bool): Whether to search subdirectories recursively
527
+
528
+ Returns:
529
+ list: List of instantiated plugin objects found in the directory
530
+
531
+ Scans for .py files and attempts to import each as a module. Looks for
532
+ a 'Plugin' class in each module and instantiates it if found.
533
+
534
+ Features:
535
+ - Automatic dependency installation for missing imports (via pip/pipx)
536
+ - Compatibility layer for import paths (plugins vs mmrelay.plugins)
537
+ - Proper sys.path management for plugin directory imports
538
+ - Comprehensive error handling and logging
539
+
540
+ Skips files that don't define a Plugin class or have import errors
541
+ that can't be automatically resolved.
542
+ """
500
543
  plugins = []
501
544
  if os.path.isdir(directory):
502
545
  for root, _dirs, files in os.walk(directory):
@@ -617,6 +660,26 @@ def load_plugins_from_directory(directory, recursive=False):
617
660
 
618
661
 
619
662
  def load_plugins(passed_config=None):
663
+ """Load and initialize all active plugins based on configuration.
664
+
665
+ Args:
666
+ passed_config (dict, optional): Configuration dictionary to use.
667
+ If None, uses global config variable.
668
+
669
+ Returns:
670
+ list: List of active plugin instances sorted by priority
671
+
672
+ This is the main plugin loading function that:
673
+ - Loads core plugins from mmrelay.plugins package
674
+ - Processes custom plugins from ~/.mmrelay/plugins/custom and plugins/custom
675
+ - Downloads and loads community plugins from configured Git repositories
676
+ - Filters plugins based on active status in configuration
677
+ - Sorts active plugins by priority and calls their start() method
678
+ - Sets up proper plugin configuration and channel mapping
679
+
680
+ Only plugins explicitly marked as active=true in config are loaded.
681
+ Custom and community plugins are cloned/updated automatically.
682
+ """
620
683
  global sorted_active_plugins
621
684
  global plugins_loaded
622
685
  global config
@@ -20,6 +20,27 @@ config = None
20
20
 
21
21
 
22
22
  class BasePlugin(ABC):
23
+ """Abstract base class for all mmrelay plugins.
24
+
25
+ Provides common functionality for plugin development including:
26
+ - Configuration management and validation
27
+ - Database storage for plugin-specific data
28
+ - Channel and direct message handling
29
+ - Matrix message sending capabilities
30
+ - Scheduling support for background tasks
31
+ - Command matching and routing
32
+
33
+ Attributes:
34
+ plugin_name (str): Unique identifier for the plugin
35
+ max_data_rows_per_node (int): Maximum data rows stored per node (default: 100)
36
+ priority (int): Plugin execution priority (lower = higher priority, default: 10)
37
+
38
+ Subclasses must:
39
+ - Set plugin_name as a class attribute
40
+ - Implement handle_meshtastic_message() and handle_room_message()
41
+ - Optionally override other methods for custom behavior
42
+ """
43
+
23
44
  # Class-level default attributes
24
45
  plugin_name = None # Must be overridden in subclasses
25
46
  max_data_rows_per_node = 100
@@ -27,9 +48,32 @@ class BasePlugin(ABC):
27
48
 
28
49
  @property
29
50
  def description(self):
51
+ """Get the plugin description for help text.
52
+
53
+ Returns:
54
+ str: Human-readable description of plugin functionality
55
+
56
+ Override this property in subclasses to provide meaningful help text
57
+ that will be displayed by the help plugin.
58
+ """
30
59
  return ""
31
60
 
32
61
  def __init__(self, plugin_name=None) -> None:
62
+ """Initialize the plugin with configuration and logging.
63
+
64
+ Args:
65
+ plugin_name (str, optional): Plugin name override. If not provided,
66
+ uses class-level plugin_name attribute.
67
+
68
+ Raises:
69
+ ValueError: If plugin_name is not set via parameter or class attribute
70
+
71
+ Sets up:
72
+ - Plugin-specific logger
73
+ - Configuration from global config
74
+ - Channel mapping and validation
75
+ - Response delay settings
76
+ """
33
77
  # Allow plugin_name to be passed as a parameter for simpler initialization
34
78
  # This maintains backward compatibility while providing a cleaner API
35
79
  super().__init__()
@@ -96,6 +140,19 @@ class BasePlugin(ABC):
96
140
  )
97
141
 
98
142
  def start(self):
143
+ """Start the plugin and set up scheduled tasks if configured.
144
+
145
+ Called automatically when the plugin is loaded. Checks plugin configuration
146
+ for scheduling settings and sets up background jobs accordingly.
147
+
148
+ Supported schedule formats in config:
149
+ - schedule.hours + schedule.at: Run every N hours at specific time
150
+ - schedule.minutes + schedule.at: Run every N minutes at specific time
151
+ - schedule.hours: Run every N hours
152
+ - schedule.minutes: Run every N minutes
153
+
154
+ Creates a daemon thread to run the scheduler if any schedule is configured.
155
+ """
99
156
  if "schedule" not in self.config or (
100
157
  "at" not in self.config["schedule"]
101
158
  and "hours" not in self.config["schedule"]
@@ -137,9 +194,27 @@ class BasePlugin(ABC):
137
194
 
138
195
  # trunk-ignore(ruff/B027)
139
196
  def background_job(self):
197
+ """Background task executed on schedule.
198
+
199
+ Override this method in subclasses to implement scheduled functionality.
200
+ Called automatically based on schedule configuration in start().
201
+
202
+ Default implementation does nothing.
203
+ """
140
204
  pass # Implement in subclass if needed
141
205
 
142
206
  def strip_raw(self, data):
207
+ """Recursively remove 'raw' keys from data structures.
208
+
209
+ Args:
210
+ data: Data structure (dict, list, or other) to clean
211
+
212
+ Returns:
213
+ Cleaned data structure with 'raw' keys removed
214
+
215
+ Useful for cleaning packet data before logging or storage to remove
216
+ binary protobuf data that's not human-readable.
217
+ """
143
218
  if isinstance(data, dict):
144
219
  data.pop("raw", None)
145
220
  for k, v in data.items():
@@ -149,19 +224,59 @@ class BasePlugin(ABC):
149
224
  return data
150
225
 
151
226
  def get_response_delay(self):
227
+ """Get the configured response delay for meshtastic messages.
228
+
229
+ Returns:
230
+ int: Delay in seconds before sending responses (default: 3)
231
+
232
+ Used to prevent message flooding and ensure proper radio etiquette.
233
+ Delay is configured via meshtastic.plugin_response_delay in config.
234
+ """
152
235
  return self.response_delay
153
236
 
154
- # Modified method to accept is_direct_message parameter
155
237
  def is_channel_enabled(self, channel, is_direct_message=False):
238
+ """Check if the plugin should respond on a specific channel.
239
+
240
+ Args:
241
+ channel: Channel identifier to check
242
+ is_direct_message (bool): Whether this is a direct message
243
+
244
+ Returns:
245
+ bool: True if plugin should respond, False otherwise
246
+
247
+ Direct messages always return True if the plugin is active.
248
+ For channel messages, checks if channel is in plugin's configured channels list.
249
+ """
156
250
  if is_direct_message:
157
251
  return True # Always respond to DMs if the plugin is active
158
252
  else:
159
253
  return channel in self.channels
160
254
 
161
255
  def get_matrix_commands(self):
256
+ """Get list of Matrix commands this plugin responds to.
257
+
258
+ Returns:
259
+ list: List of command strings (without ! prefix)
260
+
261
+ Default implementation returns [plugin_name]. Override to provide
262
+ custom commands or multiple command aliases.
263
+ """
162
264
  return [self.plugin_name]
163
265
 
164
266
  async def send_matrix_message(self, room_id, message, formatted=True):
267
+ """Send a message to a Matrix room.
268
+
269
+ Args:
270
+ room_id (str): Matrix room identifier
271
+ message (str): Message content to send
272
+ formatted (bool): Whether to send as formatted HTML (default: True)
273
+
274
+ Returns:
275
+ dict: Response from Matrix API room_send
276
+
277
+ Connects to Matrix using matrix_utils and sends a room message
278
+ with optional HTML formatting via markdown.
279
+ """
165
280
  from mmrelay.matrix_utils import connect_matrix
166
281
 
167
282
  matrix_client = await connect_matrix()
@@ -178,9 +293,26 @@ class BasePlugin(ABC):
178
293
  )
179
294
 
180
295
  def get_mesh_commands(self):
296
+ """Get list of mesh/radio commands this plugin responds to.
297
+
298
+ Returns:
299
+ list: List of command strings (without ! prefix)
300
+
301
+ Default implementation returns empty list. Override to handle
302
+ commands sent over the mesh radio network.
303
+ """
181
304
  return []
182
305
 
183
306
  def store_node_data(self, meshtastic_id, node_data):
307
+ """Store data for a specific node, appending to existing data.
308
+
309
+ Args:
310
+ meshtastic_id (str): Node identifier
311
+ node_data: Data to store (single item or list)
312
+
313
+ Retrieves existing data, appends new data, trims to max_data_rows_per_node,
314
+ and stores back to database. Use for accumulating time-series data.
315
+ """
184
316
  data = self.get_node_data(meshtastic_id=meshtastic_id)
185
317
  data = data[-self.max_data_rows_per_node :]
186
318
  if isinstance(node_data, list):
@@ -190,21 +322,56 @@ class BasePlugin(ABC):
190
322
  store_plugin_data(self.plugin_name, meshtastic_id, data)
191
323
 
192
324
  def set_node_data(self, meshtastic_id, node_data):
325
+ """Replace all data for a specific node.
326
+
327
+ Args:
328
+ meshtastic_id (str): Node identifier
329
+ node_data: Data to store (replaces existing data)
330
+
331
+ Completely replaces existing data for the node, trimming to
332
+ max_data_rows_per_node if needed. Use when you want to reset
333
+ or completely replace a node's data.
334
+ """
193
335
  node_data = node_data[-self.max_data_rows_per_node :]
194
336
  store_plugin_data(self.plugin_name, meshtastic_id, node_data)
195
337
 
196
338
  def delete_node_data(self, meshtastic_id):
339
+ """Delete all stored data for a specific node.
340
+
341
+ Args:
342
+ meshtastic_id (str): Node identifier
343
+
344
+ Returns:
345
+ bool: True if deletion succeeded, False otherwise
346
+ """
197
347
  return delete_plugin_data(self.plugin_name, meshtastic_id)
198
348
 
199
349
  def get_node_data(self, meshtastic_id):
350
+ """Retrieve stored data for a specific node.
351
+
352
+ Args:
353
+ meshtastic_id (str): Node identifier
354
+
355
+ Returns:
356
+ list: Stored data for the node (JSON deserialized)
357
+ """
200
358
  return get_plugin_data_for_node(self.plugin_name, meshtastic_id)
201
359
 
202
360
  def get_data(self):
361
+ """Retrieve all stored data for this plugin across all nodes.
362
+
363
+ Returns:
364
+ list: List of tuples containing raw data entries
365
+
366
+ Returns raw data without JSON deserialization. Use get_node_data()
367
+ for individual node data that's automatically deserialized.
368
+ """
203
369
  return get_plugin_data(self.plugin_name)
204
370
 
205
371
  def get_plugin_data_dir(self, subdir=None):
206
372
  """
207
373
  Returns the directory for storing plugin-specific data files.
374
+
208
375
  Creates the directory if it doesn't exist.
209
376
 
210
377
  Args:
@@ -230,6 +397,17 @@ class BasePlugin(ABC):
230
397
  return plugin_dir
231
398
 
232
399
  def matches(self, event):
400
+ """Check if a Matrix event matches this plugin's commands.
401
+
402
+ Args:
403
+ event: Matrix room event to check
404
+
405
+ Returns:
406
+ bool: True if event matches plugin commands, False otherwise
407
+
408
+ Uses bot_command() utility to check if the event contains any of
409
+ the plugin's matrix commands with proper bot command syntax.
410
+ """
233
411
  from mmrelay.matrix_utils import bot_command
234
412
 
235
413
  # Pass the entire event to bot_command
@@ -2,6 +2,19 @@ from mmrelay.plugins.base_plugin import BasePlugin
2
2
 
3
3
 
4
4
  class Plugin(BasePlugin):
5
+ """Debug plugin for logging packet information.
6
+
7
+ A low-priority plugin that logs all received meshtastic packets
8
+ for debugging and development purposes. Strips raw binary data
9
+ before logging to keep output readable.
10
+
11
+ Configuration:
12
+ priority: 1 (runs first, before other plugins)
13
+
14
+ Never intercepts messages (always returns False) so other plugins
15
+ can still process the same packets.
16
+ """
17
+
5
18
  plugin_name = "debug"
6
19
  priority = 1
7
20
 
@@ -5,13 +5,28 @@ from mmrelay.plugins.base_plugin import BasePlugin
5
5
 
6
6
 
7
7
  class Plugin(BasePlugin):
8
- plugin_name = "help"
8
+ """Help command plugin for listing available commands.
9
+
10
+ Provides users with information about available relay commands
11
+ and plugin functionality.
12
+
13
+ Commands:
14
+ !help: List all available commands
15
+ !help <command>: Show detailed help for a specific command
16
+
17
+ Dynamically discovers available commands from all loaded plugins
18
+ and their descriptions.
19
+ """
9
20
 
10
- # No __init__ method needed with the simplified plugin system
11
- # The BasePlugin will automatically use the class-level plugin_name
21
+ plugin_name = "help"
12
22
 
13
23
  @property
14
24
  def description(self):
25
+ """Get plugin description.
26
+
27
+ Returns:
28
+ str: Description of help functionality
29
+ """
15
30
  return "List supported relay commands"
16
31
 
17
32
  async def handle_meshtastic_message(
@@ -20,9 +35,19 @@ class Plugin(BasePlugin):
20
35
  return False
21
36
 
22
37
  def get_matrix_commands(self):
38
+ """Get Matrix commands handled by this plugin.
39
+
40
+ Returns:
41
+ list: List containing the help command
42
+ """
23
43
  return [self.plugin_name]
24
44
 
25
45
  def get_mesh_commands(self):
46
+ """Get mesh commands handled by this plugin.
47
+
48
+ Returns:
49
+ list: Empty list (help only works via Matrix)
50
+ """
26
51
  return []
27
52
 
28
53
  async def handle_room_message(self, room, event, full_message):
@@ -156,6 +156,19 @@ class TextLabel(staticmaps.Object):
156
156
 
157
157
 
158
158
  def anonymize_location(lat, lon, radius=1000):
159
+ """Add random offset to GPS coordinates for privacy protection.
160
+
161
+ Args:
162
+ lat (float): Original latitude
163
+ lon (float): Original longitude
164
+ radius (int): Maximum offset distance in meters (default: 1000)
165
+
166
+ Returns:
167
+ tuple: (new_lat, new_lon) with random offset applied
168
+
169
+ Adds random offset within specified radius to obscure exact locations
170
+ while maintaining general geographic area for mapping purposes.
171
+ """
159
172
  # Generate random offsets for latitude and longitude
160
173
  lat_offset = random.uniform(-radius / 111320, radius / 111320)
161
174
  lon_offset = random.uniform(
@@ -233,6 +246,24 @@ async def send_image(client: AsyncClient, room_id: str, image: Image.Image):
233
246
 
234
247
 
235
248
  class Plugin(BasePlugin):
249
+ """Static map generation plugin for mesh node locations.
250
+
251
+ Generates static maps showing positions of mesh nodes with labeled markers.
252
+ Supports customizable zoom levels, image sizes, and privacy features.
253
+
254
+ Commands:
255
+ !map: Generate map with default settings
256
+ !map zoom=N: Set zoom level (0-30)
257
+ !map size=W,H: Set image dimensions (max 1000x1000)
258
+
259
+ Configuration:
260
+ zoom (int): Default zoom level (default: 8)
261
+ image_width/image_height (int): Default image size (default: 1000x1000)
262
+ anonymize (bool): Whether to offset coordinates for privacy (default: true)
263
+ radius (int): Anonymization offset radius in meters (default: 1000)
264
+
265
+ Uploads generated maps as images to Matrix rooms.
266
+ """
236
267
  plugin_name = "map"
237
268
 
238
269
  # No __init__ method needed with the simplified plugin system
@@ -1,3 +1,5 @@
1
+ # Note: This plugin was experimental and is not functional.
2
+
1
3
  import base64
2
4
  import json
3
5
  import re
@@ -8,12 +10,36 @@ from mmrelay.plugins.base_plugin import BasePlugin, config
8
10
 
9
11
 
10
12
  class Plugin(BasePlugin):
13
+ """Core mesh-to-Matrix relay plugin.
14
+
15
+ Handles bidirectional message relay between Meshtastic mesh network
16
+ and Matrix chat rooms. Processes radio packets and forwards them
17
+ to configured Matrix rooms, and vice versa.
18
+
19
+ This plugin is fundamental to the relay's core functionality and
20
+ typically runs with high priority to ensure messages are properly
21
+ bridged between the two networks.
22
+
23
+ Configuration:
24
+ max_data_rows_per_node: 50 (reduced storage for performance)
25
+ """
26
+
11
27
  plugin_name = "mesh_relay"
12
28
  max_data_rows_per_node = 50
13
29
 
14
30
  def normalize(self, dict_obj):
15
- """
16
- Packets are either a dict, string dict or string
31
+ """Normalize packet data to consistent dictionary format.
32
+
33
+ Args:
34
+ dict_obj: Packet data (dict, JSON string, or plain string)
35
+
36
+ Returns:
37
+ dict: Normalized packet dictionary with raw data stripped
38
+
39
+ Handles various packet formats:
40
+ - Dict objects (passed through)
41
+ - JSON strings (parsed)
42
+ - Plain strings (wrapped in TEXT_MESSAGE_APP format)
17
43
  """
18
44
  if not isinstance(dict_obj, dict):
19
45
  try:
@@ -24,6 +50,17 @@ class Plugin(BasePlugin):
24
50
  return self.strip_raw(dict_obj)
25
51
 
26
52
  def process(self, packet):
53
+ """Process and prepare packet data for relay.
54
+
55
+ Args:
56
+ packet: Raw packet data to process
57
+
58
+ Returns:
59
+ dict: Processed packet with base64-encoded binary payloads
60
+
61
+ Normalizes packet format and encodes binary payloads as base64
62
+ for JSON serialization and Matrix transmission.
63
+ """
27
64
  packet = self.normalize(packet)
28
65
 
29
66
  if "decoded" in packet and "payload" in packet["decoded"]:
@@ -35,14 +72,39 @@ class Plugin(BasePlugin):
35
72
  return packet
36
73
 
37
74
  def get_matrix_commands(self):
75
+ """Get Matrix commands handled by this plugin.
76
+
77
+ Returns:
78
+ list: Empty list (this plugin handles all traffic, not specific commands)
79
+ """
38
80
  return []
39
81
 
40
82
  def get_mesh_commands(self):
83
+ """Get mesh commands handled by this plugin.
84
+
85
+ Returns:
86
+ list: Empty list (this plugin handles all traffic, not specific commands)
87
+ """
41
88
  return []
42
89
 
43
90
  async def handle_meshtastic_message(
44
91
  self, packet, formatted_message, longname, meshnet_name
45
92
  ):
93
+ """Handle incoming meshtastic message and relay to Matrix.
94
+
95
+ Args:
96
+ packet: Raw packet data (dict or JSON) to relay
97
+ formatted_message (str): Human-readable message extracted from packet
98
+ longname (str): Long name of the sender node
99
+ meshnet_name (str): Name of the mesh network
100
+
101
+ Returns:
102
+ bool: Always returns False to allow other plugins to process the same packet
103
+
104
+ Processes the packet by normalizing and preparing it, connects to the Matrix client,
105
+ checks if the meshtastic channel is mapped to a Matrix room based on config,
106
+ and sends the packet to the appropriate Matrix room.
107
+ """
46
108
  from mmrelay.matrix_utils import connect_matrix
47
109
 
48
110
  packet = self.process(packet)
@@ -80,6 +142,17 @@ class Plugin(BasePlugin):
80
142
  return False
81
143
 
82
144
  def matches(self, event):
145
+ """Check if Matrix event is a relayed radio packet.
146
+
147
+ Args:
148
+ event: Matrix room event object
149
+
150
+ Returns:
151
+ bool: True if event contains embedded meshtastic packet JSON
152
+
153
+ Identifies Matrix messages that contain embedded meshtastic packet
154
+ data by matching the default relay message format "Processed <portnum> radio packet".
155
+ """
83
156
  # Check for the presence of necessary keys in the event
84
157
  content = event.source.get("content", {})
85
158
  body = content.get("body", "")
@@ -90,6 +163,20 @@ class Plugin(BasePlugin):
90
163
  return False
91
164
 
92
165
  async def handle_room_message(self, room, event, full_message):
166
+ """Handle incoming Matrix room message and relay to meshtastic mesh.
167
+
168
+ Args:
169
+ room: Matrix Room object where message was received
170
+ event: Matrix room event containing the message
171
+ full_message (str): Raw message body text
172
+
173
+ Returns:
174
+ bool: True if packet relaying succeeded, False otherwise
175
+
176
+ Checks if the Matrix event matches the expected embedded packet format,
177
+ retrieves the packet JSON, decodes it, reconstructs a MeshPacket,
178
+ connects to the meshtastic client, and sends the packet via the radio.
179
+ """
93
180
  # Use the event for matching instead of full_message
94
181
  if not self.matches(event):
95
182
  return False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mmrelay
3
- Version: 1.0.8
3
+ Version: 1.0.10
4
4
  Summary: Bridge between Meshtastic mesh networks and Matrix chat rooms
5
5
  Home-page: https://github.com/geoffwhittington/meshtastic-matrix-relay
6
6
  Author: Geoff Whittington, Jeremiah K., and contributors
@@ -25,7 +25,7 @@ Requires-Dist: schedule==1.2.2
25
25
  Requires-Dist: platformdirs==4.3.8
26
26
  Requires-Dist: py-staticmaps>=0.4.0
27
27
  Requires-Dist: rich==14.0.0
28
- Requires-Dist: setuptools==80.8.0
28
+ Requires-Dist: setuptools==80.9.0
29
29
  Dynamic: license-file
30
30
 
31
31
  # M<>M Relay
@@ -1,22 +1,22 @@
1
- mmrelay/__init__.py,sha256=m8l2zIk6h1Ba54Ru2fsB0Ujsi_4btG1sZ40XjtYK0Mg,588
1
+ mmrelay/__init__.py,sha256=rWjkMuhqS5lSbzLMKWUx0-48C-MLUQjYdjvYLBj4vv0,595
2
2
  mmrelay/cli.py,sha256=hdPTlcGsXTJC9GEUiScG7b3IFp02B3lwhqgwFpU3NsM,13835
3
3
  mmrelay/config.py,sha256=5VZag8iSc5yLQgvwI76bbpizbtqag74cHnfXCrWHNyA,7910
4
4
  mmrelay/config_checker.py,sha256=UnoHVTXzfdTfFkbmXv9r_Si76v-sxXLb5FOaQSOM45E,4909
5
5
  mmrelay/db_utils.py,sha256=DP2YuKBZtV771wo9X-Z7Ww5txfaIR0inWh1K_oVZ7cA,11430
6
6
  mmrelay/log_utils.py,sha256=FXhaq4WSDHwlqiG3k1BbSxiOl5be4P4Kyr1gsIThOBw,4572
7
- mmrelay/main.py,sha256=4Cgv5bLwX9JLMAYmY4TePRY8XDMEKcj3ghYicBwednA,11301
8
- mmrelay/matrix_utils.py,sha256=7rjjHtm8JNXi2fgZ33L3VRvxomA9gQw3dJD6mqZi_lY,32770
7
+ mmrelay/main.py,sha256=88hmJzv-Fw3CSjcMSlQWKjlZlIlZBjOujMfi85r5uCk,11301
8
+ mmrelay/matrix_utils.py,sha256=D7zhTF6Jg7JVQbyQsu3htEKWhLtV5IUpJ-bTASnmEwI,32032
9
9
  mmrelay/meshtastic_utils.py,sha256=Pd0j7mz008ncBDIjRGyHOIb0U3vKq06uXWoJP-csHcQ,23381
10
- mmrelay/plugin_loader.py,sha256=Gg8E_TKqndMQHiVJwtmlIC-jCgcty-5EsX_5JJ6Onvo,36590
10
+ mmrelay/plugin_loader.py,sha256=NRiXF6Ty1WD9jNXXKvzJh7kE0ba5oICXNVAfMaTPqH4,39247
11
11
  mmrelay/setup_utils.py,sha256=N6qdScHKHEMFKDmT1l7dcLDPNTusZXPkyxrLXjFLhRI,19910
12
12
  mmrelay/plugins/__init__.py,sha256=KVMQIXRhe0wlGj4O3IZ0vOIQRKFkfPYejHXhJL17qrc,51
13
- mmrelay/plugins/base_plugin.py,sha256=AlC7Y0jMh42jouXpJNtpvo4vMbS0KedOUFuJvWmZggY,8962
14
- mmrelay/plugins/debug_plugin.py,sha256=Jziht9Nj_bRO6Rmy7TjfBXaYo5eM3XsenbWFxPpyUs4,443
13
+ mmrelay/plugins/base_plugin.py,sha256=Gf3xrWdYnXu2dnY4TcOhCGy9fZDISna8OI_Pgt_QX8Q,15415
14
+ mmrelay/plugins/debug_plugin.py,sha256=adX0cRJHUEDLldajybPfiRDDlvytkZe5aN_dSgNKP2Y,870
15
15
  mmrelay/plugins/drop_plugin.py,sha256=0GOz0dgLnFST1HTgqrMayrjwmYlnu02QxfnTZtdPZ3U,4671
16
16
  mmrelay/plugins/health_plugin.py,sha256=svV_GfpAVL0QhiVzi3PVZ1mNpsOL1NHSmkRF-Mn_ExE,2250
17
- mmrelay/plugins/help_plugin.py,sha256=GP7i1iWNiJxb_Pbuod0ctveIQYL1FNN8ctUvy24yRNM,1506
18
- mmrelay/plugins/map_plugin.py,sha256=xqQvjuY-VVuuhVQyDJHnj9uZKYaXV0qSaBTbVwfVOfo,10078
19
- mmrelay/plugins/mesh_relay_plugin.py,sha256=E04JU4uKr7KTScahS0VKBJ1xwvq1ULJaeFVYEtceMvA,4273
17
+ mmrelay/plugins/help_plugin.py,sha256=S7nBhsANK46Zv9wPHOVegPGcuYGMErBsxAnrRlSSCwg,2149
18
+ mmrelay/plugins/map_plugin.py,sha256=eHV_t3TFcypBD4xT_OQx0hD6_iGkLJOADjwYVny0PvE,11292
19
+ mmrelay/plugins/mesh_relay_plugin.py,sha256=PIQBACC7Yjq7hzKJJNycHTjkayEC9tsC8-JNG4q_AY8,7563
20
20
  mmrelay/plugins/nodes_plugin.py,sha256=RDabzyG5hKG5aYWecsRUcLSjMCCv6Pngmq2Qpld1A1U,2903
21
21
  mmrelay/plugins/ping_plugin.py,sha256=8uFnT3qfO3RBaTUOx348voIfKpzXB3zTfcT6Gtfc8kM,4070
22
22
  mmrelay/plugins/telemetry_plugin.py,sha256=8SxWv4BLXMUTbiVaD3MjlMMdQyS7S_1OfLlVNAUMSO0,6306
@@ -24,9 +24,9 @@ mmrelay/plugins/weather_plugin.py,sha256=1bQhmiX-enNphzGoFVprU0LcZQX9BvGxWAJAG8W
24
24
  mmrelay/tools/__init__.py,sha256=WFjDQjdevgg19_zT6iEoL29rvb1JPqYSd8708Jn5D7A,838
25
25
  mmrelay/tools/mmrelay.service,sha256=3vqK1VbfXvVftkTrTEOan77aTHeOT36hIAL7HqJsmTg,567
26
26
  mmrelay/tools/sample_config.yaml,sha256=iGOoVxELUEJwfmpFxyeWXXS0RWNsErnf8-6LXyIwH2k,3052
27
- mmrelay-1.0.8.dist-info/licenses/LICENSE,sha256=yceWauM1c0-FHxVplsD7W1-AbSeRaUNlmqT4UO1msBU,1073
28
- mmrelay-1.0.8.dist-info/METADATA,sha256=orw_9ScvoB4pOpJMkQ5WJvfLEZphl7N5OD0oRHpyIC8,5916
29
- mmrelay-1.0.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
30
- mmrelay-1.0.8.dist-info/entry_points.txt,sha256=SJZwGUOEpQ-qx4H8UL4xKFnKeInGUaZNW1I0ddjK7Ws,45
31
- mmrelay-1.0.8.dist-info/top_level.txt,sha256=B_ZLCRm7NYAmI3PipRUyHGymP-C-q16LSeMGzmqJfo4,8
32
- mmrelay-1.0.8.dist-info/RECORD,,
27
+ mmrelay-1.0.10.dist-info/licenses/LICENSE,sha256=yceWauM1c0-FHxVplsD7W1-AbSeRaUNlmqT4UO1msBU,1073
28
+ mmrelay-1.0.10.dist-info/METADATA,sha256=qKs8SIx9dDs3fwEmSd0Dj3Y8mdoKTpQRkyUwBhjUoTE,5917
29
+ mmrelay-1.0.10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
30
+ mmrelay-1.0.10.dist-info/entry_points.txt,sha256=SJZwGUOEpQ-qx4H8UL4xKFnKeInGUaZNW1I0ddjK7Ws,45
31
+ mmrelay-1.0.10.dist-info/top_level.txt,sha256=B_ZLCRm7NYAmI3PipRUyHGymP-C-q16LSeMGzmqJfo4,8
32
+ mmrelay-1.0.10.dist-info/RECORD,,