mmrelay 1.1.3__py3-none-any.whl → 1.1.4__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 +1 -1
- mmrelay/cli.py +124 -64
- mmrelay/config.py +63 -36
- mmrelay/config_checker.py +41 -12
- mmrelay/constants/__init__.py +54 -0
- mmrelay/constants/app.py +17 -0
- mmrelay/constants/config.py +73 -0
- mmrelay/constants/database.py +22 -0
- mmrelay/constants/formats.py +20 -0
- mmrelay/constants/messages.py +36 -0
- mmrelay/constants/network.py +35 -0
- mmrelay/constants/queue.py +17 -0
- mmrelay/db_utils.py +281 -132
- mmrelay/log_utils.py +38 -14
- mmrelay/main.py +5 -4
- mmrelay/matrix_utils.py +43 -53
- mmrelay/meshtastic_utils.py +203 -99
- mmrelay/message_queue.py +17 -17
- mmrelay/plugin_loader.py +54 -51
- mmrelay/plugins/base_plugin.py +58 -11
- mmrelay/plugins/drop_plugin.py +13 -5
- mmrelay/plugins/mesh_relay_plugin.py +7 -10
- mmrelay/plugins/weather_plugin.py +10 -1
- mmrelay/setup_utils.py +67 -30
- {mmrelay-1.1.3.dist-info → mmrelay-1.1.4.dist-info}/METADATA +3 -3
- mmrelay-1.1.4.dist-info/RECORD +43 -0
- mmrelay-1.1.3.dist-info/RECORD +0 -35
- {mmrelay-1.1.3.dist-info → mmrelay-1.1.4.dist-info}/WHEEL +0 -0
- {mmrelay-1.1.3.dist-info → mmrelay-1.1.4.dist-info}/entry_points.txt +0 -0
- {mmrelay-1.1.3.dist-info → mmrelay-1.1.4.dist-info}/licenses/LICENSE +0 -0
- {mmrelay-1.1.3.dist-info → mmrelay-1.1.4.dist-info}/top_level.txt +0 -0
mmrelay/plugin_loader.py
CHANGED
|
@@ -519,26 +519,17 @@ def clone_or_update_repo(repo_url, ref, plugins_dir):
|
|
|
519
519
|
|
|
520
520
|
|
|
521
521
|
def load_plugins_from_directory(directory, recursive=False):
|
|
522
|
-
"""
|
|
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
|
|
522
|
+
"""
|
|
523
|
+
Dynamically loads and instantiates plugin classes from Python files in a specified directory.
|
|
530
524
|
|
|
531
|
-
Scans for
|
|
532
|
-
a 'Plugin' class in each module and instantiates it if found.
|
|
525
|
+
Scans the given directory (and subdirectories if `recursive` is True) for `.py` files, importing each as a module and instantiating its `Plugin` class if present. Automatically attempts to install missing dependencies when a `ModuleNotFoundError` occurs, supporting both pip and pipx environments. Provides compatibility for plugins importing from either `plugins` or `mmrelay.plugins`. Skips files without a `Plugin` class or with unresolved import errors.
|
|
533
526
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
- Proper sys.path management for plugin directory imports
|
|
538
|
-
- Comprehensive error handling and logging
|
|
527
|
+
Parameters:
|
|
528
|
+
directory (str): Path to the directory containing plugin files.
|
|
529
|
+
recursive (bool): If True, searches subdirectories recursively.
|
|
539
530
|
|
|
540
|
-
|
|
541
|
-
|
|
531
|
+
Returns:
|
|
532
|
+
list: Instantiated plugin objects found in the directory.
|
|
542
533
|
"""
|
|
543
534
|
plugins = []
|
|
544
535
|
if os.path.isdir(directory):
|
|
@@ -626,13 +617,23 @@ def load_plugins_from_directory(directory, recursive=False):
|
|
|
626
617
|
)
|
|
627
618
|
|
|
628
619
|
# Try to load the module again
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
620
|
+
try:
|
|
621
|
+
spec.loader.exec_module(plugin_module)
|
|
622
|
+
|
|
623
|
+
if hasattr(plugin_module, "Plugin"):
|
|
624
|
+
plugins.append(plugin_module.Plugin())
|
|
625
|
+
else:
|
|
626
|
+
logger.warning(
|
|
627
|
+
f"{plugin_path} does not define a Plugin class."
|
|
628
|
+
)
|
|
629
|
+
except ModuleNotFoundError:
|
|
630
|
+
logger.error(
|
|
631
|
+
f"Module {missing_module} still not available after installation. "
|
|
632
|
+
f"The package name might be different from the import name."
|
|
633
|
+
)
|
|
634
|
+
except Exception as retry_error:
|
|
635
|
+
logger.error(
|
|
636
|
+
f"Error loading plugin {plugin_path} after dependency installation: {retry_error}"
|
|
636
637
|
)
|
|
637
638
|
|
|
638
639
|
except subprocess.CalledProcessError:
|
|
@@ -660,25 +661,16 @@ def load_plugins_from_directory(directory, recursive=False):
|
|
|
660
661
|
|
|
661
662
|
|
|
662
663
|
def load_plugins(passed_config=None):
|
|
663
|
-
"""
|
|
664
|
+
"""
|
|
665
|
+
Discovers, loads, and initializes all active plugins according to the configuration.
|
|
664
666
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
667
|
+
This function manages the full plugin lifecycle: it loads core, custom, and community plugins as specified in the configuration, handles cloning and updating of community plugin repositories, installs dependencies as needed, and starts each active plugin. Plugins are filtered and sorted by priority before being returned. If plugins have already been loaded, returns the cached list.
|
|
668
|
+
|
|
669
|
+
Parameters:
|
|
670
|
+
passed_config (dict, optional): Configuration dictionary to use instead of the global config.
|
|
668
671
|
|
|
669
672
|
Returns:
|
|
670
|
-
list:
|
|
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.
|
|
673
|
+
list: Active plugin instances sorted by priority.
|
|
682
674
|
"""
|
|
683
675
|
global sorted_active_plugins
|
|
684
676
|
global plugins_loaded
|
|
@@ -750,11 +742,15 @@ def load_plugins(passed_config=None):
|
|
|
750
742
|
plugin_path = os.path.join(custom_dir, plugin_name)
|
|
751
743
|
if os.path.exists(plugin_path):
|
|
752
744
|
logger.debug(f"Loading custom plugin from: {plugin_path}")
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
745
|
+
try:
|
|
746
|
+
plugins.extend(
|
|
747
|
+
load_plugins_from_directory(plugin_path, recursive=False)
|
|
748
|
+
)
|
|
749
|
+
plugin_found = True
|
|
750
|
+
break
|
|
751
|
+
except Exception as e:
|
|
752
|
+
logger.error(f"Failed to load custom plugin {plugin_name}: {e}")
|
|
753
|
+
continue
|
|
758
754
|
|
|
759
755
|
if not plugin_found:
|
|
760
756
|
logger.warning(
|
|
@@ -842,11 +838,17 @@ def load_plugins(passed_config=None):
|
|
|
842
838
|
plugin_path = os.path.join(dir_path, repo_name)
|
|
843
839
|
if os.path.exists(plugin_path):
|
|
844
840
|
logger.info(f"Loading community plugin from: {plugin_path}")
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
841
|
+
try:
|
|
842
|
+
plugins.extend(
|
|
843
|
+
load_plugins_from_directory(plugin_path, recursive=True)
|
|
844
|
+
)
|
|
845
|
+
plugin_found = True
|
|
846
|
+
break
|
|
847
|
+
except Exception as e:
|
|
848
|
+
logger.error(
|
|
849
|
+
f"Failed to load community plugin {repo_name}: {e}"
|
|
850
|
+
)
|
|
851
|
+
continue
|
|
850
852
|
|
|
851
853
|
if not plugin_found:
|
|
852
854
|
logger.warning(
|
|
@@ -900,4 +902,5 @@ def load_plugins(passed_config=None):
|
|
|
900
902
|
else:
|
|
901
903
|
logger.info("Loaded: none")
|
|
902
904
|
|
|
903
|
-
plugins_loaded = True # Set the flag to indicate that plugins have been
|
|
905
|
+
plugins_loaded = True # Set the flag to indicate that plugins have been loaded
|
|
906
|
+
return sorted_active_plugins
|
mmrelay/plugins/base_plugin.py
CHANGED
|
@@ -7,6 +7,11 @@ import markdown
|
|
|
7
7
|
import schedule
|
|
8
8
|
|
|
9
9
|
from mmrelay.config import get_plugin_data_dir
|
|
10
|
+
from mmrelay.constants.database import (
|
|
11
|
+
DEFAULT_MAX_DATA_ROWS_PER_NODE_BASE,
|
|
12
|
+
DEFAULT_TEXT_TRUNCATION_LENGTH,
|
|
13
|
+
)
|
|
14
|
+
from mmrelay.constants.queue import DEFAULT_MESSAGE_DELAY
|
|
10
15
|
from mmrelay.db_utils import (
|
|
11
16
|
delete_plugin_data,
|
|
12
17
|
get_plugin_data,
|
|
@@ -14,7 +19,7 @@ from mmrelay.db_utils import (
|
|
|
14
19
|
store_plugin_data,
|
|
15
20
|
)
|
|
16
21
|
from mmrelay.log_utils import get_logger
|
|
17
|
-
from mmrelay.message_queue import
|
|
22
|
+
from mmrelay.message_queue import queue_message
|
|
18
23
|
|
|
19
24
|
# Global config variable that will be set from main.py
|
|
20
25
|
config = None
|
|
@@ -47,7 +52,7 @@ class BasePlugin(ABC):
|
|
|
47
52
|
|
|
48
53
|
# Class-level default attributes
|
|
49
54
|
plugin_name = None # Must be overridden in subclasses
|
|
50
|
-
max_data_rows_per_node =
|
|
55
|
+
max_data_rows_per_node = DEFAULT_MAX_DATA_ROWS_PER_NODE_BASE
|
|
51
56
|
priority = 10
|
|
52
57
|
|
|
53
58
|
@property
|
|
@@ -251,19 +256,63 @@ class BasePlugin(ABC):
|
|
|
251
256
|
"""
|
|
252
257
|
return self.response_delay
|
|
253
258
|
|
|
259
|
+
def get_my_node_id(self):
|
|
260
|
+
"""Get the relay's Meshtastic node ID.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
int: The relay's node ID, or None if unavailable
|
|
264
|
+
|
|
265
|
+
This method provides access to the relay's own node ID without requiring
|
|
266
|
+
plugins to call connect_meshtastic() directly. Useful for determining
|
|
267
|
+
if messages are direct messages or for other node identification needs.
|
|
268
|
+
|
|
269
|
+
The node ID is cached after first successful retrieval to avoid repeated
|
|
270
|
+
connection calls, as the relay's node ID is static during runtime.
|
|
271
|
+
"""
|
|
272
|
+
if hasattr(self, "_my_node_id"):
|
|
273
|
+
return self._my_node_id
|
|
274
|
+
|
|
275
|
+
from mmrelay.meshtastic_utils import connect_meshtastic
|
|
276
|
+
|
|
277
|
+
meshtastic_client = connect_meshtastic()
|
|
278
|
+
if meshtastic_client and meshtastic_client.myInfo:
|
|
279
|
+
self._my_node_id = meshtastic_client.myInfo.my_node_num
|
|
280
|
+
return self._my_node_id
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
def is_direct_message(self, packet):
|
|
284
|
+
"""Check if a Meshtastic packet is a direct message to this relay.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
packet (dict): Meshtastic packet data
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
bool: True if the packet is a direct message to this relay, False otherwise
|
|
291
|
+
|
|
292
|
+
This method encapsulates the common pattern of checking if a message
|
|
293
|
+
is addressed directly to the relay node, eliminating the need for plugins
|
|
294
|
+
to call connect_meshtastic() directly for DM detection.
|
|
295
|
+
"""
|
|
296
|
+
toId = packet.get("to")
|
|
297
|
+
if toId is None:
|
|
298
|
+
return False
|
|
299
|
+
|
|
300
|
+
myId = self.get_my_node_id()
|
|
301
|
+
return toId == myId
|
|
302
|
+
|
|
254
303
|
def send_message(self, text: str, channel: int = 0, destination_id=None) -> bool:
|
|
255
304
|
"""
|
|
256
|
-
Send a message to the Meshtastic network
|
|
305
|
+
Send a message to the Meshtastic network using the message queue.
|
|
257
306
|
|
|
258
|
-
|
|
307
|
+
Queues the specified text for broadcast or direct delivery on the given channel. Returns True if the message was successfully queued, or False if the Meshtastic client is unavailable.
|
|
259
308
|
|
|
260
309
|
Parameters:
|
|
261
|
-
text (str): The message
|
|
262
|
-
channel (int, optional):
|
|
263
|
-
destination_id (optional):
|
|
310
|
+
text (str): The message content to send.
|
|
311
|
+
channel (int, optional): The channel index for sending the message. Defaults to 0.
|
|
312
|
+
destination_id (optional): The destination node ID for direct messages. If None, the message is broadcast.
|
|
264
313
|
|
|
265
314
|
Returns:
|
|
266
|
-
bool: True if the message was queued successfully
|
|
315
|
+
bool: True if the message was queued successfully; False otherwise.
|
|
267
316
|
"""
|
|
268
317
|
from mmrelay.meshtastic_utils import connect_meshtastic
|
|
269
318
|
|
|
@@ -272,9 +321,7 @@ class BasePlugin(ABC):
|
|
|
272
321
|
self.logger.error("No Meshtastic client available")
|
|
273
322
|
return False
|
|
274
323
|
|
|
275
|
-
description = (
|
|
276
|
-
f"Plugin {self.plugin_name}: {text[:50]}{'...' if len(text) > 50 else ''}"
|
|
277
|
-
)
|
|
324
|
+
description = f"Plugin {self.plugin_name}: {text[:DEFAULT_TEXT_TRUNCATION_LENGTH]}{'...' if len(text) > DEFAULT_TEXT_TRUNCATION_LENGTH else ''}"
|
|
278
325
|
|
|
279
326
|
send_kwargs = {
|
|
280
327
|
"text": text,
|
mmrelay/plugins/drop_plugin.py
CHANGED
|
@@ -2,6 +2,8 @@ import re
|
|
|
2
2
|
|
|
3
3
|
from haversine import haversine
|
|
4
4
|
|
|
5
|
+
from mmrelay.constants.database import DEFAULT_DISTANCE_KM_FALLBACK, DEFAULT_RADIUS_KM
|
|
6
|
+
from mmrelay.constants.formats import TEXT_MESSAGE_APP
|
|
5
7
|
from mmrelay.meshtastic_utils import connect_meshtastic
|
|
6
8
|
from mmrelay.plugins.base_plugin import BasePlugin
|
|
7
9
|
|
|
@@ -25,6 +27,14 @@ class Plugin(BasePlugin):
|
|
|
25
27
|
async def handle_meshtastic_message(
|
|
26
28
|
self, packet, formatted_message, longname, meshnet_name
|
|
27
29
|
):
|
|
30
|
+
"""
|
|
31
|
+
Handles incoming Meshtastic packets for the drop message plugin, delivering or storing dropped messages based on packet content and node location.
|
|
32
|
+
|
|
33
|
+
When a packet is received, attempts to deliver any stored dropped messages to the sender if they are within a configured radius of the message's location and are not the original dropper. If the packet contains a properly formatted drop command, extracts the message and stores it with the sender's current location for future delivery.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
True if a drop command was processed and stored, False otherwise.
|
|
37
|
+
"""
|
|
28
38
|
meshtastic_client = connect_meshtastic()
|
|
29
39
|
nodeInfo = meshtastic_client.getMyNodeInfo()
|
|
30
40
|
|
|
@@ -55,10 +65,8 @@ class Plugin(BasePlugin):
|
|
|
55
65
|
message["location"],
|
|
56
66
|
)
|
|
57
67
|
except (ValueError, TypeError):
|
|
58
|
-
distance_km =
|
|
59
|
-
radius_km = (
|
|
60
|
-
self.config["radius_km"] if "radius_km" in self.config else 5
|
|
61
|
-
)
|
|
68
|
+
distance_km = DEFAULT_DISTANCE_KM_FALLBACK
|
|
69
|
+
radius_km = self.config.get("radius_km", DEFAULT_RADIUS_KM)
|
|
62
70
|
if distance_km <= radius_km:
|
|
63
71
|
target_node = packet["fromId"]
|
|
64
72
|
self.logger.debug(f"Sending dropped message to {target_node}")
|
|
@@ -76,7 +84,7 @@ class Plugin(BasePlugin):
|
|
|
76
84
|
if (
|
|
77
85
|
"decoded" in packet
|
|
78
86
|
and "portnum" in packet["decoded"]
|
|
79
|
-
and packet["decoded"]["portnum"] ==
|
|
87
|
+
and packet["decoded"]["portnum"] == TEXT_MESSAGE_APP
|
|
80
88
|
):
|
|
81
89
|
text = packet["decoded"]["text"] if "text" in packet["decoded"] else None
|
|
82
90
|
if f"!{self.plugin_name}" not in text:
|
|
@@ -6,6 +6,7 @@ import re
|
|
|
6
6
|
|
|
7
7
|
from meshtastic import mesh_pb2
|
|
8
8
|
|
|
9
|
+
from mmrelay.constants.database import DEFAULT_MAX_DATA_ROWS_PER_NODE_MESH_RELAY
|
|
9
10
|
from mmrelay.plugins.base_plugin import BasePlugin, config
|
|
10
11
|
|
|
11
12
|
|
|
@@ -25,21 +26,17 @@ class Plugin(BasePlugin):
|
|
|
25
26
|
"""
|
|
26
27
|
|
|
27
28
|
plugin_name = "mesh_relay"
|
|
28
|
-
max_data_rows_per_node =
|
|
29
|
+
max_data_rows_per_node = DEFAULT_MAX_DATA_ROWS_PER_NODE_MESH_RELAY
|
|
29
30
|
|
|
30
31
|
def normalize(self, dict_obj):
|
|
31
|
-
"""
|
|
32
|
+
"""
|
|
33
|
+
Converts packet data in various formats (dict, JSON string, or plain string) into a normalized dictionary with raw data fields removed.
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
dict_obj: Packet data
|
|
35
|
+
Parameters:
|
|
36
|
+
dict_obj: Packet data as a dictionary, JSON string, or plain string.
|
|
35
37
|
|
|
36
38
|
Returns:
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
Handles various packet formats:
|
|
40
|
-
- Dict objects (passed through)
|
|
41
|
-
- JSON strings (parsed)
|
|
42
|
-
- Plain strings (wrapped in TEXT_MESSAGE_APP format)
|
|
39
|
+
A dictionary representing the normalized packet with raw fields stripped.
|
|
43
40
|
"""
|
|
44
41
|
if not isinstance(dict_obj, dict):
|
|
45
42
|
try:
|
|
@@ -3,6 +3,7 @@ import asyncio
|
|
|
3
3
|
import requests
|
|
4
4
|
from meshtastic.mesh_interface import BROADCAST_NUM
|
|
5
5
|
|
|
6
|
+
from mmrelay.constants.formats import TEXT_MESSAGE_APP
|
|
6
7
|
from mmrelay.plugins.base_plugin import BasePlugin
|
|
7
8
|
|
|
8
9
|
|
|
@@ -122,10 +123,18 @@ class Plugin(BasePlugin):
|
|
|
122
123
|
async def handle_meshtastic_message(
|
|
123
124
|
self, packet, formatted_message, longname, meshnet_name
|
|
124
125
|
):
|
|
126
|
+
"""
|
|
127
|
+
Processes incoming Meshtastic text messages and responds with a weather forecast if the plugin command is detected.
|
|
128
|
+
|
|
129
|
+
Checks if the message is a valid text message on the expected port, verifies channel and command enablement, retrieves the sender's GPS location, generates a weather forecast, and sends the response either as a direct message or broadcast depending on the message type.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
bool: True if the message was handled and a response was sent; False otherwise.
|
|
133
|
+
"""
|
|
125
134
|
if (
|
|
126
135
|
"decoded" in packet
|
|
127
136
|
and "portnum" in packet["decoded"]
|
|
128
|
-
and packet["decoded"]["portnum"] ==
|
|
137
|
+
and packet["decoded"]["portnum"] == TEXT_MESSAGE_APP
|
|
129
138
|
and "text" in packet["decoded"]
|
|
130
139
|
):
|
|
131
140
|
message = packet["decoded"]["text"].strip()
|
mmrelay/setup_utils.py
CHANGED
|
@@ -14,6 +14,7 @@ import subprocess
|
|
|
14
14
|
import sys
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
|
|
17
|
+
from mmrelay.constants.database import PROGRESS_COMPLETE, PROGRESS_TOTAL_STEPS
|
|
17
18
|
from mmrelay.tools import get_service_template_path
|
|
18
19
|
|
|
19
20
|
|
|
@@ -54,7 +55,11 @@ def print_service_commands():
|
|
|
54
55
|
|
|
55
56
|
|
|
56
57
|
def wait_for_service_start():
|
|
57
|
-
"""
|
|
58
|
+
"""
|
|
59
|
+
Displays a progress spinner while waiting up to 10 seconds for the mmrelay service to become active.
|
|
60
|
+
|
|
61
|
+
The function checks the service status after 5 seconds and completes early if the service is detected as active.
|
|
62
|
+
"""
|
|
58
63
|
import time
|
|
59
64
|
|
|
60
65
|
from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
|
|
@@ -67,7 +72,7 @@ def wait_for_service_start():
|
|
|
67
72
|
transient=True,
|
|
68
73
|
) as progress:
|
|
69
74
|
# Add a task that will run for approximately 10 seconds
|
|
70
|
-
task = progress.add_task("Starting", total=
|
|
75
|
+
task = progress.add_task("Starting", total=PROGRESS_TOTAL_STEPS)
|
|
71
76
|
|
|
72
77
|
# Update progress over 10 seconds
|
|
73
78
|
for i in range(10):
|
|
@@ -76,7 +81,7 @@ def wait_for_service_start():
|
|
|
76
81
|
|
|
77
82
|
# Check if service is active after 5 seconds to potentially finish early
|
|
78
83
|
if i >= 5 and is_service_active():
|
|
79
|
-
progress.update(task, completed=
|
|
84
|
+
progress.update(task, completed=PROGRESS_COMPLETE)
|
|
80
85
|
break
|
|
81
86
|
|
|
82
87
|
|
|
@@ -372,10 +377,11 @@ def check_loginctl_available():
|
|
|
372
377
|
|
|
373
378
|
|
|
374
379
|
def check_lingering_enabled():
|
|
375
|
-
"""
|
|
380
|
+
"""
|
|
381
|
+
Determine whether user lingering is enabled for the current user.
|
|
376
382
|
|
|
377
383
|
Returns:
|
|
378
|
-
bool: True if lingering is enabled, False otherwise.
|
|
384
|
+
bool: True if user lingering is enabled, False otherwise.
|
|
379
385
|
"""
|
|
380
386
|
try:
|
|
381
387
|
username = os.environ.get("USER", os.environ.get("USERNAME"))
|
|
@@ -386,7 +392,8 @@ def check_lingering_enabled():
|
|
|
386
392
|
text=True,
|
|
387
393
|
)
|
|
388
394
|
return result.returncode == 0 and "Linger=yes" in result.stdout
|
|
389
|
-
except Exception:
|
|
395
|
+
except Exception as e:
|
|
396
|
+
print(f"Error checking lingering status: {e}")
|
|
390
397
|
return False
|
|
391
398
|
|
|
392
399
|
|
|
@@ -417,7 +424,14 @@ def enable_lingering():
|
|
|
417
424
|
|
|
418
425
|
|
|
419
426
|
def install_service():
|
|
420
|
-
"""
|
|
427
|
+
"""
|
|
428
|
+
Install or update the MMRelay systemd user service, guiding the user through creation, updating, enabling, and starting the service as needed.
|
|
429
|
+
|
|
430
|
+
Prompts the user for confirmation before updating an existing service file, enabling user lingering, enabling the service to start at boot, and starting or restarting the service. Handles user interruptions gracefully and prints a summary of the service status and management commands upon completion.
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
bool: True if the installation or update process completes successfully, False otherwise.
|
|
434
|
+
"""
|
|
421
435
|
# Check if service already exists
|
|
422
436
|
existing_service = read_service_file()
|
|
423
437
|
service_path = get_user_service_path()
|
|
@@ -431,11 +445,14 @@ def install_service():
|
|
|
431
445
|
|
|
432
446
|
if update_needed:
|
|
433
447
|
print(f"The service file needs to be updated: {reason}")
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
.lower()
|
|
437
|
-
|
|
438
|
-
|
|
448
|
+
try:
|
|
449
|
+
user_input = input("Do you want to update the service file? (y/n): ")
|
|
450
|
+
if not user_input.lower().startswith("y"):
|
|
451
|
+
print("Service update cancelled.")
|
|
452
|
+
print_service_commands()
|
|
453
|
+
return True
|
|
454
|
+
except (EOFError, KeyboardInterrupt):
|
|
455
|
+
print("\nInput cancelled. Proceeding with default behavior.")
|
|
439
456
|
print("Service update cancelled.")
|
|
440
457
|
print_service_commands()
|
|
441
458
|
return True
|
|
@@ -450,9 +467,11 @@ def install_service():
|
|
|
450
467
|
if not create_service_file():
|
|
451
468
|
return False
|
|
452
469
|
|
|
453
|
-
# Reload daemon
|
|
470
|
+
# Reload daemon (continue even if this fails)
|
|
454
471
|
if not reload_daemon():
|
|
455
|
-
|
|
472
|
+
print(
|
|
473
|
+
"Warning: Failed to reload systemd daemon. You may need to run 'systemctl --user daemon-reload' manually."
|
|
474
|
+
)
|
|
456
475
|
|
|
457
476
|
if existing_service:
|
|
458
477
|
print("Service file updated successfully")
|
|
@@ -473,13 +492,16 @@ def install_service():
|
|
|
473
492
|
print(
|
|
474
493
|
"Lingering allows user services to run even when you're not logged in."
|
|
475
494
|
)
|
|
476
|
-
|
|
477
|
-
input(
|
|
495
|
+
try:
|
|
496
|
+
user_input = input(
|
|
478
497
|
"Do you want to enable lingering for your user? (requires sudo) (y/n): "
|
|
479
498
|
)
|
|
480
|
-
.lower()
|
|
481
|
-
|
|
482
|
-
|
|
499
|
+
should_enable_lingering = user_input.lower().startswith("y")
|
|
500
|
+
except (EOFError, KeyboardInterrupt):
|
|
501
|
+
print("\nInput cancelled. Skipping lingering setup.")
|
|
502
|
+
should_enable_lingering = False
|
|
503
|
+
|
|
504
|
+
if should_enable_lingering:
|
|
483
505
|
enable_lingering()
|
|
484
506
|
|
|
485
507
|
# Check if the service is already enabled
|
|
@@ -488,11 +510,16 @@ def install_service():
|
|
|
488
510
|
print("The service is already enabled to start at boot.")
|
|
489
511
|
else:
|
|
490
512
|
print("The service is not currently enabled to start at boot.")
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
513
|
+
try:
|
|
514
|
+
user_input = input(
|
|
515
|
+
"Do you want to enable the service to start at boot? (y/n): "
|
|
516
|
+
)
|
|
517
|
+
enable_service = user_input.lower().startswith("y")
|
|
518
|
+
except (EOFError, KeyboardInterrupt):
|
|
519
|
+
print("\nInput cancelled. Skipping service enable.")
|
|
520
|
+
enable_service = False
|
|
521
|
+
|
|
522
|
+
if enable_service:
|
|
496
523
|
try:
|
|
497
524
|
subprocess.run(
|
|
498
525
|
["/usr/bin/systemctl", "--user", "enable", "mmrelay.service"],
|
|
@@ -509,7 +536,14 @@ def install_service():
|
|
|
509
536
|
service_active = is_service_active()
|
|
510
537
|
if service_active:
|
|
511
538
|
print("The service is already running.")
|
|
512
|
-
|
|
539
|
+
try:
|
|
540
|
+
user_input = input("Do you want to restart the service? (y/n): ")
|
|
541
|
+
restart_service = user_input.lower().startswith("y")
|
|
542
|
+
except (EOFError, KeyboardInterrupt):
|
|
543
|
+
print("\nInput cancelled. Skipping service restart.")
|
|
544
|
+
restart_service = False
|
|
545
|
+
|
|
546
|
+
if restart_service:
|
|
513
547
|
try:
|
|
514
548
|
subprocess.run(
|
|
515
549
|
["/usr/bin/systemctl", "--user", "restart", "mmrelay.service"],
|
|
@@ -526,11 +560,14 @@ def install_service():
|
|
|
526
560
|
print(f"Error: {e}")
|
|
527
561
|
else:
|
|
528
562
|
print("The service is not currently running.")
|
|
529
|
-
|
|
530
|
-
input("Do you want to start the service now? (y/n): ")
|
|
531
|
-
.lower()
|
|
532
|
-
|
|
533
|
-
|
|
563
|
+
try:
|
|
564
|
+
user_input = input("Do you want to start the service now? (y/n): ")
|
|
565
|
+
start_now = user_input.lower().startswith("y")
|
|
566
|
+
except (EOFError, KeyboardInterrupt):
|
|
567
|
+
print("\nInput cancelled. Skipping service start.")
|
|
568
|
+
start_now = False
|
|
569
|
+
|
|
570
|
+
if start_now:
|
|
534
571
|
if start_service():
|
|
535
572
|
# Wait for the service to start
|
|
536
573
|
wait_for_service_start()
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mmrelay
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.4
|
|
4
4
|
Summary: Bridge between Meshtastic mesh networks and Matrix chat rooms
|
|
5
|
-
Home-page: https://github.com/
|
|
5
|
+
Home-page: https://github.com/jeremiah-k/meshtastic-matrix-relay
|
|
6
6
|
Author: Geoff Whittington, Jeremiah K., and contributors
|
|
7
7
|
Author-email: jeremiahk@gmx.com
|
|
8
|
-
Project-URL: Bug Tracker, https://github.com/
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/jeremiah-k/meshtastic-matrix-relay/issues
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
10
10
|
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
11
11
|
Classifier: Operating System :: OS Independent
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
mmrelay/__init__.py,sha256=HEhksEbBrRIg-V44f45Ik9ZoN8cgAZPX5hVtq4c5a5o,120
|
|
2
|
+
mmrelay/cli.py,sha256=IduTq-Uyf99LEJD8jTSJBgLdrCOlYA_b69ClhsvqXMA,17116
|
|
3
|
+
mmrelay/config.py,sha256=cOLpVx51Rd2RhUFOc-rxPVsVVPZhWJ2A07y3Mf9e_rg,9672
|
|
4
|
+
mmrelay/config_checker.py,sha256=CW89TwNmGB03LYXUmKbygjAthJw_8KSq7csBbTTCRV0,6003
|
|
5
|
+
mmrelay/db_utils.py,sha256=b_cuw4zOwGlvk4CljFh3SguqOi-TRCHW_s9RCt9gUsU,19920
|
|
6
|
+
mmrelay/log_utils.py,sha256=DKkFKkpOLbSTvvbzYzvQXzGGxSQP5vVRtmbC6l-d2WY,7380
|
|
7
|
+
mmrelay/main.py,sha256=n4Qn4KdlXxWv6QcDrkX5rnJFEbNoC2s7c2kOf3dUOx4,13225
|
|
8
|
+
mmrelay/matrix_utils.py,sha256=iyQw3mU49fHP7n9yieQLxX8AEwIzhcktnrMAzlX7Wm0,55776
|
|
9
|
+
mmrelay/meshtastic_utils.py,sha256=I9jZOXWsKbrIVEKTD_ie9Qm_gY4twzg6iMOXlMVoX2w,35939
|
|
10
|
+
mmrelay/message_queue.py,sha256=tHKziIPwQ9Xm7qpITyR1r2e-ftdn4ZivdZhek9SmMcQ,18703
|
|
11
|
+
mmrelay/plugin_loader.py,sha256=Yyu8M399BSF8OVsCkETdhdn7xo673leiardl-lO9JXQ,40139
|
|
12
|
+
mmrelay/setup_utils.py,sha256=EAVPmKHQmlXnxMcrkhlU27fBPjJCiwTVSLZdbEFsWLQ,22095
|
|
13
|
+
mmrelay/constants/__init__.py,sha256=M8AXeIcS1JuS8OwmfTmhcCOkAz5XmWlNQ53GBxYOx94,1494
|
|
14
|
+
mmrelay/constants/app.py,sha256=T7qud8aJAMIV55KuT0LakIoS1y9AdteHMU-SUaF4Eqw,419
|
|
15
|
+
mmrelay/constants/config.py,sha256=ZhEQBP6TZS89XkHzZdmPCIZNGSjI2-tTzK5v0l7ilIQ,2350
|
|
16
|
+
mmrelay/constants/database.py,sha256=4cHfYfBePDUUtVSflrWyStcxKSQv7VE-jSrb1IzAjls,646
|
|
17
|
+
mmrelay/constants/formats.py,sha256=cjbrfNNFCKoGSFsFHR1QQDEQudiGquA9MUapfm0_ZNI,494
|
|
18
|
+
mmrelay/constants/messages.py,sha256=xr77IfurBJCrM4Qp53nbDr-Bo82Di7xZcICBOinjGq4,987
|
|
19
|
+
mmrelay/constants/network.py,sha256=8zpQemeVAflSjtZJgg7cQ4IMzeQzCah38PJbLNHmjvY,933
|
|
20
|
+
mmrelay/constants/queue.py,sha256=yyWSrtq06b5GWzZwdl6IFtrMvxEuF9PdKSNPh8DdL2M,565
|
|
21
|
+
mmrelay/plugins/__init__.py,sha256=KVMQIXRhe0wlGj4O3IZ0vOIQRKFkfPYejHXhJL17qrc,51
|
|
22
|
+
mmrelay/plugins/base_plugin.py,sha256=p0ojzdiXTm9VpZYbZ-GPysCEhlsNQ04oUWefIfvfsjU,20181
|
|
23
|
+
mmrelay/plugins/debug_plugin.py,sha256=adX0cRJHUEDLldajybPfiRDDlvytkZe5aN_dSgNKP2Y,870
|
|
24
|
+
mmrelay/plugins/drop_plugin.py,sha256=x4S-e0Muun2Dy1H2qwRMTBB1ptLmy7ZZJhgPu-KefGs,5394
|
|
25
|
+
mmrelay/plugins/health_plugin.py,sha256=svV_GfpAVL0QhiVzi3PVZ1mNpsOL1NHSmkRF-Mn_ExE,2250
|
|
26
|
+
mmrelay/plugins/help_plugin.py,sha256=S7nBhsANK46Zv9wPHOVegPGcuYGMErBsxAnrRlSSCwg,2149
|
|
27
|
+
mmrelay/plugins/map_plugin.py,sha256=eHV_t3TFcypBD4xT_OQx0hD6_iGkLJOADjwYVny0PvE,11292
|
|
28
|
+
mmrelay/plugins/mesh_relay_plugin.py,sha256=gpQkO8S-LqDNwqJpqq5ewGXVEST-JZgOsJmrE-qRzuw,7631
|
|
29
|
+
mmrelay/plugins/nodes_plugin.py,sha256=RDabzyG5hKG5aYWecsRUcLSjMCCv6Pngmq2Qpld1A1U,2903
|
|
30
|
+
mmrelay/plugins/ping_plugin.py,sha256=8uFnT3qfO3RBaTUOx348voIfKpzXB3zTfcT6Gtfc8kM,4070
|
|
31
|
+
mmrelay/plugins/telemetry_plugin.py,sha256=8SxWv4BLXMUTbiVaD3MjlMMdQyS7S_1OfLlVNAUMSO0,6306
|
|
32
|
+
mmrelay/plugins/weather_plugin.py,sha256=RkiDeZpTWAb0VjsDC3qkGUrYY4NmzJgNa-iwBCwg8OM,9186
|
|
33
|
+
mmrelay/tools/__init__.py,sha256=WFjDQjdevgg19_zT6iEoL29rvb1JPqYSd8708Jn5D7A,838
|
|
34
|
+
mmrelay/tools/mmrelay.service,sha256=3vqK1VbfXvVftkTrTEOan77aTHeOT36hIAL7HqJsmTg,567
|
|
35
|
+
mmrelay/tools/sample-docker-compose.yaml,sha256=vVgJrh-6l48hkj5F-52JA5tpDWPBjiPQ36CE9Pkqn44,1251
|
|
36
|
+
mmrelay/tools/sample.env,sha256=RP-o3rX3jnEIrVG2rqCZq31O1yRXou4HcGrXWLVbKKw,311
|
|
37
|
+
mmrelay/tools/sample_config.yaml,sha256=grS70MKHFd9e_lZ3GkmzPi4RRW-PdahMOMPCAg07MWs,4718
|
|
38
|
+
mmrelay-1.1.4.dist-info/licenses/LICENSE,sha256=aB_07MhnK-bL5WLI1ucXLUSdW_yBVoepPRYB0kaAOl8,35204
|
|
39
|
+
mmrelay-1.1.4.dist-info/METADATA,sha256=icpHKviPELzrsfvnNuXx0KIIEbXyAzAFLcte4pfQ6U8,6481
|
|
40
|
+
mmrelay-1.1.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
41
|
+
mmrelay-1.1.4.dist-info/entry_points.txt,sha256=SJZwGUOEpQ-qx4H8UL4xKFnKeInGUaZNW1I0ddjK7Ws,45
|
|
42
|
+
mmrelay-1.1.4.dist-info/top_level.txt,sha256=B_ZLCRm7NYAmI3PipRUyHGymP-C-q16LSeMGzmqJfo4,8
|
|
43
|
+
mmrelay-1.1.4.dist-info/RECORD,,
|
mmrelay-1.1.3.dist-info/RECORD
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
mmrelay/__init__.py,sha256=4ciaRmcwzGfuaU_HuNDXVr4Qk0WVkPydhrdXIVo9VOs,120
|
|
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=eTMMkYVWsmO_DkrBfnZMw4ohg_xa0S9TXJoBjRFTwzo,13590
|
|
6
|
-
mmrelay/log_utils.py,sha256=1zVHBJ5qSOG1O4cq4Ebngq9dp81vaZOEq5khulAFuuM,6611
|
|
7
|
-
mmrelay/main.py,sha256=46J-B6wDRawB392u71kP6pxNoayBdmwOiKcqsleikhU,13182
|
|
8
|
-
mmrelay/matrix_utils.py,sha256=Ans7Wroh693un0LOp4x2p8NPfGFJ9z6TnG6ApXRXg7Q,55686
|
|
9
|
-
mmrelay/meshtastic_utils.py,sha256=iy7BwMlqOwpkqSB0pTx2n8oqaKs2kWQ4PXFk4y16ys8,31440
|
|
10
|
-
mmrelay/message_queue.py,sha256=spLK7pVugnl9djVLIh5-P7iHZ7kRtZEZIWTcQ4X1TfI,18529
|
|
11
|
-
mmrelay/plugin_loader.py,sha256=UJ-i6cL2q_hwMTqjRkaOAQDTj6uUrmyj56-XbUVGcng,39231
|
|
12
|
-
mmrelay/setup_utils.py,sha256=N6qdScHKHEMFKDmT1l7dcLDPNTusZXPkyxrLXjFLhRI,19910
|
|
13
|
-
mmrelay/plugins/__init__.py,sha256=KVMQIXRhe0wlGj4O3IZ0vOIQRKFkfPYejHXhJL17qrc,51
|
|
14
|
-
mmrelay/plugins/base_plugin.py,sha256=DJdM-l-69sMnx6Fgn0rweWF5CuWilnQQAT3nm6eq8f4,18359
|
|
15
|
-
mmrelay/plugins/debug_plugin.py,sha256=adX0cRJHUEDLldajybPfiRDDlvytkZe5aN_dSgNKP2Y,870
|
|
16
|
-
mmrelay/plugins/drop_plugin.py,sha256=0GOz0dgLnFST1HTgqrMayrjwmYlnu02QxfnTZtdPZ3U,4671
|
|
17
|
-
mmrelay/plugins/health_plugin.py,sha256=svV_GfpAVL0QhiVzi3PVZ1mNpsOL1NHSmkRF-Mn_ExE,2250
|
|
18
|
-
mmrelay/plugins/help_plugin.py,sha256=S7nBhsANK46Zv9wPHOVegPGcuYGMErBsxAnrRlSSCwg,2149
|
|
19
|
-
mmrelay/plugins/map_plugin.py,sha256=eHV_t3TFcypBD4xT_OQx0hD6_iGkLJOADjwYVny0PvE,11292
|
|
20
|
-
mmrelay/plugins/mesh_relay_plugin.py,sha256=PIQBACC7Yjq7hzKJJNycHTjkayEC9tsC8-JNG4q_AY8,7563
|
|
21
|
-
mmrelay/plugins/nodes_plugin.py,sha256=RDabzyG5hKG5aYWecsRUcLSjMCCv6Pngmq2Qpld1A1U,2903
|
|
22
|
-
mmrelay/plugins/ping_plugin.py,sha256=8uFnT3qfO3RBaTUOx348voIfKpzXB3zTfcT6Gtfc8kM,4070
|
|
23
|
-
mmrelay/plugins/telemetry_plugin.py,sha256=8SxWv4BLXMUTbiVaD3MjlMMdQyS7S_1OfLlVNAUMSO0,6306
|
|
24
|
-
mmrelay/plugins/weather_plugin.py,sha256=1bQhmiX-enNphzGoFVprU0LcZQX9BvGxWAJAG8Wekg0,8596
|
|
25
|
-
mmrelay/tools/__init__.py,sha256=WFjDQjdevgg19_zT6iEoL29rvb1JPqYSd8708Jn5D7A,838
|
|
26
|
-
mmrelay/tools/mmrelay.service,sha256=3vqK1VbfXvVftkTrTEOan77aTHeOT36hIAL7HqJsmTg,567
|
|
27
|
-
mmrelay/tools/sample-docker-compose.yaml,sha256=vVgJrh-6l48hkj5F-52JA5tpDWPBjiPQ36CE9Pkqn44,1251
|
|
28
|
-
mmrelay/tools/sample.env,sha256=RP-o3rX3jnEIrVG2rqCZq31O1yRXou4HcGrXWLVbKKw,311
|
|
29
|
-
mmrelay/tools/sample_config.yaml,sha256=grS70MKHFd9e_lZ3GkmzPi4RRW-PdahMOMPCAg07MWs,4718
|
|
30
|
-
mmrelay-1.1.3.dist-info/licenses/LICENSE,sha256=aB_07MhnK-bL5WLI1ucXLUSdW_yBVoepPRYB0kaAOl8,35204
|
|
31
|
-
mmrelay-1.1.3.dist-info/METADATA,sha256=gNG_TVwJfpvt0s7sd3WDEceXl8Li2WCLGHTkYkXOWVQ,6493
|
|
32
|
-
mmrelay-1.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
33
|
-
mmrelay-1.1.3.dist-info/entry_points.txt,sha256=SJZwGUOEpQ-qx4H8UL4xKFnKeInGUaZNW1I0ddjK7Ws,45
|
|
34
|
-
mmrelay-1.1.3.dist-info/top_level.txt,sha256=B_ZLCRm7NYAmI3PipRUyHGymP-C-q16LSeMGzmqJfo4,8
|
|
35
|
-
mmrelay-1.1.3.dist-info/RECORD,,
|
|
File without changes
|