mmrelay 1.0.6__py3-none-any.whl → 1.0.8__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
@@ -15,4 +15,4 @@ else:
15
15
  __version__ = pkg_resources.get_distribution("mmrelay").version
16
16
  except pkg_resources.DistributionNotFound:
17
17
  # If all else fails, use hardcoded version
18
- __version__ = "1.0.1"
18
+ __version__ = "1.0.8"
mmrelay/cli.py CHANGED
@@ -3,6 +3,7 @@ Command-line interface handling for the Meshtastic Matrix Relay.
3
3
  """
4
4
 
5
5
  import argparse
6
+ import importlib.resources
6
7
  import os
7
8
  import sys
8
9
 
@@ -11,6 +12,7 @@ from yaml.loader import SafeLoader
11
12
 
12
13
  # Import version from package
13
14
  from mmrelay import __version__
15
+ from mmrelay.tools import get_sample_config_path
14
16
 
15
17
 
16
18
  def parse_arguments():
@@ -363,29 +365,65 @@ def generate_sample_config():
363
365
  # Ensure the directory exists
364
366
  os.makedirs(os.path.dirname(target_path), exist_ok=True)
365
367
 
366
- # Try to find the sample config file
367
- # First, check in the package directory
368
- package_dir = os.path.dirname(__file__)
369
- sample_config_path = os.path.join(
370
- os.path.dirname(os.path.dirname(package_dir)), "sample_config.yaml"
371
- )
368
+ # Use the helper function to get the sample config path
369
+ sample_config_path = get_sample_config_path()
372
370
 
373
- # If not found, try the repository root
374
- if not os.path.exists(sample_config_path):
375
- repo_root = os.path.dirname(os.path.dirname(__file__))
376
- sample_config_path = os.path.join(repo_root, "sample_config.yaml")
371
+ if os.path.exists(sample_config_path):
372
+ # Copy the sample config file to the target path
373
+ import shutil
377
374
 
378
- # If still not found, try the current directory
379
- if not os.path.exists(sample_config_path):
380
- sample_config_path = os.path.join(os.getcwd(), "sample_config.yaml")
375
+ shutil.copy2(sample_config_path, target_path)
376
+ print(f"Generated sample config file at: {target_path}")
377
+ print(
378
+ "\nEdit this file with your Matrix and Meshtastic settings before running mmrelay."
379
+ )
380
+ return True
381
+
382
+ # If the helper function failed, try using importlib.resources directly
383
+ try:
384
+ # Try to get the sample config from the package resources
385
+ sample_config_content = (
386
+ importlib.resources.files("mmrelay.tools")
387
+ .joinpath("sample_config.yaml")
388
+ .read_text()
389
+ )
390
+
391
+ # Write the sample config to the target path
392
+ with open(target_path, "w") as f:
393
+ f.write(sample_config_content)
381
394
 
382
- if os.path.exists(sample_config_path):
383
- shutil.copy(sample_config_path, target_path)
384
395
  print(f"Generated sample config file at: {target_path}")
385
396
  print(
386
397
  "\nEdit this file with your Matrix and Meshtastic settings before running mmrelay."
387
398
  )
388
399
  return True
389
- else:
400
+ except (FileNotFoundError, ImportError, OSError) as e:
401
+ print(f"Error accessing sample_config.yaml: {e}")
402
+
403
+ # Fallback to traditional file paths if importlib.resources fails
404
+ # First, check in the package directory
405
+ package_dir = os.path.dirname(__file__)
406
+ sample_config_paths = [
407
+ # Check in the tools subdirectory of the package
408
+ os.path.join(package_dir, "tools", "sample_config.yaml"),
409
+ # Check in the package directory
410
+ os.path.join(package_dir, "sample_config.yaml"),
411
+ # Check in the repository root
412
+ os.path.join(
413
+ os.path.dirname(os.path.dirname(package_dir)), "sample_config.yaml"
414
+ ),
415
+ # Check in the current directory
416
+ os.path.join(os.getcwd(), "sample_config.yaml"),
417
+ ]
418
+
419
+ for path in sample_config_paths:
420
+ if os.path.exists(path):
421
+ shutil.copy(path, target_path)
422
+ print(f"Generated sample config file at: {target_path}")
423
+ print(
424
+ "\nEdit this file with your Matrix and Meshtastic settings before running mmrelay."
425
+ )
426
+ return True
427
+
390
428
  print("Error: Could not find sample_config.yaml")
391
429
  return False
mmrelay/main.py CHANGED
@@ -9,6 +9,7 @@ import signal
9
9
  import sys
10
10
 
11
11
  from nio import ReactionEvent, RoomMessageEmote, RoomMessageNotice, RoomMessageText
12
+ from nio.events.room_events import RoomMemberEvent
12
13
 
13
14
  # Import version from package
14
15
  # Import meshtastic_utils as a module to set event_loop
@@ -22,7 +23,7 @@ from mmrelay.db_utils import (
22
23
  from mmrelay.log_utils import get_logger
23
24
  from mmrelay.matrix_utils import connect_matrix, join_matrix_room
24
25
  from mmrelay.matrix_utils import logger as matrix_logger
25
- from mmrelay.matrix_utils import on_room_message
26
+ from mmrelay.matrix_utils import on_room_message, on_room_member
26
27
  from mmrelay.meshtastic_utils import connect_meshtastic
27
28
  from mmrelay.meshtastic_utils import logger as meshtastic_logger
28
29
  from mmrelay.plugin_loader import load_plugins
@@ -109,6 +110,8 @@ async def main(config):
109
110
  )
110
111
  # Add ReactionEvent callback so we can handle matrix reactions
111
112
  matrix_client.add_event_callback(on_room_message, ReactionEvent)
113
+ # Add RoomMemberEvent callback to track room-specific display name changes
114
+ matrix_client.add_event_callback(on_room_member, RoomMemberEvent)
112
115
 
113
116
  # Set up shutdown event
114
117
  shutdown_event = asyncio.Event()
mmrelay/matrix_utils.py CHANGED
@@ -18,6 +18,7 @@ from nio import (
18
18
  UploadResponse,
19
19
  WhoamiError,
20
20
  )
21
+ from nio.events.room_events import RoomMemberEvent
21
22
  from PIL import Image
22
23
 
23
24
  from mmrelay.db_utils import (
@@ -517,8 +518,14 @@ async def on_room_message(
517
518
  meshtastic_id, matrix_room_id, meshtastic_text_db, meshtastic_meshnet_db = (
518
519
  orig
519
520
  )
520
- display_name_response = await matrix_client.get_displayname(event.sender)
521
- full_display_name = display_name_response.displayname or event.sender
521
+ # Get room-specific display name if available, fallback to global display name
522
+ room_display_name = room.user_name(event.sender)
523
+ if room_display_name:
524
+ full_display_name = room_display_name
525
+ else:
526
+ # Fallback to global display name if room-specific name is not available
527
+ display_name_response = await matrix_client.get_displayname(event.sender)
528
+ full_display_name = display_name_response.displayname or event.sender
522
529
 
523
530
  # If not from a remote meshnet, proceed as normal to relay back to the originating meshnet
524
531
  short_display_name = full_display_name[:5]
@@ -578,8 +585,14 @@ async def on_room_message(
578
585
  return
579
586
  else:
580
587
  # Normal Matrix message from a Matrix user
581
- display_name_response = await matrix_client.get_displayname(event.sender)
582
- full_display_name = display_name_response.displayname or event.sender
588
+ # Get room-specific display name if available, fallback to global display name
589
+ room_display_name = room.user_name(event.sender)
590
+ if room_display_name:
591
+ full_display_name = room_display_name
592
+ else:
593
+ # Fallback to global display name if room-specific name is not available
594
+ display_name_response = await matrix_client.get_displayname(event.sender)
595
+ full_display_name = display_name_response.displayname or event.sender
583
596
  short_display_name = full_display_name[:5]
584
597
  prefix = f"{short_display_name}[M]: "
585
598
  logger.debug(f"Processing matrix message from [{full_display_name}]: {text}")
@@ -752,3 +765,38 @@ async def send_room_image(
752
765
  message_type="m.room.message",
753
766
  content={"msgtype": "m.image", "url": upload_response.content_uri, "body": ""},
754
767
  )
768
+
769
+
770
+ async def on_room_member(room: MatrixRoom, event: RoomMemberEvent) -> None:
771
+ """
772
+ Callback to handle room member events, specifically tracking room-specific display name changes.
773
+ 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
+
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.
@@ -29,25 +29,28 @@ class BasePlugin(ABC):
29
29
  def description(self):
