mmrelay 1.2.3__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 +19 -23
- mmrelay/config.py +5 -7
- mmrelay/constants/queue.py +4 -1
- mmrelay/log_utils.py +29 -8
- mmrelay/main.py +13 -4
- mmrelay/matrix_utils.py +41 -41
- mmrelay/message_queue.py +12 -11
- mmrelay/plugins/base_plugin.py +7 -7
- mmrelay/plugins/mesh_relay_plugin.py +16 -18
- mmrelay/setup_utils.py +2 -2
- mmrelay/tools/sample-docker-compose-prebuilt.yaml +18 -7
- mmrelay/tools/sample-docker-compose.yaml +18 -7
- {mmrelay-1.2.3.dist-info → mmrelay-1.2.4.dist-info}/METADATA +2 -2
- {mmrelay-1.2.3.dist-info → mmrelay-1.2.4.dist-info}/RECORD +19 -19
- {mmrelay-1.2.3.dist-info → mmrelay-1.2.4.dist-info}/WHEEL +0 -0
- {mmrelay-1.2.3.dist-info → mmrelay-1.2.4.dist-info}/entry_points.txt +0 -0
- {mmrelay-1.2.3.dist-info → mmrelay-1.2.4.dist-info}/licenses/LICENSE +0 -0
- {mmrelay-1.2.3.dist-info → mmrelay-1.2.4.dist-info}/top_level.txt +0 -0
mmrelay/__init__.py
CHANGED
mmrelay/cli.py
CHANGED
|
@@ -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): CLI arguments used to determine the config
|
|
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
|
|
@@ -1586,19 +1586,15 @@ def _diagnose_sample_config_accessibility():
|
|
|
1586
1586
|
|
|
1587
1587
|
def _diagnose_platform_specific(args):
|
|
1588
1588
|
"""
|
|
1589
|
-
Run platform-specific diagnostic checks and report
|
|
1590
|
-
|
|
1591
|
-
On Windows,
|
|
1592
|
-
|
|
1593
|
-
On non-Windows platforms this reports that platform-specific tests are not
|
|
1594
|
-
required.
|
|
1595
|
-
|
|
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
|
+
|
|
1596
1593
|
Parameters:
|
|
1597
|
-
args (argparse.Namespace): CLI arguments
|
|
1598
|
-
|
|
1599
|
-
|
|
1594
|
+
args (argparse.Namespace): CLI arguments forwarded to the Windows configuration-generation test (used only when running on Windows).
|
|
1595
|
+
|
|
1600
1596
|
Returns:
|
|
1601
|
-
bool: True if Windows checks were executed (running on Windows), False otherwise.
|
|
1597
|
+
bool: `True` if Windows checks were executed (running on Windows), `False` otherwise.
|
|
1602
1598
|
"""
|
|
1603
1599
|
print("3. Platform-specific diagnostics...")
|
|
1604
1600
|
import sys
|
|
@@ -1721,15 +1717,15 @@ def _diagnose_minimal_config_template():
|
|
|
1721
1717
|
|
|
1722
1718
|
def handle_config_diagnose(args):
|
|
1723
1719
|
"""
|
|
1724
|
-
Run non-destructive diagnostics for the MMRelay configuration subsystem and print a human-readable report.
|
|
1725
|
-
|
|
1726
|
-
Performs four checks without modifying user files: (1) resolves and reports candidate configuration file paths and their directory accessibility, (2) verifies availability of the packaged sample configuration, (3)
|
|
1727
|
-
|
|
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
|
+
|
|
1728
1724
|
Parameters:
|
|
1729
|
-
args (argparse.Namespace): Parsed CLI arguments used to
|
|
1730
|
-
|
|
1725
|
+
args (argparse.Namespace): Parsed CLI arguments used to determine configuration search paths and to control platform-specific diagnostic behavior.
|
|
1726
|
+
|
|
1731
1727
|
Returns:
|
|
1732
|
-
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).
|
|
1733
1729
|
"""
|
|
1734
1730
|
print("MMRelay Configuration System Diagnostics")
|
|
1735
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
|
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/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
|
|
@@ -155,8 +164,8 @@ async def main(config):
|
|
|
155
164
|
async def shutdown():
|
|
156
165
|
"""
|
|
157
166
|
Signal the application to begin shutdown.
|
|
158
|
-
|
|
159
|
-
|
|
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,
|
|
@@ -789,16 +788,16 @@ def bot_command(command, event):
|
|
|
789
788
|
|
|
790
789
|
async def connect_matrix(passed_config=None):
|
|
791
790
|
"""
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
791
|
+
Initialize and return a configured matrix-nio AsyncClient connected to the configured Matrix homeserver.
|
|
792
|
+
|
|
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.
|
|
794
|
+
|
|
796
795
|
Parameters:
|
|
797
|
-
passed_config (dict | None): Optional configuration to use for this connection attempt;
|
|
798
|
-
|
|
796
|
+
passed_config (dict | None): Optional configuration to use for this connection attempt; when provided it overrides the module-level config for this call.
|
|
797
|
+
|
|
799
798
|
Returns:
|
|
800
|
-
AsyncClient | None:
|
|
801
|
-
|
|
799
|
+
AsyncClient | None: A ready-to-use matrix-nio AsyncClient on success, or `None` if connection or credentials are unavailable.
|
|
800
|
+
|
|
802
801
|
Raises:
|
|
803
802
|
ValueError: If the required top-level "matrix_rooms" configuration is missing.
|
|
804
803
|
ConnectionError: If the initial Matrix sync fails or times out.
|
|
@@ -2318,17 +2317,20 @@ async def send_reply_to_meshtastic(
|
|
|
2318
2317
|
reply_id=None,
|
|
2319
2318
|
):
|
|
2320
2319
|
"""
|
|
2321
|
-
Enqueue a Matrix reply to be
|
|
2322
|
-
|
|
2323
|
-
If
|
|
2324
|
-
|
|
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
|
+
|
|
2325
2324
|
Parameters:
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
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.
|
|
2329
2331
|
storage_enabled (bool): If True, create and attach a message-mapping record to the queued Meshtastic message.
|
|
2330
|
-
|
|
2331
|
-
|
|
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 Matrix event identified by reply_to_event_id has an associated Meshtastic mapping,
|
|
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:
|
|
2443
|
+
room: Matrix room object where the reply originated.
|
|
2444
|
+
event: Matrix event object representing the reply.
|
|
2441
2445
|
reply_to_event_id (str): Matrix event ID being replied to; used to locate the Meshtastic mapping.
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
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.
|
|
2446
2452
|
longname (str | None): Sender long display name used for prefixing.
|
|
2447
2453
|
shortname (str | None): Sender short display name used for prefixing.
|
|
2448
2454
|
meshnet_name (str | None): Remote meshnet name associated with the original mapping, if any.
|
|
2449
|
-
|
|
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
|
|
2554
|
+
Handle an incoming Matrix room event and relay it to Meshtastic when applicable.
|
|
2549
2555
|
|
|
2550
|
-
Processes text, notice, emote, and reaction events (including replies and
|
|
2551
|
-
- Ignores events from before the bot started and events sent by the bot itself.
|
|
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.
|
|
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
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/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,17 +518,17 @@ class MessageQueue:
|
|
|
517
518
|
|
|
518
519
|
def _handle_message_mapping(self, result, mapping_info):
|
|
519
520
|
"""
|
|
520
|
-
Persist a sent
|
|
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: An object returned by the send function with an `id` attribute
|
|
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
528
|
- matrix_event_id (str): Matrix event ID to map to.
|
|
528
529
|
- room_id (str): Matrix room ID where the event was sent.
|
|
529
530
|
- text (str): Message text to associate with the mapping.
|
|
530
|
-
- meshnet (optional): Mesh network identifier
|
|
531
|
+
- meshnet (optional): Mesh network identifier to pass to storage.
|
|
531
532
|
- msgs_to_keep (optional, int): Number of mappings to retain when pruning.
|
|
532
533
|
"""
|
|
533
534
|
try:
|
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,21 +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 it is a string) against the anchored
|
|
145
|
-
|
|
146
|
-
pattern, otherwise False.
|
|
147
|
-
|
|
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
|
+
|
|
148
146
|
Parameters:
|
|
149
|
-
event: Matrix event object whose
|
|
150
|
-
|
|
147
|
+
event: Matrix event object whose `.source` mapping is expected to contain a `"content"` dict with a `"body"` string.
|
|
148
|
+
|
|
151
149
|
Returns:
|
|
152
|
-
|
|
150
|
+
True if the content body matches `^Processed (.+) radio packet$`, False otherwise.
|
|
153
151
|
"""
|
|
154
152
|
# Check for the presence of necessary keys in the event
|
|
155
153
|
content = event.source.get("content", {})
|
|
@@ -182,7 +180,7 @@ class Plugin(BasePlugin):
|
|
|
182
180
|
"""
|
|
183
181
|
# Use the event for matching instead of full_message
|
|
184
182
|
if not self.matches(event):
|
|
185
|
-
return
|
|
183
|
+
return False
|
|
186
184
|
|
|
187
185
|
channel = None
|
|
188
186
|
if config is not None:
|
|
@@ -193,18 +191,18 @@ class Plugin(BasePlugin):
|
|
|
193
191
|
|
|
194
192
|
if channel is None:
|
|
195
193
|
self.logger.debug(f"Skipping message from unmapped channel {channel}")
|
|
196
|
-
return
|
|
194
|
+
return False
|
|
197
195
|
|
|
198
196
|
packet_json = event.source["content"].get("meshtastic_packet")
|
|
199
197
|
if not packet_json:
|
|
200
198
|
self.logger.debug("Missing embedded packet")
|
|
201
|
-
return
|
|
199
|
+
return False
|
|
202
200
|
|
|
203
201
|
try:
|
|
204
202
|
packet = json.loads(packet_json)
|
|
205
|
-
except (json.JSONDecodeError, TypeError)
|
|
206
|
-
self.logger.exception(
|
|
207
|
-
return
|
|
203
|
+
except (json.JSONDecodeError, TypeError):
|
|
204
|
+
self.logger.exception("Error processing embedded packet")
|
|
205
|
+
return False
|
|
208
206
|
|
|
209
207
|
from mmrelay.meshtastic_utils import connect_meshtastic
|
|
210
208
|
|
|
@@ -221,4 +219,4 @@ class Plugin(BasePlugin):
|
|
|
221
219
|
meshtastic_client._sendPacket(
|
|
222
220
|
meshPacket=meshPacket, destinationId=packet["toId"]
|
|
223
221
|
)
|
|
224
|
-
return
|
|
222
|
+
return True
|
mmrelay/setup_utils.py
CHANGED
|
@@ -359,9 +359,9 @@ 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
|
-
|
|
362
|
+
|
|
363
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
|
-
|
|
364
|
+
|
|
365
365
|
Returns:
|
|
366
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
|
"""
|
|
@@ -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
|
|
@@ -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,18 +1,18 @@
|
|
|
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
7
|
mmrelay/e2ee_utils.py,sha256=7cSb3F-FYqFcibSmb7eJzQJoRdq4Xk0_L5oppza3PHE,16914
|
|
8
|
-
mmrelay/log_utils.py,sha256=
|
|
9
|
-
mmrelay/main.py,sha256=
|
|
10
|
-
mmrelay/matrix_utils.py,sha256=
|
|
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
11
|
mmrelay/meshtastic_utils.py,sha256=wbVa8HDjbYNOTDLttVMGeRwTsYt8zJilUv_63FnmHoo,48783
|
|
12
|
-
mmrelay/message_queue.py,sha256=
|
|
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=
|
|
15
|
+
mmrelay/setup_utils.py,sha256=SCFGx9e1wHZt87u2JMAhPUYOy-Ui4eV3irmzy0Zh9lE,32727
|
|
16
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
|
|
@@ -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
|