mmrelay 1.2.2__py3-none-any.whl → 1.2.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 +27 -30
- mmrelay/config.py +14 -16
- mmrelay/constants/queue.py +4 -1
- mmrelay/e2ee_utils.py +7 -2
- mmrelay/log_utils.py +29 -8
- mmrelay/main.py +14 -5
- mmrelay/matrix_utils.py +73 -73
- mmrelay/meshtastic_utils.py +20 -21
- mmrelay/message_queue.py +16 -15
- mmrelay/plugins/base_plugin.py +7 -7
- mmrelay/plugins/mesh_relay_plugin.py +16 -17
- mmrelay/setup_utils.py +5 -5
- mmrelay/tools/sample-docker-compose-prebuilt.yaml +18 -7
- mmrelay/tools/sample-docker-compose.yaml +18 -7
- mmrelay/windows_utils.py +1 -1
- {mmrelay-1.2.2.dist-info → mmrelay-1.2.4.dist-info}/METADATA +2 -2
- {mmrelay-1.2.2.dist-info → mmrelay-1.2.4.dist-info}/RECORD +22 -22
- {mmrelay-1.2.2.dist-info → mmrelay-1.2.4.dist-info}/WHEEL +0 -0
- {mmrelay-1.2.2.dist-info → mmrelay-1.2.4.dist-info}/entry_points.txt +0 -0
- {mmrelay-1.2.2.dist-info → mmrelay-1.2.4.dist-info}/licenses/LICENSE +0 -0
- {mmrelay-1.2.2.dist-info → mmrelay-1.2.4.dist-info}/top_level.txt +0 -0
mmrelay/__init__.py
CHANGED
mmrelay/cli.py
CHANGED
|
@@ -238,11 +238,11 @@ def print_version():
|
|
|
238
238
|
def _validate_e2ee_dependencies():
|
|
239
239
|
"""
|
|
240
240
|
Check whether end-to-end encryption (E2EE) is usable on the current platform.
|
|
241
|
-
|
|
241
|
+
|
|
242
242
|
Returns:
|
|
243
243
|
bool: True if the platform is supported and required E2EE libraries can be imported;
|
|
244
244
|
False otherwise.
|
|
245
|
-
|
|
245
|
+
|
|
246
246
|
Notes:
|
|
247
247
|
- This function performs only local checks (platform and importability) and does not perform
|
|
248
248
|
network I/O.
|
|
@@ -1499,13 +1499,13 @@ def handle_auth_logout(args):
|
|
|
1499
1499
|
def handle_service_command(args):
|
|
1500
1500
|
"""
|
|
1501
1501
|
Dispatch service-related subcommands.
|
|
1502
|
-
|
|
1502
|
+
|
|
1503
1503
|
Currently supports the "install" subcommand which imports and runs mmrelay.setup_utils.install_service().
|
|
1504
1504
|
Returns 0 on successful installation, 1 on failure or for unknown subcommands.
|
|
1505
|
-
|
|
1505
|
+
|
|
1506
1506
|
Parameters:
|
|
1507
1507
|
args: argparse.Namespace with a `service_command` attribute indicating the requested action.
|
|
1508
|
-
|
|
1508
|
+
|
|
1509
1509
|
Returns:
|
|
1510
1510
|
int: Exit code (0 on success, 1 on error).
|
|
1511
1511
|
"""
|
|
@@ -1524,15 +1524,15 @@ def handle_service_command(args):
|
|
|
1524
1524
|
|
|
1525
1525
|
def _diagnose_config_paths(args):
|
|
1526
1526
|
"""
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1527
|
+
Prints a diagnostic summary of resolved configuration file search paths and their directory accessibility.
|
|
1528
|
+
|
|
1529
|
+
Each candidate config path is printed with a status icon:
|
|
1530
1530
|
- ✅ directory exists and is writable
|
|
1531
1531
|
- ⚠️ directory exists but is not writable
|
|
1532
1532
|
- ❌ directory does not exist
|
|
1533
|
-
|
|
1533
|
+
|
|
1534
1534
|
Parameters:
|
|
1535
|
-
args (argparse.Namespace):
|
|
1535
|
+
args (argparse.Namespace): CLI arguments used to determine the ordered list of candidate config paths (passed to get_config_paths).
|
|
1536
1536
|
"""
|
|
1537
1537
|
print("1. Testing configuration paths...")
|
|
1538
1538
|
from mmrelay.config import get_config_paths
|
|
@@ -1551,11 +1551,11 @@ def _diagnose_config_paths(args):
|
|
|
1551
1551
|
def _diagnose_sample_config_accessibility():
|
|
1552
1552
|
"""
|
|
1553
1553
|
Check availability of the bundled sample configuration and print a short diagnostic.
|
|
1554
|
-
|
|
1554
|
+
|
|
1555
1555
|
Performs two non-destructive checks and prints human-readable results:
|
|
1556
1556
|
1) Verifies whether the sample config file exists at the path returned by mmrelay.tools.get_sample_config_path().
|
|
1557
1557
|
2) Attempts to read the embedded resource "sample_config.yaml" from the mmrelay.tools package via importlib.resources and reports success and the content length.
|
|
1558
|
-
|
|
1558
|
+
|
|
1559
1559
|
Returns:
|
|
1560
1560
|
bool: True if a filesystem sample config exists at the resolved path; False otherwise.
|
|
1561
1561
|
"""
|
|
@@ -1586,18 +1586,15 @@ def _diagnose_sample_config_accessibility():
|
|
|
1586
1586
|
|
|
1587
1587
|
def _diagnose_platform_specific(args):
|
|
1588
1588
|
"""
|
|
1589
|
-
Run platform-specific diagnostic checks.
|
|
1590
|
-
|
|
1591
|
-
On Windows,
|
|
1592
|
-
|
|
1593
|
-
platforms this reports that platform-specific tests are not required.
|
|
1594
|
-
|
|
1589
|
+
Run platform-specific diagnostic checks and print a concise report.
|
|
1590
|
+
|
|
1591
|
+
On Windows, executes Windows-specific requirement checks and a configuration-generation test using the provided CLI arguments; on non-Windows platforms, reports that platform-specific tests are not required.
|
|
1592
|
+
|
|
1595
1593
|
Parameters:
|
|
1596
|
-
args (argparse.Namespace):
|
|
1597
|
-
|
|
1598
|
-
|
|
1594
|
+
args (argparse.Namespace): CLI arguments forwarded to the Windows configuration-generation test (used only when running on Windows).
|
|
1595
|
+
|
|
1599
1596
|
Returns:
|
|
1600
|
-
bool: True if
|
|
1597
|
+
bool: `True` if Windows checks were executed (running on Windows), `False` otherwise.
|
|
1601
1598
|
"""
|
|
1602
1599
|
print("3. Platform-specific diagnostics...")
|
|
1603
1600
|
import sys
|
|
@@ -1704,7 +1701,7 @@ logging:
|
|
|
1704
1701
|
def _diagnose_minimal_config_template():
|
|
1705
1702
|
"""
|
|
1706
1703
|
Validate the built-in minimal YAML configuration template and print a concise pass/fail status.
|
|
1707
|
-
|
|
1704
|
+
|
|
1708
1705
|
Attempts to parse the string returned by _get_minimal_config_template() using yaml.safe_load. Prints a single-line result showing a ✅ with the template character length when the template is valid YAML, or a ❌ with the YAML parsing error when invalid. This is a non-destructive diagnostic helper that prints output and does not return a value.
|
|
1709
1706
|
"""
|
|
1710
1707
|
print("4. Testing minimal config template fallback...")
|
|
@@ -1720,15 +1717,15 @@ def _diagnose_minimal_config_template():
|
|
|
1720
1717
|
|
|
1721
1718
|
def handle_config_diagnose(args):
|
|
1722
1719
|
"""
|
|
1723
|
-
Run non-destructive diagnostics for the MMRelay configuration subsystem and print a human-readable report.
|
|
1724
|
-
|
|
1725
|
-
Performs four checks: resolves candidate
|
|
1726
|
-
|
|
1720
|
+
Run a set of non-destructive diagnostics for the MMRelay configuration subsystem and print a concise, human-readable report.
|
|
1721
|
+
|
|
1722
|
+
Performs four checks without modifying user files: (1) resolves and reports candidate configuration file paths and their directory accessibility, (2) verifies availability and readability of the packaged sample configuration, (3) executes platform-specific diagnostics (Windows checks when applicable), and (4) validates the built-in minimal YAML configuration template. Results and actionable guidance are written to stdout/stderr; additional Windows-specific guidance may be printed to stderr on unexpected failures.
|
|
1723
|
+
|
|
1727
1724
|
Parameters:
|
|
1728
|
-
args (argparse.Namespace): Parsed CLI arguments used to
|
|
1729
|
-
|
|
1725
|
+
args (argparse.Namespace): Parsed CLI arguments used to determine configuration search paths and to control platform-specific diagnostic behavior.
|
|
1726
|
+
|
|
1730
1727
|
Returns:
|
|
1731
|
-
int: Exit code
|
|
1728
|
+
int: Exit code where `0` indicates diagnostics completed successfully and `1` indicates a failure occurred (an error summary is printed to stderr).
|
|
1732
1729
|
"""
|
|
1733
1730
|
print("MMRelay Configuration System Diagnostics")
|
|
1734
1731
|
print("=" * 40)
|
mmrelay/config.py
CHANGED
|
@@ -196,14 +196,12 @@ def get_log_dir():
|
|
|
196
196
|
|
|
197
197
|
def get_e2ee_store_dir():
|
|
198
198
|
"""
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
On Linux and macOS
|
|
202
|
-
|
|
203
|
-
platformdirs.user_data_dir(APP_NAME, APP_AUTHOR)/store.
|
|
204
|
-
|
|
199
|
+
Get the absolute path to the application's end-to-end encryption (E2EE) data store directory, creating it if necessary.
|
|
200
|
+
|
|
201
|
+
On Linux and macOS the directory is located under the application base directory; on Windows it uses the configured custom data directory when set, otherwise the platform-specific user data directory. The directory will be created if it does not exist.
|
|
202
|
+
|
|
205
203
|
Returns:
|
|
206
|
-
str: Absolute path to the ensured store directory.
|
|
204
|
+
store_dir (str): Absolute path to the ensured E2EE store directory.
|
|
207
205
|
"""
|
|
208
206
|
if sys.platform in ["linux", "darwin"]:
|
|
209
207
|
# Use ~/.mmrelay/store/ for Linux and Mac
|
|
@@ -388,12 +386,12 @@ def is_e2ee_enabled(config):
|
|
|
388
386
|
def check_e2ee_enabled_silently(args=None):
|
|
389
387
|
"""
|
|
390
388
|
Check silently whether End-to-End Encryption (E2EE) is enabled in the first readable configuration file.
|
|
391
|
-
|
|
389
|
+
|
|
392
390
|
Searches candidate configuration paths returned by get_config_paths(args) in priority order, loads the first readable YAML file, and returns True if that configuration enables E2EE (via is_e2ee_enabled). I/O and YAML parsing errors are ignored and the function continues to the next candidate. On Windows this always returns False.
|
|
393
|
-
|
|
391
|
+
|
|
394
392
|
Parameters:
|
|
395
393
|
args (optional): Parsed command-line arguments that can influence config search order.
|
|
396
|
-
|
|
394
|
+
|
|
397
395
|
Returns:
|
|
398
396
|
bool: True if E2EE is enabled in the first valid configuration file found; otherwise False.
|
|
399
397
|
"""
|
|
@@ -493,16 +491,16 @@ def load_credentials():
|
|
|
493
491
|
def save_credentials(credentials):
|
|
494
492
|
"""
|
|
495
493
|
Persist a JSON-serializable credentials mapping to <base_dir>/credentials.json.
|
|
496
|
-
|
|
494
|
+
|
|
497
495
|
Writes the provided credentials (a JSON-serializable mapping) to the application's
|
|
498
496
|
base configuration directory as credentials.json, creating the base directory if
|
|
499
497
|
necessary. On Unix-like systems the file permissions are adjusted to be
|
|
500
498
|
restrictive (0o600) when possible. I/O and permission errors are caught and
|
|
501
499
|
logged; the function does not raise them.
|
|
502
|
-
|
|
500
|
+
|
|
503
501
|
Parameters:
|
|
504
502
|
credentials (dict): JSON-serializable mapping of credentials to persist.
|
|
505
|
-
|
|
503
|
+
|
|
506
504
|
Returns:
|
|
507
505
|
None
|
|
508
506
|
"""
|
|
@@ -822,18 +820,18 @@ def load_config(config_file=None, args=None):
|
|
|
822
820
|
def validate_yaml_syntax(config_content, config_path):
|
|
823
821
|
"""
|
|
824
822
|
Validate YAML text for syntax and common style issues, parse it with PyYAML, and return results.
|
|
825
|
-
|
|
823
|
+
|
|
826
824
|
Performs lightweight line-based checks for frequent mistakes (using '=' instead of ':'
|
|
827
825
|
for mappings and non-standard boolean words like 'yes'/'no' or 'on'/'off') and then
|
|
828
826
|
attempts to parse the content with yaml.safe_load. If only style warnings are found,
|
|
829
827
|
parsing is considered successful and warnings are returned; if parsing fails or true
|
|
830
828
|
syntax errors are detected, a detailed error message is returned that references
|
|
831
829
|
config_path to identify the source.
|
|
832
|
-
|
|
830
|
+
|
|
833
831
|
Parameters:
|
|
834
832
|
config_content (str): Raw YAML text to validate.
|
|
835
833
|
config_path (str): Path or label used in error messages to identify the source of the content.
|
|
836
|
-
|
|
834
|
+
|
|
837
835
|
Returns:
|
|
838
836
|
tuple:
|
|
839
837
|
is_valid (bool): True if YAML parsed successfully (style warnings allowed), False on syntax/parsing error.
|
mmrelay/constants/queue.py
CHANGED
|
@@ -6,7 +6,10 @@ delays, size limits, and water marks for queue management.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
# Message timing constants
|
|
9
|
-
DEFAULT_MESSAGE_DELAY =
|
|
9
|
+
DEFAULT_MESSAGE_DELAY = (
|
|
10
|
+
2.5 # Set above the 2.0s firmware limit to prevent message dropping
|
|
11
|
+
)
|
|
12
|
+
MINIMUM_MESSAGE_DELAY = 2.1 # Minimum delay enforced to stay above firmware limit
|
|
10
13
|
|
|
11
14
|
# Queue size management
|
|
12
15
|
MAX_QUEUE_SIZE = 500
|
mmrelay/e2ee_utils.py
CHANGED
|
@@ -80,8 +80,13 @@ def get_e2ee_status(
|
|
|
80
80
|
importlib.import_module("olm")
|
|
81
81
|
|
|
82
82
|
if os.getenv("MMRELAY_TESTING") != "1":
|
|
83
|
-
importlib.import_module("nio.crypto")
|
|
84
|
-
|
|
83
|
+
nio_crypto = importlib.import_module("nio.crypto")
|
|
84
|
+
if not hasattr(nio_crypto, "OlmDevice"):
|
|
85
|
+
raise ImportError("nio.crypto.OlmDevice is unavailable")
|
|
86
|
+
|
|
87
|
+
nio_store = importlib.import_module("nio.store")
|
|
88
|
+
if not hasattr(nio_store, "SqliteStore"):
|
|
89
|
+
raise ImportError("nio.store.SqliteStore is unavailable")
|
|
85
90
|
|
|
86
91
|
status["dependencies_installed"] = True
|
|
87
92
|
except ImportError:
|
mmrelay/log_utils.py
CHANGED
|
@@ -69,14 +69,13 @@ _COMPONENT_LOGGERS = {
|
|
|
69
69
|
|
|
70
70
|
def configure_component_debug_logging():
|
|
71
71
|
"""
|
|
72
|
-
Configure log levels for external component loggers based on config
|
|
72
|
+
Configure log levels and handlers for external component loggers based on config.
|
|
73
73
|
|
|
74
|
-
Reads
|
|
75
|
-
-
|
|
76
|
-
-
|
|
77
|
-
- string: interpret as a logging level name (case-insensitive); invalid names fall back to DEBUG
|
|
74
|
+
Reads `config["logging"]["debug"]` and for each component:
|
|
75
|
+
- If enabled (True or a valid log level string), sets the component's loggers to the specified level and attaches the main application's handlers to them. This makes component logs appear in the console and log file.
|
|
76
|
+
- If disabled (falsy or missing), silences the component by setting its loggers to a level higher than CRITICAL.
|
|
78
77
|
|
|
79
|
-
This function
|
|
78
|
+
This function runs only once. It is not thread-safe and should be called early in the application startup, after the main logger is configured but before other modules are imported.
|
|
80
79
|
"""
|
|
81
80
|
global _component_debug_configured, config
|
|
82
81
|
|
|
@@ -84,7 +83,22 @@ def configure_component_debug_logging():
|
|
|
84
83
|
if _component_debug_configured or config is None:
|
|
85
84
|
return
|
|
86
85
|
|
|
87
|
-
|
|
86
|
+
# Get the main application logger and its handlers to attach to component loggers
|
|
87
|
+
main_logger = logging.getLogger(APP_DISPLAY_NAME)
|
|
88
|
+
main_handlers = main_logger.handlers
|
|
89
|
+
debug_settings = config.get("logging", {}).get("debug")
|
|
90
|
+
|
|
91
|
+
# Ensure debug_config is a dictionary, handling malformed configs gracefully
|
|
92
|
+
if isinstance(debug_settings, dict):
|
|
93
|
+
debug_config = debug_settings
|
|
94
|
+
else:
|
|
95
|
+
if debug_settings is not None:
|
|
96
|
+
main_logger.warning(
|
|
97
|
+
"Debug logging section is not a dictionary. "
|
|
98
|
+
"All component debug logging will be disabled. "
|
|
99
|
+
"Check your config.yaml debug section formatting."
|
|
100
|
+
)
|
|
101
|
+
debug_config = {}
|
|
88
102
|
|
|
89
103
|
for component, loggers in _COMPONENT_LOGGERS.items():
|
|
90
104
|
component_config = debug_config.get(component)
|
|
@@ -105,8 +119,15 @@ def configure_component_debug_logging():
|
|
|
105
119
|
# Invalid config, fall back to DEBUG
|
|
106
120
|
log_level = logging.DEBUG
|
|
107
121
|
|
|
122
|
+
# Configure all loggers for this component
|
|
108
123
|
for logger_name in loggers:
|
|
109
|
-
logging.getLogger(logger_name)
|
|
124
|
+
component_logger = logging.getLogger(logger_name)
|
|
125
|
+
component_logger.setLevel(log_level)
|
|
126
|
+
component_logger.propagate = False # Prevent duplicate logging
|
|
127
|
+
# Attach main handlers to the component logger
|
|
128
|
+
for handler in main_handlers:
|
|
129
|
+
if handler not in component_logger.handlers:
|
|
130
|
+
component_logger.addHandler(handler)
|
|
110
131
|
else:
|
|
111
132
|
# Component debug is disabled - completely suppress external library logging
|
|
112
133
|
# Use a level higher than CRITICAL to effectively disable all messages
|
mmrelay/main.py
CHANGED
|
@@ -73,9 +73,18 @@ def print_banner():
|
|
|
73
73
|
|
|
74
74
|
async def main(config):
|
|
75
75
|
"""
|
|
76
|
-
|
|
76
|
+
Coordinate the asynchronous relay loop between Meshtastic and Matrix clients.
|
|
77
77
|
|
|
78
|
-
Initializes the database
|
|
78
|
+
Initializes the database and plugins, starts the message queue, connects to Meshtastic and Matrix, joins configured Matrix rooms, registers event callbacks, monitors connection health, runs the Matrix sync loop with automatic retries, and ensures an orderly shutdown of all components (including optional message map wiping on startup and shutdown).
|
|
79
|
+
|
|
80
|
+
Parameters:
|
|
81
|
+
config (dict): Application configuration mapping. Expected keys used by this function include:
|
|
82
|
+
- "matrix_rooms": list of room dicts with at least an "id" entry,
|
|
83
|
+
- "meshtastic": optional dict with "message_delay",
|
|
84
|
+
- "database" (preferred) or legacy "db": optional dict containing "msg_map" with "wipe_on_restart" boolean.
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
ConnectionError: If connecting to Matrix fails and no Matrix client can be obtained.
|
|
79
88
|
"""
|
|
80
89
|
# Extract Matrix configuration
|
|
81
90
|
from typing import List
|
|
@@ -154,9 +163,9 @@ async def main(config):
|
|
|
154
163
|
|
|
155
164
|
async def shutdown():
|
|
156
165
|
"""
|
|
157
|
-
Signal the application to
|
|
158
|
-
|
|
159
|
-
|
|
166
|
+
Signal the application to begin shutdown.
|
|
167
|
+
|
|
168
|
+
Set the Meshtastic shutdown flag and set the local shutdown event so any coroutines waiting on that event can start cleanup.
|
|
160
169
|
"""
|
|
161
170
|
matrix_logger.info("Shutdown signal received. Closing down...")
|
|
162
171
|
meshtastic_utils.shutting_down = True # Set the shutting_down flag
|
mmrelay/matrix_utils.py
CHANGED
|
@@ -13,7 +13,6 @@ import time
|
|
|
13
13
|
from typing import Any, Dict, Optional, Union
|
|
14
14
|
from urllib.parse import urlparse
|
|
15
15
|
|
|
16
|
-
import meshtastic.protobuf.portnums_pb2
|
|
17
16
|
from nio import (
|
|
18
17
|
AsyncClient,
|
|
19
18
|
AsyncClientConfig,
|
|
@@ -113,15 +112,15 @@ def _is_room_alias(value: Any) -> bool:
|
|
|
113
112
|
def _iter_room_alias_entries(mapping):
|
|
114
113
|
"""
|
|
115
114
|
Yield (alias_or_id, setter) pairs for entries in a Matrix room mapping.
|
|
116
|
-
|
|
115
|
+
|
|
117
116
|
Each yielded tuple contains:
|
|
118
117
|
- alias_or_id (str): the room alias or room ID found in the entry (may be an alias starting with '#' or a canonical room ID). If a dict entry has no `"id"` key, an empty string is yielded.
|
|
119
118
|
- setter (callable): a function accepting a single argument `new_id` which updates the underlying mapping in-place to replace the alias with the resolved room ID.
|
|
120
|
-
|
|
119
|
+
|
|
121
120
|
Supports two mapping shapes:
|
|
122
121
|
- list: items may be strings (alias/ID) or dicts with an `"id"` key.
|
|
123
122
|
- dict: values may be strings (alias/ID) or dicts with an `"id"` key.
|
|
124
|
-
|
|
123
|
+
|
|
125
124
|
The setter updates the original collection (list element or dict value) so callers can resolve aliases and persist resolved IDs back into the provided mapping.
|
|
126
125
|
"""
|
|
127
126
|
|
|
@@ -150,13 +149,13 @@ def _iter_room_alias_entries(mapping):
|
|
|
150
149
|
async def _resolve_aliases_in_mapping(mapping, resolver):
|
|
151
150
|
"""
|
|
152
151
|
Resolve Matrix room alias entries found in a mapping (list or dict) by calling an async resolver and replacing aliases with resolved room IDs in-place.
|
|
153
|
-
|
|
152
|
+
|
|
154
153
|
This function iterates entries produced by _iter_room_alias_entries(mapping). For each entry whose key/value looks like a Matrix room alias (a string starting with '#'), it awaits the provided resolver coroutine with the alias; if the resolver returns a truthy room ID, the corresponding entry in the original mapping is updated via the entry's setter. If mapping is not a list or dict, the function logs a warning and returns without modifying anything.
|
|
155
|
-
|
|
154
|
+
|
|
156
155
|
Parameters:
|
|
157
156
|
mapping (list|dict): A mapping of Matrix rooms where some entries may be aliases (e.g., "#room:example.org").
|
|
158
157
|
resolver (Callable[[str], Awaitable[Optional[str]]]): Async callable that accepts an alias and returns a resolved room ID (or falsy on failure).
|
|
159
|
-
|
|
158
|
+
|
|
160
159
|
Returns:
|
|
161
160
|
None
|
|
162
161
|
"""
|
|
@@ -178,12 +177,12 @@ async def _resolve_aliases_in_mapping(mapping, resolver):
|
|
|
178
177
|
def _update_room_id_in_mapping(mapping, alias, resolved_id) -> bool:
|
|
179
178
|
"""
|
|
180
179
|
Replace a room alias with its resolved room ID in a mapping.
|
|
181
|
-
|
|
180
|
+
|
|
182
181
|
Parameters:
|
|
183
182
|
mapping (list|dict): A matrix_rooms mapping represented as a list of aliases or a dict of entries; only list and dict types are supported.
|
|
184
183
|
alias (str): The room alias to replace (e.g., "#room:server").
|
|
185
184
|
resolved_id (str): The canonical room ID to substitute for the alias (e.g., "!abcdef:server").
|
|
186
|
-
|
|
185
|
+
|
|
187
186
|
Returns:
|
|
188
187
|
bool: True if the alias was found and replaced with resolved_id; False if the mapping type is unsupported or the alias was not present.
|
|
189
188
|
"""
|
|
@@ -789,29 +788,18 @@ def bot_command(command, event):
|
|
|
789
788
|
|
|
790
789
|
async def connect_matrix(passed_config=None):
|
|
791
790
|
"""
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
This routine selects credentials in the following order:
|
|
795
|
-
1. credentials.json (preferred; restores full session including device_id and E2EE store).
|
|
796
|
-
2. Automatic login using username/password from the provided config (saved to credentials.json on success).
|
|
797
|
-
3. Direct token-based values from the config matrix section.
|
|
791
|
+
Initialize and return a configured matrix-nio AsyncClient connected to the configured Matrix homeserver.
|
|
798
792
|
|
|
799
|
-
|
|
800
|
-
- Validates presence of a top-level "matrix_rooms" configuration and raises ValueError if missing.
|
|
801
|
-
- Builds an AsyncClient with a certifi-backed SSL context when available.
|
|
802
|
-
- When E2EE is enabled in configuration and dependencies are present, prepares the encryption store, restores session state, and uploads device keys if needed.
|
|
803
|
-
- Performs an initial full_state sync and resolves room aliases configured as aliases to room IDs.
|
|
804
|
-
- Populates module-level globals used elsewhere (e.g., matrix_client, bot_user_name, matrix_homeserver, matrix_rooms, matrix_access_token, bot_user_id).
|
|
805
|
-
- Returns the initialized AsyncClient instance ready for use.
|
|
793
|
+
Creates or restores client credentials (prefers credentials.json, falls back to automatic login using username/password from config, then to direct tokens in config), optionally enables End-to-End Encryption when configured and dependencies are available, performs an initial full-state sync to populate rooms, resolves room aliases found in configuration, and sets module-level connection state used by other functions.
|
|
806
794
|
|
|
807
795
|
Parameters:
|
|
808
|
-
passed_config (dict | None): Optional configuration to use for this connection attempt
|
|
796
|
+
passed_config (dict | None): Optional configuration to use for this connection attempt; when provided it overrides the module-level config for this call.
|
|
809
797
|
|
|
810
798
|
Returns:
|
|
811
|
-
AsyncClient | None:
|
|
799
|
+
AsyncClient | None: A ready-to-use matrix-nio AsyncClient on success, or `None` if connection or credentials are unavailable.
|
|
812
800
|
|
|
813
801
|
Raises:
|
|
814
|
-
ValueError: If the top-level "matrix_rooms" configuration is missing
|
|
802
|
+
ValueError: If the required top-level "matrix_rooms" configuration is missing.
|
|
815
803
|
ConnectionError: If the initial Matrix sync fails or times out.
|
|
816
804
|
"""
|
|
817
805
|
global matrix_client, bot_user_name, matrix_homeserver, matrix_rooms, matrix_access_token, bot_user_id, config
|
|
@@ -1010,8 +998,15 @@ async def connect_matrix(passed_config=None):
|
|
|
1010
998
|
# Also check for other required E2EE dependencies unless tests skip them
|
|
1011
999
|
if os.getenv("MMRELAY_TESTING") != "1":
|
|
1012
1000
|
try:
|
|
1013
|
-
|
|
1014
|
-
|
|
1001
|
+
nio_crypto = importlib.import_module("nio.crypto")
|
|
1002
|
+
if not hasattr(nio_crypto, "OlmDevice"):
|
|
1003
|
+
raise ImportError("nio.crypto.OlmDevice is unavailable")
|
|
1004
|
+
|
|
1005
|
+
nio_store = importlib.import_module("nio.store")
|
|
1006
|
+
if not hasattr(nio_store, "SqliteStore"):
|
|
1007
|
+
raise ImportError(
|
|
1008
|
+
"nio.store.SqliteStore is unavailable"
|
|
1009
|
+
)
|
|
1015
1010
|
|
|
1016
1011
|
logger.debug("All E2EE dependencies are available")
|
|
1017
1012
|
except ImportError:
|
|
@@ -1034,6 +1029,10 @@ async def connect_matrix(passed_config=None):
|
|
|
1034
1029
|
"Skipping additional E2EE dependency imports in test mode"
|
|
1035
1030
|
)
|
|
1036
1031
|
|
|
1032
|
+
if e2ee_enabled:
|
|
1033
|
+
# Ensure nio still receives a store path even when dependency
|
|
1034
|
+
# checks are skipped (e.g. production runs without MMRELAY_TESTING);
|
|
1035
|
+
# without this the client will not load encryption state.
|
|
1037
1036
|
# Get store path from config or use default
|
|
1038
1037
|
if (
|
|
1039
1038
|
"encryption" in config["matrix"]
|
|
@@ -1050,8 +1049,6 @@ async def connect_matrix(passed_config=None):
|
|
|
1050
1049
|
config["matrix"]["e2ee"]["store_path"]
|
|
1051
1050
|
)
|
|
1052
1051
|
else:
|
|
1053
|
-
from mmrelay.config import get_e2ee_store_dir
|
|
1054
|
-
|
|
1055
1052
|
e2ee_store_path = get_e2ee_store_dir()
|
|
1056
1053
|
|
|
1057
1054
|
# Create store directory if it doesn't exist
|
|
@@ -1215,7 +1212,7 @@ async def connect_matrix(passed_config=None):
|
|
|
1215
1212
|
async def _resolve_alias(alias: str) -> Optional[str]:
|
|
1216
1213
|
"""
|
|
1217
1214
|
Resolve a Matrix room alias to its canonical room ID.
|
|
1218
|
-
|
|
1215
|
+
|
|
1219
1216
|
Attempts to resolve the provided room alias using the module's Matrix client. Returns the resolved room ID string on success; returns None if the alias cannot be resolved or if an error/timeout occurs (errors from the underlying nio client are caught and handled internally).
|
|
1220
1217
|
"""
|
|
1221
1218
|
logger.debug(f"Resolving alias from config: {alias}")
|
|
@@ -1778,17 +1775,17 @@ async def login_matrix_bot(
|
|
|
1778
1775
|
async def join_matrix_room(matrix_client, room_id_or_alias: str) -> None:
|
|
1779
1776
|
"""
|
|
1780
1777
|
Join the bot to a Matrix room by ID or alias.
|
|
1781
|
-
|
|
1778
|
+
|
|
1782
1779
|
Resolves a room alias (e.g. "#room:server") to its canonical room ID, updates the in-memory
|
|
1783
1780
|
matrix_rooms mapping with the resolved ID (if available), and attempts to join the resolved
|
|
1784
1781
|
room ID. No-op if the client is already joined to the room. Errors during alias resolution
|
|
1785
1782
|
or join are caught and logged; the function does not raise exceptions.
|
|
1786
|
-
|
|
1783
|
+
|
|
1787
1784
|
Parameters documented only where meaning is not obvious:
|
|
1788
1785
|
room_id_or_alias (str): A Matrix room identifier, either a canonical room ID (e.g. "!abc:server")
|
|
1789
1786
|
or a room alias (starts with '#'). When an alias is provided, it will be resolved and
|
|
1790
1787
|
the resolved room ID will be used for joining and recorded in the module's matrix_rooms mapping.
|
|
1791
|
-
|
|
1788
|
+
|
|
1792
1789
|
Returns:
|
|
1793
1790
|
None
|
|
1794
1791
|
"""
|
|
@@ -2221,12 +2218,12 @@ def strip_quoted_lines(text: str) -> str:
|
|
|
2221
2218
|
async def get_user_display_name(room, event):
|
|
2222
2219
|
"""
|
|
2223
2220
|
Return the display name for the event sender, preferring a room-specific name.
|
|
2224
|
-
|
|
2221
|
+
|
|
2225
2222
|
If the room provides a per-room display name for the sender, that name is returned.
|
|
2226
2223
|
Otherwise the function performs an asynchronous lookup against the homeserver for the
|
|
2227
2224
|
user's global display name and returns it if present. If no display name is available,
|
|
2228
2225
|
the sender's Matrix ID (MXID) is returned.
|
|
2229
|
-
|
|
2226
|
+
|
|
2230
2227
|
Returns:
|
|
2231
2228
|
str: A human-readable display name or the sender's MXID.
|
|
2232
2229
|
"""
|
|
@@ -2320,15 +2317,20 @@ async def send_reply_to_meshtastic(
|
|
|
2320
2317
|
reply_id=None,
|
|
2321
2318
|
):
|
|
2322
2319
|
"""
|
|
2323
|
-
Enqueue a Matrix reply
|
|
2324
|
-
|
|
2325
|
-
If
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2320
|
+
Enqueue a Matrix reply to be delivered over Meshtastic as either a structured reply or a regular broadcast.
|
|
2321
|
+
|
|
2322
|
+
If broadcasting is disabled this function does nothing. When storage_enabled is True, it constructs a mapping record that links the originating Matrix event to the Meshtastic message and attaches it to the queued message so replies and reactions can be correlated later. Errors are logged; the function does not raise.
|
|
2323
|
+
|
|
2324
|
+
Parameters:
|
|
2325
|
+
reply_message (str): Text payload already formatted for Meshtastic.
|
|
2326
|
+
full_display_name (str): Sender display name used in queue descriptions and logs.
|
|
2327
|
+
room_config (dict): Room-specific configuration; must contain "meshtastic_channel" (integer channel index).
|
|
2328
|
+
room: Matrix room object; its room_id is used for mapping metadata.
|
|
2329
|
+
event: Matrix event object; its event_id is used for mapping metadata.
|
|
2330
|
+
text (str): Original Matrix message text used when building mapping metadata.
|
|
2331
|
+
storage_enabled (bool): If True, create and attach a message-mapping record to the queued Meshtastic message.
|
|
2332
|
+
local_meshnet_name (str | None): Local meshnet name included in mapping metadata when present.
|
|
2333
|
+
reply_id (int | None): If provided, send as a structured Meshtastic reply targeting this Meshtastic message ID; otherwise send a regular broadcast.
|
|
2332
2334
|
"""
|
|
2333
2335
|
loop = asyncio.get_running_loop()
|
|
2334
2336
|
meshtastic_interface = await loop.run_in_executor(None, connect_meshtastic)
|
|
@@ -2433,22 +2435,26 @@ async def handle_matrix_reply(
|
|
|
2433
2435
|
meshnet_name=None,
|
|
2434
2436
|
):
|
|
2435
2437
|
"""
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
If the
|
|
2439
|
-
|
|
2438
|
+
Forward a Matrix reply to Meshtastic when the replied-to Matrix event maps to a Meshtastic message.
|
|
2439
|
+
|
|
2440
|
+
If the Matrix event identified by reply_to_event_id has an associated Meshtastic mapping, format a Meshtastic reply that preserves sender attribution and enqueue it referencing the original Meshtastic message ID. If no mapping exists, do nothing.
|
|
2441
|
+
|
|
2440
2442
|
Parameters:
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2443
|
+
room: Matrix room object where the reply originated.
|
|
2444
|
+
event: Matrix event object representing the reply.
|
|
2445
|
+
reply_to_event_id (str): Matrix event ID being replied to; used to locate the Meshtastic mapping.
|
|
2446
|
+
text (str): The reply text from Matrix.
|
|
2447
|
+
room_config (dict): Per-room relay configuration used when sending to Meshtastic.
|
|
2448
|
+
storage_enabled (bool): Whether message mapping/storage is enabled.
|
|
2449
|
+
local_meshnet_name (str): Local meshnet name used to determine cross-meshnet formatting.
|
|
2450
|
+
config (dict): Global relay configuration passed to formatting routines.
|
|
2451
|
+
mesh_text_override (str | None): Optional override text to send instead of the derived text.
|
|
2452
|
+
longname (str | None): Sender long display name used for prefixing.
|
|
2453
|
+
shortname (str | None): Sender short display name used for prefixing.
|
|
2454
|
+
meshnet_name (str | None): Remote meshnet name associated with the original mapping, if any.
|
|
2455
|
+
|
|
2450
2456
|
Returns:
|
|
2451
|
-
bool: True if a mapping was found and the reply was queued to Meshtastic
|
|
2457
|
+
bool: `True` if a mapping was found and the reply was queued to Meshtastic, `False` otherwise.
|
|
2452
2458
|
"""
|
|
2453
2459
|
# Look up the original message in the message map
|
|
2454
2460
|
loop = asyncio.get_running_loop()
|
|
@@ -2545,23 +2551,14 @@ async def on_room_message(
|
|
|
2545
2551
|
],
|
|
2546
2552
|
) -> None:
|
|
2547
2553
|
"""
|
|
2548
|
-
Handle an incoming Matrix room event and
|
|
2549
|
-
|
|
2550
|
-
Processes text, notice, emote, and reaction events (including replies and
|
|
2551
|
-
|
|
2552
|
-
- Uses per-room configuration and global interaction settings to decide whether to process or ignore the event.
|
|
2553
|
-
- Routes reactions back to the originating Meshtastic message when a mapping exists (supports local and remote-meshnet reaction handling).
|
|
2554
|
-
- Bridges Matrix replies to Meshtastic replies when a corresponding Meshtastic mapping is found and replies are enabled.
|
|
2555
|
-
- Relays regular Matrix messages to Meshtastic using configured prefix/truncation rules; handles special detection-sensor port forwarding.
|
|
2556
|
-
- Integrates with the plugin system; plugins may consume or modify messages. Messages identified as bot commands are not relayed to Meshtastic.
|
|
2557
|
-
|
|
2554
|
+
Handle an incoming Matrix room event and relay it to Meshtastic when applicable.
|
|
2555
|
+
|
|
2556
|
+
Processes text, notice, emote, and reaction events for configured rooms: ignores events from before the bot started and events sent by the bot itself; respects per-room configuration and global interaction settings; routes reactions back to the originating Meshtastic message when a mapping exists (including forwarding remote-meshnet emote reactions as radio text); bridges Matrix replies to Meshtastic replies when a corresponding mapping is found and replies are enabled; relays regular Matrix messages to Meshtastic using configured prefix and truncation rules; and honours detection-sensor forwarding when enabled. Integrates with the plugin system and treats recognized bot commands as non-relayed.
|
|
2557
|
+
|
|
2558
2558
|
Side effects:
|
|
2559
2559
|
- May enqueue Meshtastic send operations (text or data) via the internal queue.
|
|
2560
|
-
- May read
|
|
2561
|
-
- May call Matrix APIs (e.g., to fetch display names).
|
|
2562
|
-
|
|
2563
|
-
Returns:
|
|
2564
|
-
- None
|
|
2560
|
+
- May read and write persistent message mappings to support reply/reaction bridging.
|
|
2561
|
+
- May call Matrix APIs (e.g., to fetch display names) and connect to Meshtastic.
|
|
2565
2562
|
"""
|
|
2566
2563
|
# DEBUG: Log all Matrix message events to trace reception
|
|
2567
2564
|
logger.debug(
|
|
@@ -2971,6 +2968,9 @@ async def on_room_message(
|
|
|
2971
2968
|
if get_meshtastic_config_value(
|
|
2972
2969
|
config, "detection_sensor", DEFAULT_DETECTION_SENSOR
|
|
2973
2970
|
):
|
|
2971
|
+
# Import meshtastic protobuf only when needed to delay logger creation
|
|
2972
|
+
import meshtastic.protobuf.portnums_pb2
|
|
2973
|
+
|
|
2974
2974
|
success = queue_message(
|
|
2975
2975
|
meshtastic_interface.sendData,
|
|
2976
2976
|
data=full_message.encode("utf-8"),
|
mmrelay/meshtastic_utils.py
CHANGED
|
@@ -44,8 +44,17 @@ from mmrelay.constants.network import (
|
|
|
44
44
|
CONNECTION_TYPE_TCP,
|
|
45
45
|
DEFAULT_BACKOFF_TIME,
|
|
46
46
|
ERRNO_BAD_FILE_DESCRIPTOR,
|
|
47
|
-
INFINITE_RETRIES,
|
|
47
|
+
INFINITE_RETRIES,
|
|
48
48
|
)
|
|
49
|
+
from mmrelay.db_utils import (
|
|
50
|
+
get_longname,
|
|
51
|
+
get_message_map_by_meshtastic_id,
|
|
52
|
+
get_shortname,
|
|
53
|
+
save_longname,
|
|
54
|
+
save_shortname,
|
|
55
|
+
)
|
|
56
|
+
from mmrelay.log_utils import get_logger
|
|
57
|
+
from mmrelay.runtime_utils import is_running_as_service
|
|
49
58
|
|
|
50
59
|
# Maximum number of timeout retries when retries are configured as infinite.
|
|
51
60
|
MAX_TIMEOUT_RETRIES_INFINITE = 5
|
|
@@ -62,16 +71,6 @@ except ImportError:
|
|
|
62
71
|
pass
|
|
63
72
|
|
|
64
73
|
|
|
65
|
-
from mmrelay.db_utils import (
|
|
66
|
-
get_longname,
|
|
67
|
-
get_message_map_by_meshtastic_id,
|
|
68
|
-
get_shortname,
|
|
69
|
-
save_longname,
|
|
70
|
-
save_shortname,
|
|
71
|
-
)
|
|
72
|
-
from mmrelay.log_utils import get_logger
|
|
73
|
-
from mmrelay.runtime_utils import is_running_as_service
|
|
74
|
-
|
|
75
74
|
# Global config variable that will be set from config.py
|
|
76
75
|
config = None
|
|
77
76
|
|
|
@@ -153,15 +152,15 @@ def _submit_coro(coro, loop=None):
|
|
|
153
152
|
def _resolve_plugin_timeout(cfg: dict | None, default: float = 5.0) -> float:
|
|
154
153
|
"""
|
|
155
154
|
Resolve and return a positive plugin timeout value from the given configuration.
|
|
156
|
-
|
|
155
|
+
|
|
157
156
|
Attempts to read meshtastic.plugin_timeout from cfg and convert it to a positive float.
|
|
158
157
|
If the value is missing, non-numeric, or not greater than 0, the provided default is returned.
|
|
159
158
|
Invalid or non-positive values will cause a warning to be logged.
|
|
160
|
-
|
|
159
|
+
|
|
161
160
|
Parameters:
|
|
162
161
|
cfg (dict | None): Configuration mapping that may contain a "meshtastic" section.
|
|
163
162
|
default (float): Fallback timeout (seconds) used when cfg does not provide a valid value.
|
|
164
|
-
|
|
163
|
+
|
|
165
164
|
Returns:
|
|
166
165
|
float: A positive timeout in seconds.
|
|
167
166
|
"""
|
|
@@ -270,13 +269,13 @@ def serial_port_exists(port_name):
|
|
|
270
269
|
def connect_meshtastic(passed_config=None, force_connect=False):
|
|
271
270
|
"""
|
|
272
271
|
Establish and return a Meshtastic client connection (serial, BLE, or TCP), with configurable retries, exponential backoff, and single-time event subscription.
|
|
273
|
-
|
|
272
|
+
|
|
274
273
|
Attempts to (re)connect using the module configuration and updates module-level state on success (meshtastic_client, matrix_rooms, and event subscriptions). Supports the legacy "network" alias for TCP, verifies serial port presence before connecting, and honors a retry limit (or infinite retries when unspecified). On successful connection the client's node info and firmware metadata are probed and message/connection-lost handlers are subscribed once for the process lifetime.
|
|
275
|
-
|
|
274
|
+
|
|
276
275
|
Parameters:
|
|
277
276
|
passed_config (dict, optional): If provided, replaces the module-level configuration (and may update matrix_rooms).
|
|
278
277
|
force_connect (bool, optional): When True, forces creating a new connection even if one already exists.
|
|
279
|
-
|
|
278
|
+
|
|
280
279
|
Returns:
|
|
281
280
|
The connected Meshtastic client instance on success, or None if connection cannot be established or shutdown is in progress.
|
|
282
281
|
"""
|
|
@@ -464,7 +463,7 @@ def connect_meshtastic(passed_config=None, force_connect=False):
|
|
|
464
463
|
subscribed_to_connection_lost = True
|
|
465
464
|
logger.debug("Subscribed to meshtastic.connection.lost")
|
|
466
465
|
|
|
467
|
-
except (ConnectionRefusedError, MemoryError)
|
|
466
|
+
except (ConnectionRefusedError, MemoryError):
|
|
468
467
|
# Handle critical errors that should not be retried
|
|
469
468
|
logger.exception("Critical connection error")
|
|
470
469
|
return None
|
|
@@ -1142,17 +1141,17 @@ def sendTextReply(
|
|
|
1142
1141
|
):
|
|
1143
1142
|
"""
|
|
1144
1143
|
Send a Meshtastic text reply that references a previous Meshtastic message.
|
|
1145
|
-
|
|
1144
|
+
|
|
1146
1145
|
Builds a Data payload containing `text` and `reply_id`, wraps it in a MeshPacket on `channelIndex`,
|
|
1147
1146
|
and sends it using the provided Meshtastic interface.
|
|
1148
|
-
|
|
1147
|
+
|
|
1149
1148
|
Parameters:
|
|
1150
1149
|
text (str): UTF-8 text to send.
|
|
1151
1150
|
reply_id (int): ID of the Meshtastic message being replied to.
|
|
1152
1151
|
destinationId (int | str, optional): Recipient address or node ID (defaults to broadcast).
|
|
1153
1152
|
wantAck (bool, optional): If True, request an acknowledgement for the packet.
|
|
1154
1153
|
channelIndex (int, optional): Channel index to send the packet on.
|
|
1155
|
-
|
|
1154
|
+
|
|
1156
1155
|
Returns:
|
|
1157
1156
|
The result returned by the interface's _sendPacket call (typically the sent MeshPacket), or
|
|
1158
1157
|
None if the interface is not available or sending fails.
|
mmrelay/message_queue.py
CHANGED
|
@@ -473,11 +473,12 @@ class MessageQueue:
|
|
|
473
473
|
|
|
474
474
|
def _should_send_message(self) -> bool:
|
|
475
475
|
"""
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
Performs runtime checks:
|
|
479
|
-
|
|
480
|
-
|
|
476
|
+
Check whether the queue may send a Meshtastic message.
|
|
477
|
+
|
|
478
|
+
Performs runtime checks: returns True only if the reconnection flag is not set, a Meshtastic client object exists, and—if the client exposes `is_connected`—that check indicates the client is connected. If importing Meshtastic utilities raises ImportError, a critical log is emitted and the queue is stopped asynchronously.
|
|
479
|
+
|
|
480
|
+
Returns:
|
|
481
|
+
`True` if not reconnecting, a Meshtastic client exists, and the client is connected when checkable; `False` otherwise.
|
|
481
482
|
"""
|
|
482
483
|
# Import here to avoid circular imports
|
|
483
484
|
try:
|
|
@@ -517,18 +518,18 @@ class MessageQueue:
|
|
|
517
518
|
|
|
518
519
|
def _handle_message_mapping(self, result, mapping_info):
|
|
519
520
|
"""
|
|
520
|
-
Persist a
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
521
|
+
Persist a mapping from a sent Meshtastic message to a Matrix event and optionally prune old mappings.
|
|
522
|
+
|
|
523
|
+
Stores a mapping when `mapping_info` contains `matrix_event_id`, `room_id`, and `text`, using `result.id` as the Meshtastic message id. If `mapping_info["msgs_to_keep"]` is present and greater than 0, prunes older mappings to retain that many entries; otherwise uses DEFAULT_MSGS_TO_KEEP.
|
|
524
|
+
|
|
524
525
|
Parameters:
|
|
525
|
-
result:
|
|
526
|
+
result: An object returned by the send function with an `id` attribute representing the Meshtastic message id.
|
|
526
527
|
mapping_info (dict): Mapping details. Relevant keys:
|
|
527
|
-
- matrix_event_id (str)
|
|
528
|
-
- room_id (str)
|
|
529
|
-
- text (str)
|
|
530
|
-
- meshnet (optional):
|
|
531
|
-
- msgs_to_keep (optional, int):
|
|
528
|
+
- matrix_event_id (str): Matrix event ID to map to.
|
|
529
|
+
- room_id (str): Matrix room ID where the event was sent.
|
|
530
|
+
- text (str): Message text to associate with the mapping.
|
|
531
|
+
- meshnet (optional): Mesh network identifier to pass to storage.
|
|
532
|
+
- msgs_to_keep (optional, int): Number of mappings to retain when pruning.
|
|
532
533
|
"""
|
|
533
534
|
try:
|
|
534
535
|
# Import here to avoid circular imports
|
mmrelay/plugins/base_plugin.py
CHANGED
|
@@ -11,7 +11,7 @@ from mmrelay.constants.database import (
|
|
|
11
11
|
DEFAULT_MAX_DATA_ROWS_PER_NODE_BASE,
|
|
12
12
|
DEFAULT_TEXT_TRUNCATION_LENGTH,
|
|
13
13
|
)
|
|
14
|
-
from mmrelay.constants.queue import DEFAULT_MESSAGE_DELAY
|
|
14
|
+
from mmrelay.constants.queue import DEFAULT_MESSAGE_DELAY, MINIMUM_MESSAGE_DELAY
|
|
15
15
|
from mmrelay.db_utils import (
|
|
16
16
|
delete_plugin_data,
|
|
17
17
|
get_plugin_data,
|
|
@@ -77,7 +77,7 @@ class BasePlugin(ABC):
|
|
|
77
77
|
Raises:
|
|
78
78
|
ValueError: If the plugin name is not set via parameter or class attribute.
|
|
79
79
|
|
|
80
|
-
Loads plugin-specific configuration from the global config, validates assigned channels, and determines the response delay, enforcing a minimum of 2.
|
|
80
|
+
Loads plugin-specific configuration from the global config, validates assigned channels, and determines the response delay, enforcing a minimum of 2.1 seconds. Logs a warning if deprecated configuration options are used or if channels are not mapped.
|
|
81
81
|
"""
|
|
82
82
|
# Allow plugin_name to be passed as a parameter for simpler initialization
|
|
83
83
|
# This maintains backward compatibility while providing a cleaner API
|
|
@@ -174,12 +174,12 @@ class BasePlugin(ABC):
|
|
|
174
174
|
|
|
175
175
|
if delay is not None:
|
|
176
176
|
self.response_delay = delay
|
|
177
|
-
# Enforce minimum delay
|
|
178
|
-
if self.response_delay <
|
|
177
|
+
# Enforce minimum delay above firmware limit to prevent message dropping
|
|
178
|
+
if self.response_delay < MINIMUM_MESSAGE_DELAY:
|
|
179
179
|
self.logger.warning(
|
|
180
|
-
f"{delay_key} of {self.response_delay}s is below minimum of
|
|
180
|
+
f"{delay_key} of {self.response_delay}s is below minimum of {MINIMUM_MESSAGE_DELAY}s (above firmware limit). Using {MINIMUM_MESSAGE_DELAY}s."
|
|
181
181
|
)
|
|
182
|
-
self.response_delay =
|
|
182
|
+
self.response_delay = MINIMUM_MESSAGE_DELAY
|
|
183
183
|
|
|
184
184
|
def start(self):
|
|
185
185
|
"""
|
|
@@ -261,7 +261,7 @@ class BasePlugin(ABC):
|
|
|
261
261
|
"""
|
|
262
262
|
Return the configured delay in seconds before sending a Meshtastic response.
|
|
263
263
|
|
|
264
|
-
The delay is determined by the `meshtastic.message_delay` configuration option, defaulting to 2.
|
|
264
|
+
The delay is determined by the `meshtastic.message_delay` configuration option, defaulting to 2.5 seconds with a minimum of 2.1 seconds. The deprecated `plugin_response_delay` option is also supported for backward compatibility.
|
|
265
265
|
|
|
266
266
|
Returns:
|
|
267
267
|
float: The response delay in seconds.
|
|
@@ -122,7 +122,7 @@ class Plugin(BasePlugin):
|
|
|
122
122
|
|
|
123
123
|
if not channel_mapped:
|
|
124
124
|
self.logger.debug(f"Skipping message from unmapped channel {channel}")
|
|
125
|
-
return
|
|
125
|
+
return False
|
|
126
126
|
|
|
127
127
|
await matrix_client.room_send(
|
|
128
128
|
room_id=room["id"],
|
|
@@ -135,20 +135,19 @@ class Plugin(BasePlugin):
|
|
|
135
135
|
},
|
|
136
136
|
)
|
|
137
137
|
|
|
138
|
-
return
|
|
138
|
+
return True
|
|
139
139
|
|
|
140
140
|
def matches(self, event):
|
|
141
141
|
"""
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
Checks event.source["content"]["body"] (when a string) against the anchored pattern
|
|
145
|
-
|
|
146
|
-
|
|
142
|
+
Determine whether a Matrix event's message body contains the bridged-packet marker.
|
|
143
|
+
|
|
144
|
+
Checks event.source["content"]["body"] (when it is a string) against the anchored pattern `^Processed (.+) radio packet$`.
|
|
145
|
+
|
|
147
146
|
Parameters:
|
|
148
|
-
event: Matrix event object
|
|
149
|
-
|
|
147
|
+
event: Matrix event object whose `.source` mapping is expected to contain a `"content"` dict with a `"body"` string.
|
|
148
|
+
|
|
150
149
|
Returns:
|
|
151
|
-
|
|
150
|
+
True if the content body matches `^Processed (.+) radio packet$`, False otherwise.
|
|
152
151
|
"""
|
|
153
152
|
# Check for the presence of necessary keys in the event
|
|
154
153
|
content = event.source.get("content", {})
|
|
@@ -181,7 +180,7 @@ class Plugin(BasePlugin):
|
|
|
181
180
|
"""
|
|
182
181
|
# Use the event for matching instead of full_message
|
|
183
182
|
if not self.matches(event):
|
|
184
|
-
return
|
|
183
|
+
return False
|
|
185
184
|
|
|
186
185
|
channel = None
|
|
187
186
|
if config is not None:
|
|
@@ -192,18 +191,18 @@ class Plugin(BasePlugin):
|
|
|
192
191
|
|
|
193
192
|
if channel is None:
|
|
194
193
|
self.logger.debug(f"Skipping message from unmapped channel {channel}")
|
|
195
|
-
return
|
|
194
|
+
return False
|
|
196
195
|
|
|
197
196
|
packet_json = event.source["content"].get("meshtastic_packet")
|
|
198
197
|
if not packet_json:
|
|
199
198
|
self.logger.debug("Missing embedded packet")
|
|
200
|
-
return
|
|
199
|
+
return False
|
|
201
200
|
|
|
202
201
|
try:
|
|
203
202
|
packet = json.loads(packet_json)
|
|
204
|
-
except (json.JSONDecodeError, TypeError)
|
|
205
|
-
self.logger.exception(
|
|
206
|
-
return
|
|
203
|
+
except (json.JSONDecodeError, TypeError):
|
|
204
|
+
self.logger.exception("Error processing embedded packet")
|
|
205
|
+
return False
|
|
207
206
|
|
|
208
207
|
from mmrelay.meshtastic_utils import connect_meshtastic
|
|
209
208
|
|
|
@@ -220,4 +219,4 @@ class Plugin(BasePlugin):
|
|
|
220
219
|
meshtastic_client._sendPacket(
|
|
221
220
|
meshPacket=meshPacket, destinationId=packet["toId"]
|
|
222
221
|
)
|
|
223
|
-
return
|
|
222
|
+
return True
|
mmrelay/setup_utils.py
CHANGED
|
@@ -359,11 +359,11 @@ def is_service_active():
|
|
|
359
359
|
def create_service_file():
|
|
360
360
|
"""
|
|
361
361
|
Create or update the per-user systemd unit file for MMRelay.
|
|
362
|
-
|
|
363
|
-
Ensures the user systemd directory (~/.config/systemd/user) and the MMRelay logs directory (~/.mmrelay/logs) exist, obtains a service unit template
|
|
364
|
-
|
|
362
|
+
|
|
363
|
+
Ensures the user systemd directory (~/.config/systemd/user) and the MMRelay logs directory (~/.mmrelay/logs) exist, obtains a service unit template using the module's template-loading fallbacks, substitutes known placeholders (working directory, packaged launcher, and config path), normalizes the Unit's ExecStart to the resolved MMRelay invocation (an mmrelay executable on PATH or a Python `-m mmrelay` fallback) while preserving any trailing arguments, and writes the resulting unit to ~/.config/systemd/user/mmrelay.service.
|
|
364
|
+
|
|
365
365
|
Returns:
|
|
366
|
-
bool: True if the service file was written successfully; False if
|
|
366
|
+
bool: True if the service file was written successfully; False if a template could not be obtained or writing the file failed.
|
|
367
367
|
"""
|
|
368
368
|
# Get executable paths once to avoid duplicate calls and output
|
|
369
369
|
executable_path = get_executable_path()
|
|
@@ -581,7 +581,7 @@ def check_lingering_enabled():
|
|
|
581
581
|
def enable_lingering():
|
|
582
582
|
"""
|
|
583
583
|
Enable systemd "lingering" for the current user by running `sudo loginctl enable-linger <user>`.
|
|
584
|
-
|
|
584
|
+
|
|
585
585
|
Determines the username from environment variables or getpass.getuser(), invokes the privileged `loginctl` command to enable lingering, and returns True if the command exits successfully. On failure (non-zero exit, missing username, or subprocess/OSError), returns False and prints an error message to stderr.
|
|
586
586
|
"""
|
|
587
587
|
try:
|
|
@@ -7,13 +7,24 @@ services:
|
|
|
7
7
|
user: "${UID:-1000}:${GID:-1000}"
|
|
8
8
|
environment:
|
|
9
9
|
- TZ=UTC # Set timezone (PYTHONUNBUFFERED and MPLCONFIGDIR are set in Dockerfile)
|
|
10
|
+
|
|
10
11
|
volumes:
|
|
11
12
|
# Mount your config directory - create ~/.mmrelay/config.yaml first
|
|
12
13
|
# See docs/DOCKER.md for setup instructions
|
|
13
|
-
# For SELinux systems (
|
|
14
|
-
- ${MMRELAY_HOME:-$HOME}/.mmrelay/config.yaml:/app/config.yaml:ro
|
|
15
|
-
- ${MMRELAY_HOME:-$HOME}/.mmrelay:/app/data
|
|
16
|
-
# For
|
|
17
|
-
# - ${MMRELAY_HOME:-$HOME}/.mmrelay/config.yaml:/app/config.yaml:ro
|
|
18
|
-
# - ${MMRELAY_HOME:-$HOME}/.mmrelay:/app/data
|
|
19
|
-
|
|
14
|
+
# For non-SELinux systems (most common):
|
|
15
|
+
- ${MMRELAY_HOME:-$HOME}/.mmrelay/config.yaml:/app/config.yaml:ro
|
|
16
|
+
- ${MMRELAY_HOME:-$HOME}/.mmrelay:/app/data
|
|
17
|
+
# For SELinux systems (RHEL/CentOS/Fedora), add :Z flag to prevent permission denied errors:
|
|
18
|
+
# - ${MMRELAY_HOME:-$HOME}/.mmrelay/config.yaml:/app/config.yaml:ro,Z
|
|
19
|
+
# - ${MMRELAY_HOME:-$HOME}/.mmrelay:/app/data:Z
|
|
20
|
+
|
|
21
|
+
# For BLE connections, uncomment these lines (Linux only):
|
|
22
|
+
# - /var/run/dbus:/var/run/dbus:ro
|
|
23
|
+
|
|
24
|
+
# For BLE connections, uncomment these options (Linux only). See DOCKER.md for alternatives.
|
|
25
|
+
# network_mode: host
|
|
26
|
+
# security_opt:
|
|
27
|
+
# - apparmor=unconfined # Recommended for BLE to allow DBus communication
|
|
28
|
+
# privileged: true # Alternative if apparmor=unconfined is not acceptable
|
|
29
|
+
|
|
30
|
+
# Tip: For correct permissions and paths, ensure UID, GID, and MMRELAY_HOME are set in a .env file or exported
|
|
@@ -7,13 +7,24 @@ services:
|
|
|
7
7
|
user: "${UID:-1000}:${GID:-1000}"
|
|
8
8
|
environment:
|
|
9
9
|
- TZ=UTC # Set timezone (PYTHONUNBUFFERED and MPLCONFIGDIR are set in Dockerfile)
|
|
10
|
+
|
|
10
11
|
volumes:
|
|
11
12
|
# Mount your config directory - create ~/.mmrelay/config.yaml first
|
|
12
13
|
# Run 'make config' to set up the files
|
|
13
|
-
# For SELinux systems (
|
|
14
|
-
- ${MMRELAY_HOME:-$HOME}/.mmrelay/config.yaml:/app/config.yaml:ro
|
|
15
|
-
- ${MMRELAY_HOME:-$HOME}/.mmrelay:/app/data
|
|
16
|
-
# For
|
|
17
|
-
# - ${MMRELAY_HOME:-$HOME}/.mmrelay/config.yaml:/app/config.yaml:ro
|
|
18
|
-
# - ${MMRELAY_HOME:-$HOME}/.mmrelay:/app/data
|
|
19
|
-
|
|
14
|
+
# For non-SELinux systems (most common):
|
|
15
|
+
- ${MMRELAY_HOME:-$HOME}/.mmrelay/config.yaml:/app/config.yaml:ro
|
|
16
|
+
- ${MMRELAY_HOME:-$HOME}/.mmrelay:/app/data
|
|
17
|
+
# For SELinux systems (RHEL/CentOS/Fedora), add :Z flag to prevent permission denied errors:
|
|
18
|
+
# - ${MMRELAY_HOME:-$HOME}/.mmrelay/config.yaml:/app/config.yaml:ro,Z
|
|
19
|
+
# - ${MMRELAY_HOME:-$HOME}/.mmrelay:/app/data:Z
|
|
20
|
+
|
|
21
|
+
# For BLE connections, uncomment these lines (Linux only):
|
|
22
|
+
# - /var/run/dbus:/var/run/dbus:ro
|
|
23
|
+
|
|
24
|
+
# For BLE connections, uncomment these options (Linux only). See DOCKER.md for alternatives.
|
|
25
|
+
# network_mode: host
|
|
26
|
+
# security_opt:
|
|
27
|
+
# - apparmor=unconfined # Recommended for BLE to allow DBus communication
|
|
28
|
+
# privileged: true # Alternative if apparmor=unconfined is not acceptable
|
|
29
|
+
|
|
30
|
+
# Tip: For correct permissions and paths, ensure UID, GID, and MMRELAY_HOME are set in a .env file or exported
|
mmrelay/windows_utils.py
CHANGED
|
@@ -28,7 +28,7 @@ def is_windows() -> bool:
|
|
|
28
28
|
def setup_windows_console() -> None:
|
|
29
29
|
"""
|
|
30
30
|
Configure the Windows console for UTF-8 output and ANSI (VT100) color support.
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
Best-effort operation: on Windows this attempts to set stdout/stderr encoding to UTF-8
|
|
33
33
|
(if supported) and enable Virtual Terminal Processing so ANSI color sequences and
|
|
34
34
|
Unicode render correctly. No-op on non-Windows platforms; failures are silently ignored.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mmrelay
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.4
|
|
4
4
|
Summary: Bridge between Meshtastic mesh networks and Matrix chat rooms
|
|
5
5
|
Home-page: https://github.com/jeremiah-k/meshtastic-matrix-relay
|
|
6
6
|
Author: Geoff Whittington, Jeremiah K., and contributors
|
|
@@ -18,7 +18,7 @@ Classifier: Topic :: Communications
|
|
|
18
18
|
Requires-Python: >=3.9
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
20
|
License-File: LICENSE
|
|
21
|
-
Requires-Dist: meshtastic>=2.
|
|
21
|
+
Requires-Dist: meshtastic>=2.7.3
|
|
22
22
|
Requires-Dist: Pillow==11.3.0
|
|
23
23
|
Requires-Dist: matrix-nio==0.25.2
|
|
24
24
|
Requires-Dist: matplotlib==3.10.1
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
mmrelay/__init__.py,sha256=
|
|
1
|
+
mmrelay/__init__.py,sha256=DMVdCPD3oN7x0SkEuR6Pq5etPc622-e-kkxsSN2rl2A,120
|
|
2
2
|
mmrelay/__main__.py,sha256=Z839i5jxZmhdoPvR1-reWoQL4Xpmty81p8TsTKsJLC8,814
|
|
3
|
-
mmrelay/cli.py,sha256=
|
|
3
|
+
mmrelay/cli.py,sha256=T4apzc8aS7smXh0bpqoMSDHpHOOxcjls9-vSB-ue5Nk,79543
|
|
4
4
|
mmrelay/cli_utils.py,sha256=h0JKhU_sToO2_Orf9ddO8x07_L1k201V61yXydfU2es,28887
|
|
5
|
-
mmrelay/config.py,sha256=
|
|
5
|
+
mmrelay/config.py,sha256=H93dRHAsZOtPQmeO5RZd-S7qx5zsoAn7bt1GgXdOPqw,38285
|
|
6
6
|
mmrelay/db_utils.py,sha256=utt254MipaIRTaGqSAe7N9L_S7eIPSnQsOQj6Gk2E3U,22502
|
|
7
|
-
mmrelay/e2ee_utils.py,sha256=
|
|
8
|
-
mmrelay/log_utils.py,sha256=
|
|
9
|
-
mmrelay/main.py,sha256=
|
|
10
|
-
mmrelay/matrix_utils.py,sha256=
|
|
11
|
-
mmrelay/meshtastic_utils.py,sha256=
|
|
12
|
-
mmrelay/message_queue.py,sha256=
|
|
7
|
+
mmrelay/e2ee_utils.py,sha256=7cSb3F-FYqFcibSmb7eJzQJoRdq4Xk0_L5oppza3PHE,16914
|
|
8
|
+
mmrelay/log_utils.py,sha256=CGTsHkeviCUKbe_SArYqOjPE5xRTslikMwzrag9LSFE,10290
|
|
9
|
+
mmrelay/main.py,sha256=TlINxwGgk5pVe8gUuxzv29tYlZPuHYdqLcYsmrkUurI,17015
|
|
10
|
+
mmrelay/matrix_utils.py,sha256=QPjwgkEKPgGLNFzeTXgfVusyfran7U_2NDs9UayTLWg,134586
|
|
11
|
+
mmrelay/meshtastic_utils.py,sha256=wbVa8HDjbYNOTDLttVMGeRwTsYt8zJilUv_63FnmHoo,48783
|
|
12
|
+
mmrelay/message_queue.py,sha256=aSitms4pr34VZ7nycpXuPw5ioA6x5xEdnwZ7_nh1Npw,28243
|
|
13
13
|
mmrelay/plugin_loader.py,sha256=iXUpLGyjir7sfxtnL1REHdg5i5ZGWaHO_6sRQqf_JCE,59052
|
|
14
14
|
mmrelay/runtime_utils.py,sha256=u8DtpfI-U7Ip3SAwjn214WfMTzc-xr5wffjN0i7WBZc,1261
|
|
15
|
-
mmrelay/setup_utils.py,sha256=
|
|
16
|
-
mmrelay/windows_utils.py,sha256=
|
|
15
|
+
mmrelay/setup_utils.py,sha256=SCFGx9e1wHZt87u2JMAhPUYOy-Ui4eV3irmzy0Zh9lE,32727
|
|
16
|
+
mmrelay/windows_utils.py,sha256=FqjjsrCWhYxvEH5TcfVBuONCpA19VX9kcx7jHKsRWbM,13060
|
|
17
17
|
mmrelay/constants/__init__.py,sha256=M8AXeIcS1JuS8OwmfTmhcCOkAz5XmWlNQ53GBxYOx94,1494
|
|
18
18
|
mmrelay/constants/app.py,sha256=iNKqQMp5WZSfzsuRXYZ00znVm_ZyfwqiWMQgAblG6rY,725
|
|
19
19
|
mmrelay/constants/config.py,sha256=B1A_OZLLNXHM9rHOwc01ZaJBvFugR2eXlhYYl4nUxlY,2479
|
|
@@ -21,28 +21,28 @@ mmrelay/constants/database.py,sha256=4cHfYfBePDUUtVSflrWyStcxKSQv7VE-jSrb1IzAjls
|
|
|
21
21
|
mmrelay/constants/formats.py,sha256=cjbrfNNFCKoGSFsFHR1QQDEQudiGquA9MUapfm0_ZNI,494
|
|
22
22
|
mmrelay/constants/messages.py,sha256=Reu_-6gZGGZQVP6BuqBm01QBhVTFjHVRQSPTUcQJG2Q,1531
|
|
23
23
|
mmrelay/constants/network.py,sha256=QjROOAMxcP1pA1F_gUtuXzm2tORCqo5koqZbwrZRSns,1186
|
|
24
|
-
mmrelay/constants/queue.py,sha256=
|
|
24
|
+
mmrelay/constants/queue.py,sha256=cNK0cLsgBfQ6nVJm9_Scxj-ICv4RU6DU_PYsUwNyU_E,675
|
|
25
25
|
mmrelay/plugins/__init__.py,sha256=KVMQIXRhe0wlGj4O3IZ0vOIQRKFkfPYejHXhJL17qrc,51
|
|
26
|
-
mmrelay/plugins/base_plugin.py,sha256=
|
|
26
|
+
mmrelay/plugins/base_plugin.py,sha256=lcgKwrAMAIT9RjiC1Nyfs3fgdzUu3OQhCTZEjqh1_wg,20915
|
|
27
27
|
mmrelay/plugins/debug_plugin.py,sha256=adX0cRJHUEDLldajybPfiRDDlvytkZe5aN_dSgNKP2Y,870
|
|
28
28
|
mmrelay/plugins/drop_plugin.py,sha256=x4S-e0Muun2Dy1H2qwRMTBB1ptLmy7ZZJhgPu-KefGs,5394
|
|
29
29
|
mmrelay/plugins/health_plugin.py,sha256=svV_GfpAVL0QhiVzi3PVZ1mNpsOL1NHSmkRF-Mn_ExE,2250
|
|
30
30
|
mmrelay/plugins/help_plugin.py,sha256=S7nBhsANK46Zv9wPHOVegPGcuYGMErBsxAnrRlSSCwg,2149
|
|
31
31
|
mmrelay/plugins/map_plugin.py,sha256=eHV_t3TFcypBD4xT_OQx0hD6_iGkLJOADjwYVny0PvE,11292
|
|
32
|
-
mmrelay/plugins/mesh_relay_plugin.py,sha256=
|
|
32
|
+
mmrelay/plugins/mesh_relay_plugin.py,sha256=OE07Kz5NvmyjXH3hneK5_eTPQ41mriJGZgsPcg-gpOQ,8428
|
|
33
33
|
mmrelay/plugins/nodes_plugin.py,sha256=RDabzyG5hKG5aYWecsRUcLSjMCCv6Pngmq2Qpld1A1U,2903
|
|
34
34
|
mmrelay/plugins/ping_plugin.py,sha256=8uFnT3qfO3RBaTUOx348voIfKpzXB3zTfcT6Gtfc8kM,4070
|
|
35
35
|
mmrelay/plugins/telemetry_plugin.py,sha256=8SxWv4BLXMUTbiVaD3MjlMMdQyS7S_1OfLlVNAUMSO0,6306
|
|
36
36
|
mmrelay/plugins/weather_plugin.py,sha256=ZmTNlkEjeCvZWHlO7VMend3vnj4h2qWeeU4RUQJY2vM,14183
|
|
37
37
|
mmrelay/tools/__init__.py,sha256=WFjDQjdevgg19_zT6iEoL29rvb1JPqYSd8708Jn5D7A,838
|
|
38
38
|
mmrelay/tools/mmrelay.service,sha256=6_TAskmTh9pXQSDRDeh0EXl2BfsUgm9Q3W9ob5tWCS8,600
|
|
39
|
-
mmrelay/tools/sample-docker-compose-prebuilt.yaml,sha256=
|
|
40
|
-
mmrelay/tools/sample-docker-compose.yaml,sha256=
|
|
39
|
+
mmrelay/tools/sample-docker-compose-prebuilt.yaml,sha256=ytQRW882VAecbWO1LsRsU6UbudybNhC-iv2w0Fta1Ls,1368
|
|
40
|
+
mmrelay/tools/sample-docker-compose.yaml,sha256=1UqZFvnJuhVwfNJ92C0qQ5KSdNRVjk8YbRNZgD8yF_M,1350
|
|
41
41
|
mmrelay/tools/sample.env,sha256=RP-o3rX3jnEIrVG2rqCZq31O1yRXou4HcGrXWLVbKKw,311
|
|
42
42
|
mmrelay/tools/sample_config.yaml,sha256=odAeiU-wesWuwnZGyXzVjxaXVYc26p8QI1HOEA-Tvco,6396
|
|
43
|
-
mmrelay-1.2.
|
|
44
|
-
mmrelay-1.2.
|
|
45
|
-
mmrelay-1.2.
|
|
46
|
-
mmrelay-1.2.
|
|
47
|
-
mmrelay-1.2.
|
|
48
|
-
mmrelay-1.2.
|
|
43
|
+
mmrelay-1.2.4.dist-info/licenses/LICENSE,sha256=aB_07MhnK-bL5WLI1ucXLUSdW_yBVoepPRYB0kaAOl8,35204
|
|
44
|
+
mmrelay-1.2.4.dist-info/METADATA,sha256=SOYGlz1SUe7pRR-VESGmmpy_jO49fwctDcQFvvkjBHo,6167
|
|
45
|
+
mmrelay-1.2.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
46
|
+
mmrelay-1.2.4.dist-info/entry_points.txt,sha256=SJZwGUOEpQ-qx4H8UL4xKFnKeInGUaZNW1I0ddjK7Ws,45
|
|
47
|
+
mmrelay-1.2.4.dist-info/top_level.txt,sha256=B_ZLCRm7NYAmI3PipRUyHGymP-C-q16LSeMGzmqJfo4,8
|
|
48
|
+
mmrelay-1.2.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|