30
30
  return ""
31
31
 
32
- def __init__(self) -> None:
33
- # IMPORTANT NOTE FOR PLUGIN DEVELOPERS:
34
- # When creating a plugin that inherits from BasePlugin, you MUST set
35
- # self.plugin_name in your __init__ method BEFORE calling super().__init__()
36
- # Example:
37
- # def __init__(self):
38
- # self.plugin_name = "your_plugin_name" # Set this FIRST
39
- # super().__init__() # Then call parent
40
- #
41
- # Failure to do this will cause command recognition issues and other problems.
42
-
32
+ def __init__(self, plugin_name=None) -> None:
33
+ # Allow plugin_name to be passed as a parameter for simpler initialization
34
+ # This maintains backward compatibility while providing a cleaner API
43
35
  super().__init__()
44
36
 
45
- # Verify plugin_name is properly defined
37
+ # If plugin_name is provided as a parameter, use it
38
+ if plugin_name is not None:
39
+ self.plugin_name = plugin_name
40
+
41
+ # For backward compatibility: if plugin_name is not provided as a parameter,
42
+ # check if it's set as an instance attribute (old way) or use the class attribute
46
43
  if not hasattr(self, "plugin_name") or self.plugin_name is None:
47
- raise ValueError(
48
- f"{self.__class__.__name__} is missing plugin_name definition. "
49
- f"Make sure to set self.plugin_name BEFORE calling super().__init__()"
50
- )
44
+ # Try to get the class-level plugin_name
45
+ class_plugin_name = getattr(self.__class__, "plugin_name", None)
46
+ if class_plugin_name is not None:
47
+ self.plugin_name = class_plugin_name
48
+ else:
49
+ raise ValueError(
50
+ f"{self.__class__.__name__} is missing plugin_name definition. "
51
+ f"Either set class.plugin_name, pass plugin_name to __init__, "
52
+ f"or set self.plugin_name before calling super().__init__()"
53
+ )
51
54
 
