mmrelay 1.1.3__py3-none-any.whl → 1.2.0__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 +1097 -110
- mmrelay/cli_utils.py +696 -0
- mmrelay/config.py +632 -44
- mmrelay/constants/__init__.py +54 -0
- mmrelay/constants/app.py +29 -0
- mmrelay/constants/config.py +78 -0
- mmrelay/constants/database.py +22 -0
- mmrelay/constants/formats.py +20 -0
- mmrelay/constants/messages.py +45 -0
- mmrelay/constants/network.py +42 -0
- mmrelay/constants/queue.py +17 -0
- mmrelay/db_utils.py +281 -132
- mmrelay/e2ee_utils.py +392 -0
- mmrelay/log_utils.py +77 -19
- mmrelay/main.py +101 -30
- mmrelay/matrix_utils.py +1083 -118
- mmrelay/meshtastic_utils.py +374 -118
- mmrelay/message_queue.py +17 -17
- mmrelay/plugin_loader.py +126 -91
- mmrelay/plugins/base_plugin.py +74 -15
- mmrelay/plugins/drop_plugin.py +13 -5
- mmrelay/plugins/mesh_relay_plugin.py +7 -10
- mmrelay/plugins/weather_plugin.py +118 -12
- mmrelay/setup_utils.py +67 -30
- mmrelay/tools/sample-docker-compose-prebuilt.yaml +80 -0
- mmrelay/tools/sample-docker-compose.yaml +34 -8
- mmrelay/tools/sample_config.yaml +29 -4
- {mmrelay-1.1.3.dist-info → mmrelay-1.2.0.dist-info}/METADATA +21 -50
- mmrelay-1.2.0.dist-info/RECORD +45 -0
- mmrelay/config_checker.py +0 -133
- mmrelay-1.1.3.dist-info/RECORD +0 -35
- {mmrelay-1.1.3.dist-info → mmrelay-1.2.0.dist-info}/WHEEL +0 -0
- {mmrelay-1.1.3.dist-info → mmrelay-1.2.0.dist-info}/entry_points.txt +0 -0
- {mmrelay-1.1.3.dist-info → mmrelay-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {mmrelay-1.1.3.dist-info → mmrelay-1.2.0.dist-info}/top_level.txt +0 -0
mmrelay/main.py
CHANGED
|
@@ -5,16 +5,23 @@ It uses Meshtastic-python and Matrix nio client library to interface with the ra
|
|
|
5
5
|
|
|
6
6
|
import asyncio
|
|
7
7
|
import concurrent.futures
|
|
8
|
-
import logging
|
|
9
8
|
import signal
|
|
10
9
|
import sys
|
|
11
10
|
|
|
12
|
-
from nio import
|
|
11
|
+
from nio import (
|
|
12
|
+
MegolmEvent,
|
|
13
|
+
ReactionEvent,
|
|
14
|
+
RoomMessageEmote,
|
|
15
|
+
RoomMessageNotice,
|
|
16
|
+
RoomMessageText,
|
|
17
|
+
)
|
|
13
18
|
from nio.events.room_events import RoomMemberEvent
|
|
14
19
|
|
|
15
20
|
# Import version from package
|
|
16
21
|
# Import meshtastic_utils as a module to set event_loop
|
|
17
22
|
from mmrelay import __version__, meshtastic_utils
|
|
23
|
+
from mmrelay.cli_utils import msg_suggest_check_config, msg_suggest_generate_config
|
|
24
|
+
from mmrelay.constants.app import APP_DISPLAY_NAME, WINDOWS_PLATFORM
|
|
18
25
|
from mmrelay.db_utils import (
|
|
19
26
|
initialize_database,
|
|
20
27
|
update_longnames,
|
|
@@ -22,9 +29,16 @@ from mmrelay.db_utils import (
|
|
|
22
29
|
wipe_message_map,
|
|
23
30
|
)
|
|
24
31
|
from mmrelay.log_utils import get_logger
|
|
25
|
-
from mmrelay.matrix_utils import
|
|
32
|
+
from mmrelay.matrix_utils import (
|
|
33
|
+
connect_matrix,
|
|
34
|
+
join_matrix_room,
|
|
35
|
+
)
|
|
26
36
|
from mmrelay.matrix_utils import logger as matrix_logger
|
|
27
|
-
from mmrelay.matrix_utils import
|
|
37
|
+
from mmrelay.matrix_utils import (
|
|
38
|
+
on_decryption_failure,
|
|
39
|
+
on_room_member,
|
|
40
|
+
on_room_message,
|
|
41
|
+
)
|
|
28
42
|
from mmrelay.meshtastic_utils import connect_meshtastic
|
|
29
43
|
from mmrelay.meshtastic_utils import logger as meshtastic_logger
|
|
30
44
|
from mmrelay.message_queue import (
|
|
@@ -36,10 +50,7 @@ from mmrelay.message_queue import (
|
|
|
36
50
|
from mmrelay.plugin_loader import load_plugins
|
|
37
51
|
|
|
38
52
|
# Initialize logger
|
|
39
|
-
logger = get_logger(name=
|
|
40
|
-
|
|
41
|
-
# Set the logging level for 'nio' to ERROR to suppress warnings
|
|
42
|
-
logging.getLogger("nio").setLevel(logging.ERROR)
|
|
53
|
+
logger = get_logger(name=APP_DISPLAY_NAME)
|
|
43
54
|
|
|
44
55
|
|
|
45
56
|
# Flag to track if banner has been printed
|
|
@@ -47,19 +58,24 @@ _banner_printed = False
|
|
|
47
58
|
|
|
48
59
|
|
|
49
60
|
def print_banner():
|
|
50
|
-
"""
|
|
61
|
+
"""
|
|
62
|
+
Log the MMRelay startup banner with version information once.
|
|
63
|
+
|
|
64
|
+
This records an informational message "Starting MMRelay version <version>" via the module logger
|
|
65
|
+
the first time it is called and sets a module-level flag to prevent subsequent prints.
|
|
66
|
+
"""
|
|
51
67
|
global _banner_printed
|
|
52
68
|
# Only print the banner once
|
|
53
69
|
if not _banner_printed:
|
|
54
|
-
logger.info(f"Starting MMRelay
|
|
70
|
+
logger.info(f"Starting MMRelay version {__version__}")
|
|
55
71
|
_banner_printed = True
|
|
56
72
|
|
|
57
73
|
|
|
58
74
|
async def main(config):
|
|
59
75
|
"""
|
|
60
|
-
|
|
76
|
+
Coordinates the main asynchronous relay loop between Meshtastic and Matrix clients.
|
|
61
77
|
|
|
62
|
-
Initializes the database, loads plugins, starts the message queue, and establishes connections to both Meshtastic and Matrix. Joins configured Matrix rooms, registers event callbacks for message and membership events, and periodically updates node names from the Meshtastic network. Monitors connection health, manages the Matrix sync loop with reconnection and shutdown handling, and ensures graceful shutdown of all components
|
|
78
|
+
Initializes the database, loads plugins, starts the message queue, and establishes connections to both Meshtastic and Matrix. Joins configured Matrix rooms, registers event callbacks for message and membership events, and periodically updates node names from the Meshtastic network. Monitors connection health, manages the Matrix sync loop with reconnection and shutdown handling, and ensures graceful shutdown of all components. Optionally wipes the message map on startup and shutdown if configured.
|
|
63
79
|
"""
|
|
64
80
|
# Extract Matrix configuration
|
|
65
81
|
from typing import List
|
|
@@ -108,6 +124,13 @@ async def main(config):
|
|
|
108
124
|
# Connect to Matrix
|
|
109
125
|
matrix_client = await connect_matrix(passed_config=config)
|
|
110
126
|
|
|
127
|
+
# Check if Matrix connection was successful
|
|
128
|
+
if matrix_client is None:
|
|
129
|
+
# The error is logged by connect_matrix, so we can just raise here.
|
|
130
|
+
raise ConnectionError(
|
|
131
|
+
"Failed to connect to Matrix. Cannot continue without Matrix client."
|
|
132
|
+
)
|
|
133
|
+
|
|
111
134
|
# Join the rooms specified in the config.yaml
|
|
112
135
|
for room in matrix_rooms:
|
|
113
136
|
await join_matrix_room(matrix_client, room["id"])
|
|
@@ -115,12 +138,14 @@ async def main(config):
|
|
|
115
138
|
# Register the message callback for Matrix
|
|
116
139
|
matrix_logger.info("Listening for inbound Matrix messages...")
|
|
117
140
|
matrix_client.add_event_callback(
|
|
118
|
-
on_room_message,
|
|
141
|
+
on_room_message,
|
|
142
|
+
(RoomMessageText, RoomMessageNotice, RoomMessageEmote, ReactionEvent),
|
|
119
143
|
)
|
|
120
|
-
# Add
|
|
121
|
-
|
|
144
|
+
# Add E2EE callbacks - MegolmEvent only goes to decryption failure handler
|
|
145
|
+
# Successfully decrypted messages will be converted to RoomMessageText etc. by matrix-nio
|
|
146
|
+
matrix_client.add_event_callback(on_decryption_failure, (MegolmEvent,))
|
|
122
147
|
# Add RoomMemberEvent callback to track room-specific display name changes
|
|
123
|
-
matrix_client.add_event_callback(on_room_member, RoomMemberEvent)
|
|
148
|
+
matrix_client.add_event_callback(on_room_member, (RoomMemberEvent,))
|
|
124
149
|
|
|
125
150
|
# Set up shutdown event
|
|
126
151
|
shutdown_event = asyncio.Event()
|
|
@@ -133,7 +158,7 @@ async def main(config):
|
|
|
133
158
|
loop = asyncio.get_running_loop()
|
|
134
159
|
|
|
135
160
|
# Handle signals differently based on the platform
|
|
136
|
-
if sys.platform !=
|
|
161
|
+
if sys.platform != WINDOWS_PLATFORM:
|
|
137
162
|
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
138
163
|
loop.add_signal_handler(sig, lambda: asyncio.create_task(shutdown()))
|
|
139
164
|
else:
|
|
@@ -170,15 +195,33 @@ async def main(config):
|
|
|
170
195
|
[sync_task, shutdown_task],
|
|
171
196
|
return_when=asyncio.FIRST_COMPLETED,
|
|
172
197
|
)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
198
|
+
|
|
199
|
+
# Cancel any pending tasks
|
|
200
|
+
for task in pending:
|
|
201
|
+
task.cancel()
|
|
176
202
|
try:
|
|
177
|
-
await
|
|
203
|
+
await task
|
|
178
204
|
except asyncio.CancelledError:
|
|
179
205
|
pass
|
|
206
|
+
|
|
207
|
+
if shutdown_event.is_set():
|
|
208
|
+
matrix_logger.info("Shutdown event detected. Stopping sync loop...")
|
|
180
209
|
break
|
|
181
210
|
|
|
211
|
+
# Check if sync_task completed with an exception
|
|
212
|
+
if sync_task in done:
|
|
213
|
+
try:
|
|
214
|
+
# This will raise the exception if the task failed
|
|
215
|
+
sync_task.result()
|
|
216
|
+
# If we get here, sync completed normally (shouldn't happen with sync_forever)
|
|
217
|
+
matrix_logger.warning(
|
|
218
|
+
"Matrix sync_forever completed unexpectedly"
|
|
219
|
+
)
|
|
220
|
+
except Exception as e:
|
|
221
|
+
# Log the exception and continue to retry
|
|
222
|
+
matrix_logger.error(f"Matrix sync failed: {e}")
|
|
223
|
+
# The outer try/catch will handle the retry logic
|
|
224
|
+
|
|
182
225
|
except Exception as e:
|
|
183
226
|
if shutdown_event.is_set():
|
|
184
227
|
break
|
|
@@ -245,13 +288,16 @@ async def main(config):
|
|
|
245
288
|
|
|
246
289
|
|
|
247
290
|
def run_main(args):
|
|
248
|
-
"""
|
|
291
|
+
"""
|
|
292
|
+
Run the application's top-level startup sequence and invoke the main async runner.
|
|
293
|
+
|
|
294
|
+
Performs initial setup (prints banner, optionally sets a custom data directory, loads and applies configuration and logging overrides), validates that required configuration sections are present (required keys differ if credentials.json is present), then runs the main coroutine. Returns an exit code: 0 for successful run or user interrupt, 1 for configuration errors or unhandled exceptions.
|
|
249
295
|
|
|
250
|
-
|
|
251
|
-
args:
|
|
296
|
+
Parameters:
|
|
297
|
+
args: Parsed command-line arguments (may be None). Recognized options used here include `data_dir` and `log_level`.
|
|
252
298
|
|
|
253
299
|
Returns:
|
|
254
|
-
int: Exit code (0
|
|
300
|
+
int: Exit code (0 on success or user-initiated interrupt, 1 on failure such as invalid config or runtime error).
|
|
255
301
|
"""
|
|
256
302
|
# Print the banner at startup
|
|
257
303
|
print_banner()
|
|
@@ -316,7 +362,17 @@ def run_main(args):
|
|
|
316
362
|
config_rich_logger.info(f"Log file location: {log_file_path}")
|
|
317
363
|
|
|
318
364
|
# Check if config exists and has the required keys
|
|
319
|
-
|
|
365
|
+
# Note: matrix section is optional if credentials.json exists
|
|
366
|
+
from mmrelay.config import load_credentials
|
|
367
|
+
|
|
368
|
+
credentials = load_credentials()
|
|
369
|
+
|
|
370
|
+
if credentials:
|
|
371
|
+
# With credentials.json, only meshtastic and matrix_rooms are required
|
|
372
|
+
required_keys = ["meshtastic", "matrix_rooms"]
|
|
373
|
+
else:
|
|
374
|
+
# Without credentials.json, all sections are required
|
|
375
|
+
required_keys = ["matrix", "meshtastic", "matrix_rooms"]
|
|
320
376
|
|
|
321
377
|
# Check each key individually for better debugging
|
|
322
378
|
for key in required_keys:
|
|
@@ -326,10 +382,21 @@ def run_main(args):
|
|
|
326
382
|
if not config or not all(key in config for key in required_keys):
|
|
327
383
|
# Exit with error if no config exists
|
|
328
384
|
missing_keys = [key for key in required_keys if key not in config]
|
|
329
|
-
|
|
330
|
-
f"Configuration is missing required keys: {missing_keys}
|
|
331
|
-
"
|
|
332
|
-
|
|
385
|
+
if credentials:
|
|
386
|
+
logger.error(f"Configuration is missing required keys: {missing_keys}")
|
|
387
|
+
logger.error("Matrix authentication will use credentials.json")
|
|
388
|
+
logger.error("Next steps:")
|
|
389
|
+
logger.error(
|
|
390
|
+
f" • Create a valid config.yaml file or {msg_suggest_generate_config()}"
|
|
391
|
+
)
|
|
392
|
+
logger.error(f" • {msg_suggest_check_config()}")
|
|
393
|
+
else:
|
|
394
|
+
logger.error(f"Configuration is missing required keys: {missing_keys}")
|
|
395
|
+
logger.error("Next steps:")
|
|
396
|
+
logger.error(
|
|
397
|
+
f" • Create a valid config.yaml file or {msg_suggest_generate_config()}"
|
|
398
|
+
)
|
|
399
|
+
logger.error(f" • {msg_suggest_check_config()}")
|
|
333
400
|
return 1
|
|
334
401
|
|
|
335
402
|
try:
|
|
@@ -339,7 +406,11 @@ def run_main(args):
|
|
|
339
406
|
logger.info("Interrupted by user. Exiting.")
|
|
340
407
|
return 0
|
|
341
408
|
except Exception as e:
|
|
409
|
+
import traceback
|
|
410
|
+
|
|
342
411
|
logger.error(f"Error running main functionality: {e}")
|
|
412
|
+
logger.error("Full traceback:")
|
|
413
|
+
logger.error(traceback.format_exc())
|
|
343
414
|
return 1
|
|
344
415
|
|
|
345
416
|
|