52
55
  self.logger = get_logger(f"Plugin:{self.plugin_name}")
53
56
  self.config = {"active": False}
@@ -66,6 +69,8 @@ class BasePlugin(ABC):
66
69
  room.get("meshtastic_channel")
67
70
  for room in config.get("matrix_rooms", [])
68
71
  ]
72
+ else:
73
+ self.mapped_channels = []
69
74
 
70
75
  # Get the channels specified for this plugin, or default to all mapped channels
71
76
  self.channels = self.config.get("channels", self.mapped_channels)
@@ -10,9 +10,8 @@ class Plugin(BasePlugin):
10
10
  plugin_name = "drop"
11
11
  special_node = "!NODE_MSGS!"
12
12
 
13
- def __init__(self):
14
- self.plugin_name = "drop"
15
- super().__init__()
13
+ # No __init__ method needed with the simplified plugin system
14
+ # The BasePlugin will automatically use the class-level plugin_name
16
15
 
17
16
  def get_position(self, meshtastic_client, node_id):
18
17
  for _node, info in meshtastic_client.nodes.items():
@@ -7,9 +7,8 @@ from mmrelay.plugins.base_plugin import BasePlugin
7
7
  class Plugin(BasePlugin):
8
8
  plugin_name = "help"
9
9
 
10
- def __init__(self):
11
- self.plugin_name = "help"
12
- super().__init__()
10
+ # No __init__ method needed with the simplified plugin system
11
+ # The BasePlugin will automatically use the class-level plugin_name
13
12
 
14
13
  @property
15
14
  def description(self):
@@ -235,9 +235,8 @@ async def send_image(client: AsyncClient, room_id: str, image: Image.Image):
235
235
  class Plugin(BasePlugin):
236
236
  plugin_name = "map"
237
237
 
238
- def __init__(self):
239
- self.plugin_name = "map"
240
- super().__init__()
238
+ # No __init__ method needed with the simplified plugin system
239
+ # The BasePlugin will automatically use the class-level plugin_name
241
240
 
242
241
  @property
243
242
  def description(self):
@@ -17,9 +17,8 @@ class Plugin(BasePlugin):
17
17
  plugin_name = "ping"
18
18
  punctuation = string.punctuation
19
19
 
20
- def __init__(self):
21
- self.plugin_name = "ping"
22
- super().__init__()
20
+ # No __init__ method needed with the simplified plugin system
21
+ # The BasePlugin will automatically use the class-level plugin_name
23
22
 
24
23
  @property
25
24
  def description(self):
@@ -9,9 +9,8 @@ from mmrelay.plugins.base_plugin import BasePlugin
9
9
  class Plugin(BasePlugin):
10
10
  plugin_name = "weather"
11
11
 
12
- def __init__(self):
13
- self.plugin_name = "weather"
14
- super().__init__()
12
+ # No __init__ method needed with the simplified plugin system
13
+ # The BasePlugin will automatically use the class-level plugin_name
15
14
 
16
15
  @property
17
16
  def description(self):
mmrelay/setup_utils.py CHANGED
@@ -5,6 +5,8 @@ This module provides simple functions for managing the systemd user service
5
5
  and generating configuration files.
6
6
  """
7
7
 
8
+ import importlib.resources
9
+
8
10
  # Import version from package
9
11
  import os
10
12
  import shutil
@@ -12,6 +14,8 @@ import subprocess
12
14
  import sys
13
15
  from pathlib import Path
14
16
 
17
+ from mmrelay.tools import get_service_template_path
18
+
15
19
 
16
20
  def get_executable_path():
17
21
  """Get the full path to the mmrelay executable.
@@ -103,16 +107,31 @@ def get_template_service_path():
103
107
  os.path.join(sys.prefix, "share", "mmrelay", "mmrelay.service"),
104
108
  os.path.join(sys.prefix, "share", "mmrelay", "tools", "mmrelay.service"),
105
109
  # Check in the user site-packages location
106
- os.path.join(os.path.expanduser("~"), ".local", "share", "mmrelay", "mmrelay.service"),
107
- os.path.join(os.path.expanduser("~"), ".local", "share", "mmrelay", "tools", "mmrelay.service"),
110
+ os.path.join(
111
+ os.path.expanduser("~"), ".local", "share", "mmrelay", "mmrelay.service"
112
+ ),
113
+ os.path.join(
114
+ os.path.expanduser("~"),
115
+ ".local",
116
+ "share",
117
+ "mmrelay",
118
+ "tools",
119
+ "mmrelay.service",
120
+ ),
108
121
  # Check one level up from the package directory
109
122
  os.path.join(os.path.dirname(package_dir), "tools", "mmrelay.service"),
110
123
  # Check two levels up from the package directory (for development)
111
- os.path.join(os.path.dirname(os.path.dirname(package_dir)), "tools", "mmrelay.service"),
124
+ os.path.join(
125
+ os.path.dirname(os.path.dirname(package_dir)), "tools", "mmrelay.service"
126
+ ),
112
127
  # Check in the repository root (for development)
113
- os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "tools", "mmrelay.service"),
128
+ os.path.join(
129
+ os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
130
+ "tools",
131
+ "mmrelay.service",
132
+ ),
114
133
  # Check in the current directory (fallback)
115
- os.path.join(os.getcwd(), "tools", "mmrelay.service")
134
+ os.path.join(os.getcwd(), "tools", "mmrelay.service"),
116
135
  ]
117
136
 
118
137
  # Try each path until we find one that exists
@@ -136,8 +155,10 @@ def get_template_service_content():
136
155
  Returns:
137
156
  str: The content of the template service file, or a default template if not found.
138
157
  """
139
- template_path = get_template_service_path()
140
- if template_path:
158
+ # Use the helper function to get the service template path
159
+ template_path = get_service_template_path()
160
+
161
+ if template_path and os.path.exists(template_path):
141
162
  # Read the template from file
142
163
  try:
143
164
  with open(template_path, "r") as f:
@@ -146,6 +167,28 @@ def get_template_service_content():
146
167
  except Exception as e:
147
168
  print(f"Error reading service template file: {e}")
148
169
 
170
+ # If the helper function failed, try using importlib.resources directly
171
+ try:
172
+ service_template = (
173
+ importlib.resources.files("mmrelay.tools")
174
+ .joinpath("mmrelay.service")
175
+ .read_text()
176
+ )
177
+ return service_template
178
+ except (FileNotFoundError, ImportError, OSError) as e:
179
+ print(f"Error accessing mmrelay.service via importlib.resources: {e}")
180
+
181
+ # Fall back to the file path method
182
+ template_path = get_template_service_path()
183
+ if template_path:
184
+ # Read the template from file
185
+ try:
186
+ with open(template_path, "r") as f:
187
+ service_template = f.read()
188
+ return service_template
189
+ except Exception as e:
190
+ print(f"Error reading service template file: {e}")
191
+
149
192
  # If we couldn't find or read the template file, use a default template
150
193
  print("Using default service template")
151
194
  return """[Unit]
@@ -154,7 +197,7 @@ After=network-online.target
154
197
  Wants=network-online.target
155
198
 
156
199
  [Service]
157
- Type=idle
200
+ Type=simple
158
201
  # The mmrelay binary can be installed via pipx or pip
159
202
  ExecStart=%h/.local/bin/mmrelay --config %h/.mmrelay/config.yaml --logfile %h/.mmrelay/logs/mmrelay.log
160
203
  WorkingDirectory=%h/.mmrelay
@@ -310,6 +353,69 @@ def service_needs_update():
310
353
  return False, "Service file is up to date"
311
354
 
312
355
 
356
+ def check_loginctl_available():
357
+ """Check if loginctl is available on the system.
358
+
359
+ Returns:
360
+ bool: True if loginctl is available, False otherwise.
361
+ """
362
+ try:
363
+ result = subprocess.run(
364
+ ["which", "loginctl"],
365
+ check=False,
366
+ capture_output=True,
367
+ text=True,
368
+ )
369
+ return result.returncode == 0
370
+ except Exception:
371
+ return False
372
+
373
+
374
+ def check_lingering_enabled():
375
+ """Check if user lingering is enabled.
376
+
377
+ Returns:
378
+ bool: True if lingering is enabled, False otherwise.
379
+ """
380
+ try:
381
+ username = os.environ.get("USER", os.environ.get("USERNAME"))
382
+ result = subprocess.run(
383
+ ["loginctl", "show-user", username, "--property=Linger"],
384
+ check=False,
385
+ capture_output=True,
386
+ text=True,
387
+ )
388
+ return result.returncode == 0 and "Linger=yes" in result.stdout
389
+ except Exception:
390
+ return False
391
+
392
+
393
+ def enable_lingering():
394
+ """Enable user lingering using sudo.
395
+
396
+ Returns:
397
+ bool: True if lingering was enabled successfully, False otherwise.
398
+ """
399
+ try:
400
+ username = os.environ.get("USER", os.environ.get("USERNAME"))
401
+ print(f"Enabling lingering for user {username}...")
402
+ result = subprocess.run(
403
+ ["sudo", "loginctl", "enable-linger", username],
404
+ check=False,
405
+ capture_output=True,
406
+ text=True,
407
+ )
408
+ if result.returncode == 0:
409
+ print("Lingering enabled successfully")
410
+ return True
411
+ else:
412
+ print(f"Error enabling lingering: {result.stderr}")
413
+ return False
414
+ except Exception as e:
415
+ print(f"Error enabling lingering: {e}")
416
+ return False
417
+
418
+
313
419
  def install_service():
314
420
  """Install or update the MMRelay user service."""
315
421
  # Check if service already exists
@@ -355,6 +461,27 @@ def install_service():
355
461
 
356
462
  # We don't need to validate the config here as it will be validated when the service starts
357
463
 
464
+ # Check if loginctl is available
465
+ loginctl_available = check_loginctl_available()
466
+ if loginctl_available:
467
+ # Check if user lingering is enabled
468
+ lingering_enabled = check_lingering_enabled()
469
+ if not lingering_enabled:
470
+ print(
471
+ "\nUser lingering is not enabled. This is required for the service to start automatically at boot."
472
+ )
473
+ print(
474
+ "Lingering allows user services to run even when you're not logged in."
475
+ )
476
+ if (
477
+ input(
478
+ "Do you want to enable lingering for your user? (requires sudo) (y/n): "
479
+ )
480
+ .lower()
481
+ .startswith("y")
482
+ ):
483
+ enable_lingering()
484
+
358
485
  # Check if the service is already enabled
359
486
  service_enabled = is_service_enabled()
360
487
  if service_enabled:
@@ -417,6 +544,8 @@ def install_service():
417
544
  print("\nService Status Summary:")
418
545
  print(f" Service File: {service_path}")
419
546
  print(f" Enabled at Boot: {'Yes' if service_enabled else 'No'}")
547
+ if loginctl_available:
548
+ print(f" User Lingering: {'Yes' if check_lingering_enabled() else 'No'}")
420
549
  print(f" Currently Running: {'Yes' if is_service_active() else 'No'}")
421
550
  print("\nService Management Commands:")
422
551
  print_service_commands()
@@ -0,0 +1,28 @@
1
+ """Tools and resources for MMRelay."""
2
+
3
+ import importlib.resources
4
+ import pathlib
5
+
6
+
7
+ def get_sample_config_path():
8
+ """Get the path to the sample config file."""
9
+ try:
10
+ # For Python 3.9+
11
+ return str(
12
+ importlib.resources.files("mmrelay.tools").joinpath("sample_config.yaml")
13
+ )
14
+ except AttributeError:
15
+ # Fallback for older Python versions
16
+ return str(pathlib.Path(__file__).parent / "sample_config.yaml")
17
+
18
+
19
+ def get_service_template_path():
20
+ """Get the path to the service template file."""
21
+ try:
22
+ # For Python 3.9+
23
+ return str(
24
+ importlib.resources.files("mmrelay.tools").joinpath("mmrelay.service")
25
+ )
26
+ except AttributeError:
27
+ # Fallback for older Python versions
28
+ return str(pathlib.Path(__file__).parent / "mmrelay.service")
@@ -0,0 +1,18 @@
1
+ [Unit]
2
+ Description=A Meshtastic <=> Matrix Relay
3
+ After=network-online.target
4
+ Wants=network-online.target
5
+
6
+ [Service]
7
+ Type=simple
8
+ # The mmrelay binary can be installed via pipx or pip
9
+ ExecStart=%h/.local/bin/mmrelay --config %h/.mmrelay/config.yaml --logfile %h/.mmrelay/logs/mmrelay.log
10
+ WorkingDirectory=%h/.mmrelay
11
+ Restart=on-failure
12
+ RestartSec=10
13
+ Environment=PYTHONUNBUFFERED=1
14
+ # Ensure both pipx and pip environments are properly loaded
15
+ Environment=PATH=%h/.local/bin:%h/.local/pipx/venvs/mmrelay/bin:/usr/local/bin:/usr/bin:/bin
16
+
17
+ [Install]
18
+ WantedBy=default.target
@@ -0,0 +1,64 @@
1
+ matrix:
2
+ homeserver: https://example.matrix.org
3
+ access_token: reaalllllyloooooongsecretttttcodeeeeeeforrrrbot # See: https://t2bot.io/docs/access_tokens/
4
+ bot_user_id: "@botuser:example.matrix.org"
5
+
6
+ matrix_rooms: # Needs at least 1 room & channel, but supports all Meshtastic channels
7
+ - id: "#someroomalias:example.matrix.org" # Matrix room aliases & IDs supported
8
+ meshtastic_channel: 0
9
+ - id: "!someroomid:example.matrix.org"
10
+ meshtastic_channel: 2
11
+
12
+ meshtastic:
13
+ connection_type: serial # Choose either "tcp", "serial", or "ble"
14
+ serial_port: /dev/ttyUSB0 # Only used when connection is "serial"
15
+ host: meshtastic.local # Only used when connection is "tcp"
16
+ ble_address: AA:BB:CC:DD:EE:FF # Only used when connection is "ble" - Uses either an address or name from a `meshtastic --ble-scan`
17
+ meshnet_name: Your Meshnet Name # This is displayed in full on Matrix, but is truncated when sent to a Meshnet
18
+ broadcast_enabled: true # Must be set to true to enable Matrix to Meshtastic messages
19
+ detection_sensor: true # Must be set to true to forward messages of Meshtastic's detection sensor module
20
+ plugin_response_delay: 3 # Default response delay in seconds for plugins that respond on the mesh;
21
+ relay_reactions: true # Defaults to false, set to true to enable relay reactions between platforms
22
+
23
+ logging:
24
+ level: info
25
+ #log_to_file: true # Set to true to enable file logging
26
+ #filename: ~/.mmrelay/logs/mmrelay.log # Default location if log_to_file is true
27
+ #max_log_size: 10485760 # 10 MB default if omitted
28
+ #backup_count: 1 # Keeps 1 backup as the default if omitted
29
+ #color_enabled: true # Set to false to disable colored console output
30
+
31
+ #database:
32
+ # path: ~/.mmrelay/data/meshtastic.sqlite # Default location
33
+ # msg_map: # The message map is necessary for the relay_reactions functionality. If `relay_reactions` is set to false, nothing will be saved to the message map.
34
+ # msgs_to_keep: 500 # If set to 0, it will not delete any messages; Defaults to 500
35
+ # wipe_on_restart: true # Clears out the message map when the relay is restarted; Defaults to False
36
+
37
+ # These are core Plugins - Note: Some plugins are experimental and some need maintenance.
38
+ plugins:
39
+ ping:
40
+ active: true
41
+ #channels: [2,3,5] # List of channels the plugin will respond to; DMs are always processed if the plugin is active
42
+ weather:
43
+ active: true
44
+ units: imperial # Options: metric, imperial - Default is metric
45
+ #channels: [] # Empty list, will only respond to DMs
46
+ nodes:
47
+ active: true
48
+ # Does not need to specify channels, as it's a Matrix-only plugin
49
+
50
+ #community-plugins:
51
+ # sample_plugin:
52
+ # active: true
53
+ # repository: https://github.com/username/sample_plugin.git
54
+ # tag: master
55
+ # advanced_plugin:
56
+ # active: false
57
+ # repository: https://github.com/username/advanced_plugin.git
58
+ # tag: v1.2.0
59
+
60
+ #custom-plugins:
61
+ # my_custom_plugin:
62
+ # active: true
63
+ # another_custom_plugin:
64
+ # active: false
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mmrelay
3
- Version: 1.0.6
3
+ Version: 1.0.8
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
@@ -22,72 +22,45 @@ Requires-Dist: requests==2.32.3
22
22
  Requires-Dist: markdown==3.8
23
23
  Requires-Dist: haversine==2.9.0
24
24
  Requires-Dist: schedule==1.2.2
25
- Requires-Dist: platformdirs==4.3.7
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.3.1
28
+ Requires-Dist: setuptools==80.8.0
29
29
  Dynamic: license-file
30
30
 
31
31
  # M<>M Relay
32
32
 
33
- ## Version 1.0 Released! ✨
33
+ ## (Meshtastic <=> Matrix Relay)
34
34
 
35
- **We're excited to announce MMRelay v1.0 with improved packaging, standardized directories, and enhanced CLI!**
35
+ A powerful and easy-to-use relay between Meshtastic devices and Matrix chat rooms, allowing seamless communication across platforms. This opens the door for bridging Meshtastic devices to [many other platforms](https://matrix.org/bridges/).
36
36
 
37
- **Existing users:** Version 1.0 requires a few quick migration steps:
37
+ ## Documentation
38
38
 
39
- 1. Follow the [UPGRADE_TO_V1.md](UPGRADE_TO_V1.md) guide for a smooth transition
40
- 2. Move your configuration to the new standard location (`~/.mmrelay/config.yaml`)
41
- 3. See [ANNOUNCEMENT.md](ANNOUNCEMENT.md) for all the exciting new features
39
+ Visit our [Wiki](https://github.com/geoffwhittington/meshtastic-matrix-relay/wiki) for comprehensive guides and information.
42
40
 
43
- Not ready to upgrade yet? No problem! Run `git checkout 0.10.1` to continue using the previous version.
44
-
45
- ## (Meshtastic <=> Matrix Relay)
46
-
47
- A powerful and easy-to-use relay between Meshtastic devices and Matrix chat rooms, allowing seamless communication across platforms. This opens the door for bridging Meshtastic devices to [many other platforms](https://matrix.org/bridges/).
41
+ - [Installation Instructions](docs/INSTRUCTIONS.md) - Setup and configuration guide
42
+ - [v1.0 Release Announcement](docs/ANNOUNCEMENT.md) - New changes in v1.0
43
+ - [Upgrade Guide](docs/UPGRADE_TO_V1.md) - Migration guidance for existing users
48
44
 
49
45
  ---
50
46
 
51
- ## Getting Started
47
+ ## Quick Start
52
48
 
53
49
  MMRelay runs on Linux, macOS, and Windows.
54
50
 
55
- ### Quick Installation
56
-
57
51
  ```bash
58
52
  # Install using pipx for isolated installation (recommended)
59
53
  pipx install mmrelay
60
54
 
61
- # Pip will also work if you prefer
62
- pip install mmrelay
63
- ```
64
-
65
- For pipx installation instructions, see: [pipx installation guide](https://pipx.pypa.io/stable/installation/#on-linux)
55
+ # Generate a sample configuration file & then edit it
56
+ mmrelay --generate-config
66
57
 
67
- ### Resources
68
-
69
- - **New Users**: See [INSTRUCTIONS.md](INSTRUCTIONS.md) for setup and configuration
70
- - **Existing Users**: See [UPGRADE_TO_V1.md](UPGRADE_TO_V1.md) for migration guidance
71
- - **Configuration**: Review [sample_config.yaml](sample_config.yaml) for examples
72
-
73
- ### Command-Line Options
74
-
75
- ```bash
76
- usage: mmrelay [-h] [--config CONFIG] [--data-dir DATA_DIR] [--log-level {error,warning,info,debug}] [--logfile LOGFILE] [--version] [--generate-config] [--install-service] [--check-config]
77
-
78
- Options:
79
- -h, --help Show this help message and exit
80
- --config CONFIG Path to config file
81
- --data-dir DATA_DIR Base directory for all data (logs, database, plugins)
82
- --log-level {error,warning,info,debug}
83
- Set logging level
84
- --logfile LOGFILE Path to log file (can be overridden by --data-dir)
85
- --version Show version and exit
86
- --generate-config Generate a sample config.yaml file
87
- --install-service Install or update the systemd user service
88
- --check-config Check if the configuration file is valid
58
+ # Start the relay (without --install-service to run manually)
59
+ mmrelay --install-service
89
60
  ```
90
61
 
62
+ For detailed installation and configuration instructions, see the [Installation Guide](docs/INSTRUCTIONS.md).
63
+
91
64
  ---
92
65
 
93
66
  ## Features
@@ -133,7 +106,12 @@ See the full list of core plugins [here](https://github.com/geoffwhittington/mes
133
106
 
134
107
  ### Community & Custom Plugins
135
108
 
136
- It is possible to create custom plugins and share them with the community. Check [example_plugins/README.md](https://github.com/geoffwhittington/meshtastic-matrix-relay/tree/main/example_plugins) and the [Community Plugins Development Guide](https://github.com/geoffwhittington/meshtastic-matrix-relay/wiki/Community-Plugin-Development-Guide).
109
+ MMRelay's plugin system allows you to extend functionality in two ways:
110
+
111
+ - **Custom Plugins**: Create personal plugins for your own use, stored in `~/.mmrelay/plugins/custom/`
112
+ - **Community Plugins**: Share your creations with others or use plugins developed by the community
113
+
114
+ Check the [Community Plugins Development Guide](https://github.com/geoffwhittington/meshtastic-matrix-relay/wiki/Community-Plugin-Development-Guide) in our wiki to get started.
137
115
 
138
116
  ✨️ Visit the [Community Plugins List](https://github.com/geoffwhittington/meshtastic-matrix-relay/wiki/Community-Plugin-List)!
139
117
 
@@ -151,14 +129,12 @@ community-plugins:
151
129
 
152
130
  ### Plugin System
153
131
 
154
- MMRelay features a powerful plugin system with standardized locations:
132
+ Plugins make it easy to extend functionality without modifying the core program. MMRelay features a powerful plugin system with standardized locations:
155
133
 
156
134
  - **Core Plugins**: Pre-installed with the package
157
135
  - **Custom Plugins**: Your own plugins in `~/.mmrelay/plugins/custom/`
158
136
  - **Community Plugins**: Third-party plugins in `~/.mmrelay/plugins/community/`
159
137
 
160
- Plugins make it easy to extend functionality without modifying the core code.
161
-
162
138
  ---
163
139
 
164
140
  ## Getting Started with Matrix
@@ -0,0 +1,32 @@
1
+ mmrelay/__init__.py,sha256=m8l2zIk6h1Ba54Ru2fsB0Ujsi_4btG1sZ40XjtYK0Mg,588
2
+ mmrelay/cli.py,sha256=hdPTlcGsXTJC9GEUiScG7b3IFp02B3lwhqgwFpU3NsM,13835
3
+ mmrelay/config.py,sha256=5VZag8iSc5yLQgvwI76bbpizbtqag74cHnfXCrWHNyA,7910
4
+ mmrelay/config_checker.py,sha256=UnoHVTXzfdTfFkbmXv9r_Si76v-sxXLb5FOaQSOM45E,4909
5
+ mmrelay/db_utils.py,sha256=DP2YuKBZtV771wo9X-Z7Ww5txfaIR0inWh1K_oVZ7cA,11430
6
+ mmrelay/log_utils.py,sha256=FXhaq4WSDHwlqiG3k1BbSxiOl5be4P4Kyr1gsIThOBw,4572
7
+ mmrelay/main.py,sha256=4Cgv5bLwX9JLMAYmY4TePRY8XDMEKcj3ghYicBwednA,11301
8
+ mmrelay/matrix_utils.py,sha256=7rjjHtm8JNXi2fgZ33L3VRvxomA9gQw3dJD6mqZi_lY,32770
9
+ mmrelay/meshtastic_utils.py,sha256=Pd0j7mz008ncBDIjRGyHOIb0U3vKq06uXWoJP-csHcQ,23381
10
+ mmrelay/plugin_loader.py,sha256=Gg8E_TKqndMQHiVJwtmlIC-jCgcty-5EsX_5JJ6Onvo,36590
11
+ mmrelay/setup_utils.py,sha256=N6qdScHKHEMFKDmT1l7dcLDPNTusZXPkyxrLXjFLhRI,19910
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
15
+ mmrelay/plugins/drop_plugin.py,sha256=0GOz0dgLnFST1HTgqrMayrjwmYlnu02QxfnTZtdPZ3U,4671
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
20
+ mmrelay/plugins/nodes_plugin.py,sha256=RDabzyG5hKG5aYWecsRUcLSjMCCv6Pngmq2Qpld1A1U,2903
21
+ mmrelay/plugins/ping_plugin.py,sha256=8uFnT3qfO3RBaTUOx348voIfKpzXB3zTfcT6Gtfc8kM,4070
22
+ mmrelay/plugins/telemetry_plugin.py,sha256=8SxWv4BLXMUTbiVaD3MjlMMdQyS7S_1OfLlVNAUMSO0,6306
23
+ mmrelay/plugins/weather_plugin.py,sha256=1bQhmiX-enNphzGoFVprU0LcZQX9BvGxWAJAG8Wekg0,8596
24
+ mmrelay/tools/__init__.py,sha256=WFjDQjdevgg19_zT6iEoL29rvb1JPqYSd8708Jn5D7A,838
25
+ mmrelay/tools/mmrelay.service,sha256=3vqK1VbfXvVftkTrTEOan77aTHeOT36hIAL7HqJsmTg,567
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.3.1)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,29 +0,0 @@
1
- mmrelay/__init__.py,sha256=8fghAZAeSUb8uIaiit0FGNUWI0ZsBR__QbXNwqNbGtw,588
2
- mmrelay/cli.py,sha256=yBYOkCwGYDJ2gRKPoPltxB_PydX1CPNUerVJHcNuHTo,12355
3
- mmrelay/config.py,sha256=5VZag8iSc5yLQgvwI76bbpizbtqag74cHnfXCrWHNyA,7910
4
- mmrelay/config_checker.py,sha256=UnoHVTXzfdTfFkbmXv9r_Si76v-sxXLb5FOaQSOM45E,4909
5
- mmrelay/db_utils.py,sha256=DP2YuKBZtV771wo9X-Z7Ww5txfaIR0inWh1K_oVZ7cA,11430
6
- mmrelay/log_utils.py,sha256=FXhaq4WSDHwlqiG3k1BbSxiOl5be4P4Kyr1gsIThOBw,4572
7
- mmrelay/main.py,sha256=acgBF-DnL0Rs8MmYAfbNHZ9xjqM3Qc0_oKtATN4iOEE,11085
8
- mmrelay/matrix_utils.py,sha256=GkIVj2bbPHtx1emFMwhEhc1SWHcv4UvkuyZYdb-Wnwo,30511
9
- mmrelay/meshtastic_utils.py,sha256=Pd0j7mz008ncBDIjRGyHOIb0U3vKq06uXWoJP-csHcQ,23381
10
- mmrelay/plugin_loader.py,sha256=Gg8E_TKqndMQHiVJwtmlIC-jCgcty-5EsX_5JJ6Onvo,36590
11
- mmrelay/setup_utils.py,sha256=UZLX944GyiUQPUGNGNDwLocgShA8SzOJtvfntEu821A,16060
12
- mmrelay/plugins/__init__.py,sha256=KVMQIXRhe0wlGj4O3IZ0vOIQRKFkfPYejHXhJL17qrc,51
13
- mmrelay/plugins/base_plugin.py,sha256=hv21tSEYG-AB36aLAFdW9DDKm0NOTRNPpGIO5F3i1ts,8633
14
- mmrelay/plugins/debug_plugin.py,sha256=Jziht9Nj_bRO6Rmy7TjfBXaYo5eM3XsenbWFxPpyUs4,443
15
- mmrelay/plugins/drop_plugin.py,sha256=ACchX6GfEldqTyvsZVg-5jwbe2-CjENQVVZFnw8SaEM,4618
16
- mmrelay/plugins/health_plugin.py,sha256=svV_GfpAVL0QhiVzi3PVZ1mNpsOL1NHSmkRF-Mn_ExE,2250
17
- mmrelay/plugins/help_plugin.py,sha256=-C4M1PPi1m-fPRvNAlcMyHDNiEEIF6013z2vzrepa1Q,1453
18
- mmrelay/plugins/map_plugin.py,sha256=tWzNvErSoIjqpWtdR-uhbnqdX1SQZl1XQn3Qrp195Kk,10024
19
- mmrelay/plugins/mesh_relay_plugin.py,sha256=E04JU4uKr7KTScahS0VKBJ1xwvq1ULJaeFVYEtceMvA,4273
20
- mmrelay/plugins/nodes_plugin.py,sha256=RDabzyG5hKG5aYWecsRUcLSjMCCv6Pngmq2Qpld1A1U,2903
21
- mmrelay/plugins/ping_plugin.py,sha256=RTRdgDQUSO33lreDTmWsTlI0L1C3FJrXE0KYqfEWYO0,4017
22
- mmrelay/plugins/telemetry_plugin.py,sha256=8SxWv4BLXMUTbiVaD3MjlMMdQyS7S_1OfLlVNAUMSO0,6306
23
- mmrelay/plugins/weather_plugin.py,sha256=yoKA_HdFqFEhgYdXqLhvXatLphCyLJFuGUKCR7fILv0,8546
24
- mmrelay-1.0.6.dist-info/licenses/LICENSE,sha256=yceWauM1c0-FHxVplsD7W1-AbSeRaUNlmqT4UO1msBU,1073
25
- mmrelay-1.0.6.dist-info/METADATA,sha256=V_JcF794O_pNB39Bm3K1TxVpB3C5f1lWYGQu4s3j3Dc,6987
26
- mmrelay-1.0.6.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
27
- mmrelay-1.0.6.dist-info/entry_points.txt,sha256=SJZwGUOEpQ-qx4H8UL4xKFnKeInGUaZNW1I0ddjK7Ws,45
28
- mmrelay-1.0.6.dist-info/top_level.txt,sha256=B_ZLCRm7NYAmI3PipRUyHGymP-C-q16LSeMGzmqJfo4,8
29
- mmrelay-1.0.6.dist-info/RECORD,,