mmrelay 1.2.0__py3-none-any.whl → 1.2.2__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/__main__.py +29 -0
- mmrelay/cli.py +735 -135
- mmrelay/cli_utils.py +59 -9
- mmrelay/config.py +198 -71
- mmrelay/constants/app.py +2 -2
- mmrelay/db_utils.py +73 -26
- mmrelay/e2ee_utils.py +6 -3
- mmrelay/log_utils.py +16 -5
- mmrelay/main.py +41 -38
- mmrelay/matrix_utils.py +1069 -293
- mmrelay/meshtastic_utils.py +350 -206
- mmrelay/message_queue.py +212 -62
- mmrelay/plugin_loader.py +634 -205
- mmrelay/plugins/mesh_relay_plugin.py +43 -38
- mmrelay/plugins/weather_plugin.py +11 -12
- mmrelay/runtime_utils.py +35 -0
- mmrelay/setup_utils.py +324 -129
- mmrelay/tools/mmrelay.service +2 -1
- mmrelay/tools/sample-docker-compose-prebuilt.yaml +11 -72
- mmrelay/tools/sample-docker-compose.yaml +12 -58
- mmrelay/tools/sample_config.yaml +14 -13
- mmrelay/windows_utils.py +349 -0
- {mmrelay-1.2.0.dist-info → mmrelay-1.2.2.dist-info}/METADATA +11 -11
- mmrelay-1.2.2.dist-info/RECORD +48 -0
- mmrelay-1.2.0.dist-info/RECORD +0 -45
- {mmrelay-1.2.0.dist-info → mmrelay-1.2.2.dist-info}/WHEEL +0 -0
- {mmrelay-1.2.0.dist-info → mmrelay-1.2.2.dist-info}/entry_points.txt +0 -0
- {mmrelay-1.2.0.dist-info → mmrelay-1.2.2.dist-info}/licenses/LICENSE +0 -0
- {mmrelay-1.2.0.dist-info → mmrelay-1.2.2.dist-info}/top_level.txt +0 -0
mmrelay/cli_utils.py
CHANGED
|
@@ -550,19 +550,21 @@ def _handle_matrix_error(exception: Exception, context: str, log_level: str = "e
|
|
|
550
550
|
|
|
551
551
|
async def logout_matrix_bot(password: str):
|
|
552
552
|
"""
|
|
553
|
-
Log out the configured Matrix account and remove local session data.
|
|
553
|
+
Log out the configured Matrix account (if any), verify credentials, and remove local session data.
|
|
554
554
|
|
|
555
|
-
|
|
556
|
-
log out the active session on the homeserver (invalidating the access token) and remove
|
|
557
|
-
local session artifacts (credentials.json and any E2EE store). If there is no active
|
|
558
|
-
session, the function reports that and returns True.
|
|
555
|
+
Performs an optional verification of the supplied Matrix password by performing a temporary login, attempts to log out the active server session (invalidating the access token), and removes local session artifacts (e.g., credentials.json and any E2EE store directories). If the stored credentials lack a user_id but include an access_token and homeserver, the function will try to fetch and persist the missing user_id before proceeding.
|
|
559
556
|
|
|
560
557
|
Parameters:
|
|
561
|
-
password (str): The Matrix account password used to verify the session before logout.
|
|
558
|
+
password (str): The Matrix account password used to verify the session before performing server logout.
|
|
562
559
|
|
|
563
560
|
Returns:
|
|
564
|
-
bool: True when local cleanup (and server logout, if
|
|
565
|
-
|
|
561
|
+
bool: True when local cleanup (and server logout, if attempted) completed successfully; False on failure.
|
|
562
|
+
If the matrix-nio dependency is not available the function prints an error and returns False.
|
|
563
|
+
|
|
564
|
+
Side effects:
|
|
565
|
+
- May update credentials.json if the user_id is fetched.
|
|
566
|
+
- Removes local session files and E2EE store directories when cleanup runs.
|
|
567
|
+
- Performs network requests to the homeserver for verification and logout when credentials are complete.
|
|
566
568
|
"""
|
|
567
569
|
|
|
568
570
|
# Import inside function to avoid circular imports
|
|
@@ -589,6 +591,54 @@ async def logout_matrix_bot(password: str):
|
|
|
589
591
|
access_token = credentials.get("access_token")
|
|
590
592
|
device_id = credentials.get("device_id")
|
|
591
593
|
|
|
594
|
+
# If user_id is missing, try to fetch it using the access token
|
|
595
|
+
if not user_id and access_token and homeserver:
|
|
596
|
+
logger.info("user_id missing from credentials, attempting to fetch it...")
|
|
597
|
+
print("🔍 user_id missing from credentials, attempting to fetch it...")
|
|
598
|
+
|
|
599
|
+
try:
|
|
600
|
+
# Create SSL context for the temporary client
|
|
601
|
+
ssl_context = _create_ssl_context()
|
|
602
|
+
|
|
603
|
+
# Create a temporary client to fetch user_id
|
|
604
|
+
temp_client = AsyncClient(homeserver, ssl=ssl_context)
|
|
605
|
+
temp_client.access_token = access_token
|
|
606
|
+
|
|
607
|
+
# Fetch user_id using whoami
|
|
608
|
+
whoami_response = await asyncio.wait_for(
|
|
609
|
+
temp_client.whoami(),
|
|
610
|
+
timeout=MATRIX_LOGIN_TIMEOUT,
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
if hasattr(whoami_response, "user_id"):
|
|
614
|
+
user_id = whoami_response.user_id
|
|
615
|
+
logger.info(f"Successfully fetched user_id: {user_id}")
|
|
616
|
+
print(f"✅ Successfully fetched user_id: {user_id}")
|
|
617
|
+
|
|
618
|
+
# Update credentials with the fetched user_id
|
|
619
|
+
credentials["user_id"] = user_id
|
|
620
|
+
from mmrelay.config import save_credentials
|
|
621
|
+
|
|
622
|
+
save_credentials(credentials)
|
|
623
|
+
logger.info("Updated credentials.json with fetched user_id")
|
|
624
|
+
print("✅ Updated credentials.json with fetched user_id")
|
|
625
|
+
else:
|
|
626
|
+
logger.error("Failed to fetch user_id from whoami response")
|
|
627
|
+
print("❌ Failed to fetch user_id from whoami response")
|
|
628
|
+
|
|
629
|
+
except asyncio.TimeoutError:
|
|
630
|
+
logger.error("Timeout while fetching user_id")
|
|
631
|
+
print("❌ Timeout while fetching user_id")
|
|
632
|
+
except Exception as e:
|
|
633
|
+
logger.exception("Error fetching user_id")
|
|
634
|
+
print(f"❌ Error fetching user_id: {e}")
|
|
635
|
+
finally:
|
|
636
|
+
try:
|
|
637
|
+
await temp_client.close()
|
|
638
|
+
except Exception:
|
|
639
|
+
# Ignore errors when closing client during logout
|
|
640
|
+
pass
|
|
641
|
+
|
|
592
642
|
if not all([homeserver, user_id, access_token, device_id]):
|
|
593
643
|
logger.error("Invalid credentials found. Cannot verify logout.")
|
|
594
644
|
logger.info("Proceeding with local cleanup only...")
|
|
@@ -691,6 +741,6 @@ async def logout_matrix_bot(password: str):
|
|
|
691
741
|
return success
|
|
692
742
|
|
|
693
743
|
except Exception as e:
|
|
694
|
-
logger.
|
|
744
|
+
logger.exception("Error during logout process")
|
|
695
745
|
print(f"❌ Error during logout process: {e}")
|
|
696
746
|
return False
|
mmrelay/config.py
CHANGED
|
@@ -120,15 +120,24 @@ def get_config_paths(args=None):
|
|
|
120
120
|
|
|
121
121
|
def get_data_dir():
|
|
122
122
|
"""
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
Return the directory for application data, creating it if it does not exist.
|
|
124
|
+
|
|
125
|
+
On Linux and macOS this is <base_dir>/data (where base_dir is returned by get_base_dir()).
|
|
126
|
+
On Windows, if a global custom_data_dir is set it returns <custom_data_dir>/data; otherwise it falls back to platformdirs.user_data_dir(APP_NAME, APP_AUTHOR).
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
str: Absolute path to the data directory.
|
|
125
130
|
"""
|
|
126
131
|
if sys.platform in ["linux", "darwin"]:
|
|
127
132
|
# Use ~/.mmrelay/data/ for Linux and Mac
|
|
128
133
|
data_dir = os.path.join(get_base_dir(), "data")
|
|
129
134
|
else:
|
|
130
|
-
#
|
|
131
|
-
|
|
135
|
+
# Honor --data-dir on Windows too
|
|
136
|
+
if custom_data_dir:
|
|
137
|
+
data_dir = os.path.join(custom_data_dir, "data")
|
|
138
|
+
else:
|
|
139
|
+
# Use platformdirs default for Windows
|
|
140
|
+
data_dir = platformdirs.user_data_dir(APP_NAME, APP_AUTHOR)
|
|
132
141
|
|
|
133
142
|
os.makedirs(data_dir, exist_ok=True)
|
|
134
143
|
return data_dir
|
|
@@ -162,15 +171,24 @@ def get_plugin_data_dir(plugin_name=None):
|
|
|
162
171
|
|
|
163
172
|
def get_log_dir():
|
|
164
173
|
"""
|
|
165
|
-
|
|
166
|
-
|
|
174
|
+
Return the path to the application's log directory, creating it if missing.
|
|
175
|
+
|
|
176
|
+
On Linux/macOS this is '<base_dir>/logs' (where base_dir is returned by get_base_dir()).
|
|
177
|
+
On Windows, if a global custom_data_dir is set it uses '<custom_data_dir>/logs'; otherwise it uses the platform-specific user log directory from platformdirs.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
str: Absolute path to the log directory that now exists (created if necessary).
|
|
167
181
|
"""
|
|
168
182
|
if sys.platform in ["linux", "darwin"]:
|
|
169
183
|
# Use ~/.mmrelay/logs/ for Linux and Mac
|
|
170
184
|
log_dir = os.path.join(get_base_dir(), "logs")
|
|
171
185
|
else:
|
|
172
|
-
#
|
|
173
|
-
|
|
186
|
+
# Honor --data-dir on Windows too
|
|
187
|
+
if custom_data_dir:
|
|
188
|
+
log_dir = os.path.join(custom_data_dir, "logs")
|
|
189
|
+
else:
|
|
190
|
+
# Use platformdirs default for Windows
|
|
191
|
+
log_dir = platformdirs.user_log_dir(APP_NAME, APP_AUTHOR)
|
|
174
192
|
|
|
175
193
|
os.makedirs(log_dir, exist_ok=True)
|
|
176
194
|
return log_dir
|
|
@@ -178,22 +196,27 @@ def get_log_dir():
|
|
|
178
196
|
|
|
179
197
|
def get_e2ee_store_dir():
|
|
180
198
|
"""
|
|
181
|
-
Return the path to the
|
|
182
|
-
|
|
183
|
-
On Linux
|
|
184
|
-
On Windows
|
|
185
|
-
|
|
199
|
+
Return the absolute path to the E2EE data store directory, creating it if missing.
|
|
200
|
+
|
|
201
|
+
On Linux and macOS this is "<base_dir>/store" where base_dir is returned by get_base_dir().
|
|
202
|
+
On Windows this is "<custom_data_dir>/store" when module-level custom_data_dir is set; otherwise it uses
|
|
203
|
+
platformdirs.user_data_dir(APP_NAME, APP_AUTHOR)/store.
|
|
204
|
+
|
|
186
205
|
Returns:
|
|
187
|
-
str: Absolute path to the ensured store directory.
|
|
206
|
+
str: Absolute path to the ensured store directory. The directory is created if it does not exist.
|
|
188
207
|
"""
|
|
189
208
|
if sys.platform in ["linux", "darwin"]:
|
|
190
209
|
# Use ~/.mmrelay/store/ for Linux and Mac
|
|
191
210
|
store_dir = os.path.join(get_base_dir(), "store")
|
|
192
211
|
else:
|
|
193
|
-
#
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
212
|
+
# Honor --data-dir on Windows too
|
|
213
|
+
if custom_data_dir:
|
|
214
|
+
store_dir = os.path.join(custom_data_dir, "store")
|
|
215
|
+
else:
|
|
216
|
+
# Use platformdirs default for Windows
|
|
217
|
+
store_dir = os.path.join(
|
|
218
|
+
platformdirs.user_data_dir(APP_NAME, APP_AUTHOR), "store"
|
|
219
|
+
)
|
|
197
220
|
|
|
198
221
|
os.makedirs(store_dir, exist_ok=True)
|
|
199
222
|
return store_dir
|
|
@@ -320,10 +343,9 @@ def load_logging_config_from_env():
|
|
|
320
343
|
|
|
321
344
|
def load_database_config_from_env():
|
|
322
345
|
"""
|
|
323
|
-
|
|
346
|
+
Build a database configuration fragment from environment variables.
|
|
324
347
|
|
|
325
|
-
Returns
|
|
326
|
-
dict: Database configuration dictionary if any env vars found, None otherwise.
|
|
348
|
+
Reads environment variables defined in the module-level _DATABASE_ENV_VAR_MAPPINGS and converts them into a configuration dictionary suitable for merging into the application's config. Returns None if no mapped environment variables were present.
|
|
327
349
|
"""
|
|
328
350
|
config = _load_config_from_env_mapping(_DATABASE_ENV_VAR_MAPPINGS)
|
|
329
351
|
if config:
|
|
@@ -333,20 +355,84 @@ def load_database_config_from_env():
|
|
|
333
355
|
return config
|
|
334
356
|
|
|
335
357
|
|
|
358
|
+
def is_e2ee_enabled(config):
|
|
359
|
+
"""
|
|
360
|
+
Check if End-to-End Encryption (E2EE) is enabled in the configuration.
|
|
361
|
+
|
|
362
|
+
Checks both 'encryption' and 'e2ee' keys in the matrix section for backward compatibility.
|
|
363
|
+
On Windows, this always returns False since E2EE is not supported.
|
|
364
|
+
|
|
365
|
+
Parameters:
|
|
366
|
+
config (dict): Configuration dictionary to check.
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
bool: True if E2EE is enabled, False otherwise.
|
|
370
|
+
"""
|
|
371
|
+
# E2EE is not supported on Windows
|
|
372
|
+
if sys.platform == "win32":
|
|
373
|
+
return False
|
|
374
|
+
|
|
375
|
+
if not config:
|
|
376
|
+
return False
|
|
377
|
+
|
|
378
|
+
matrix_cfg = config.get("matrix", {}) or {}
|
|
379
|
+
if not matrix_cfg:
|
|
380
|
+
return False
|
|
381
|
+
|
|
382
|
+
encryption_enabled = matrix_cfg.get("encryption", {}).get("enabled", False)
|
|
383
|
+
e2ee_enabled = matrix_cfg.get("e2ee", {}).get("enabled", False)
|
|
384
|
+
|
|
385
|
+
return encryption_enabled or e2ee_enabled
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def check_e2ee_enabled_silently(args=None):
|
|
389
|
+
"""
|
|
390
|
+
Check silently whether End-to-End Encryption (E2EE) is enabled in the first readable configuration file.
|
|
391
|
+
|
|
392
|
+
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
|
+
|
|
394
|
+
Parameters:
|
|
395
|
+
args (optional): Parsed command-line arguments that can influence config search order.
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
bool: True if E2EE is enabled in the first valid configuration file found; otherwise False.
|
|
399
|
+
"""
|
|
400
|
+
# E2EE is not supported on Windows
|
|
401
|
+
if sys.platform == "win32":
|
|
402
|
+
return False
|
|
403
|
+
|
|
404
|
+
# Get config paths without logging
|
|
405
|
+
config_paths = get_config_paths(args)
|
|
406
|
+
|
|
407
|
+
# Try each config path silently
|
|
408
|
+
for path in config_paths:
|
|
409
|
+
if os.path.isfile(path):
|
|
410
|
+
try:
|
|
411
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
412
|
+
config = yaml.load(f, Loader=SafeLoader)
|
|
413
|
+
if config and is_e2ee_enabled(config):
|
|
414
|
+
return True
|
|
415
|
+
except (yaml.YAMLError, PermissionError, OSError):
|
|
416
|
+
continue # Silently try the next path
|
|
417
|
+
# No valid config found or E2EE not enabled in any config
|
|
418
|
+
return False
|
|
419
|
+
|
|
420
|
+
|
|
336
421
|
def apply_env_config_overrides(config):
|
|
337
422
|
"""
|
|
338
|
-
Apply environment-
|
|
423
|
+
Apply environment-derived configuration overrides to a configuration dictionary.
|
|
339
424
|
|
|
340
|
-
If `config` is falsy a new dict is created. Environment
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
425
|
+
If `config` is falsy, a new dict is created. Environment variables are read and merged into
|
|
426
|
+
the top-level keys "meshtastic", "logging", and "database" when corresponding environment
|
|
427
|
+
fragments are present. Existing subkeys are updated with environment values while other keys
|
|
428
|
+
in those sections are preserved. The input dict may be mutated in place.
|
|
344
429
|
|
|
345
430
|
Parameters:
|
|
346
|
-
config (dict): Base configuration to update
|
|
431
|
+
config (dict | None): Base configuration to update.
|
|
347
432
|
|
|
348
433
|
Returns:
|
|
349
|
-
dict: The
|
|
434
|
+
dict: The configuration dictionary with environment overrides applied (the same object
|
|
435
|
+
passed in, or a newly created dict if a falsy value was provided).
|
|
350
436
|
"""
|
|
351
437
|
if not config:
|
|
352
438
|
config = {}
|
|
@@ -382,51 +468,90 @@ def load_credentials():
|
|
|
382
468
|
config_dir = get_base_dir()
|
|
383
469
|
credentials_path = os.path.join(config_dir, "credentials.json")
|
|
384
470
|
|
|
471
|
+
logger.debug(f"Looking for credentials at: {credentials_path}")
|
|
472
|
+
|
|
385
473
|
if os.path.exists(credentials_path):
|
|
386
|
-
with open(credentials_path, "r") as f:
|
|
474
|
+
with open(credentials_path, "r", encoding="utf-8") as f:
|
|
387
475
|
credentials = json.load(f)
|
|
388
|
-
logger.debug(f"
|
|
476
|
+
logger.debug(f"Successfully loaded credentials from {credentials_path}")
|
|
389
477
|
return credentials
|
|
390
478
|
else:
|
|
391
479
|
logger.debug(f"No credentials file found at {credentials_path}")
|
|
480
|
+
# On Windows, also log the directory contents for debugging
|
|
481
|
+
if sys.platform == "win32" and os.path.exists(config_dir):
|
|
482
|
+
try:
|
|
483
|
+
files = os.listdir(config_dir)
|
|
484
|
+
logger.debug(f"Directory contents of {config_dir}: {files}")
|
|
485
|
+
except OSError:
|
|
486
|
+
pass
|
|
392
487
|
return None
|
|
393
|
-
except (OSError, PermissionError, json.JSONDecodeError)
|
|
394
|
-
logger.
|
|
488
|
+
except (OSError, PermissionError, json.JSONDecodeError):
|
|
489
|
+
logger.exception(f"Error loading credentials.json from {config_dir}")
|
|
395
490
|
return None
|
|
396
491
|
|
|
397
492
|
|
|
398
493
|
def save_credentials(credentials):
|
|
399
494
|
"""
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
495
|
+
Persist a JSON-serializable credentials mapping to <base_dir>/credentials.json.
|
|
496
|
+
|
|
497
|
+
Writes the provided credentials (a JSON-serializable mapping) to the application's
|
|
498
|
+
base configuration directory as credentials.json, creating the base directory if
|
|
499
|
+
necessary. On Unix-like systems the file permissions are adjusted to be
|
|
500
|
+
restrictive (0o600) when possible. I/O and permission errors are caught and
|
|
501
|
+
logged; the function does not raise them.
|
|
502
|
+
|
|
503
|
+
Parameters:
|
|
504
|
+
credentials (dict): JSON-serializable mapping of credentials to persist.
|
|
505
|
+
|
|
506
|
+
Returns:
|
|
507
|
+
None
|
|
403
508
|
"""
|
|
404
509
|
try:
|
|
405
510
|
config_dir = get_base_dir()
|
|
511
|
+
# Ensure the directory exists and is writable
|
|
512
|
+
os.makedirs(config_dir, exist_ok=True)
|
|
406
513
|
credentials_path = os.path.join(config_dir, "credentials.json")
|
|
407
514
|
|
|
408
|
-
|
|
515
|
+
# Log the path for debugging, especially on Windows
|
|
516
|
+
logger.info(f"Saving credentials to: {credentials_path}")
|
|
517
|
+
|
|
518
|
+
with open(credentials_path, "w", encoding="utf-8") as f:
|
|
409
519
|
json.dump(credentials, f, indent=2)
|
|
410
520
|
|
|
411
521
|
# Set secure permissions on Unix systems (600 - owner read/write only)
|
|
412
522
|
set_secure_file_permissions(credentials_path)
|
|
413
523
|
|
|
414
|
-
logger.info(f"
|
|
415
|
-
|
|
416
|
-
|
|
524
|
+
logger.info(f"Successfully saved credentials to {credentials_path}")
|
|
525
|
+
|
|
526
|
+
# Verify the file was actually created
|
|
527
|
+
if os.path.exists(credentials_path):
|
|
528
|
+
logger.debug(f"Verified credentials.json exists at {credentials_path}")
|
|
529
|
+
else:
|
|
530
|
+
logger.error(f"Failed to create credentials.json at {credentials_path}")
|
|
531
|
+
|
|
532
|
+
except (OSError, PermissionError):
|
|
533
|
+
logger.exception(f"Error saving credentials.json to {config_dir}")
|
|
534
|
+
# Try to provide helpful Windows-specific guidance
|
|
535
|
+
if sys.platform == "win32":
|
|
536
|
+
logger.error(
|
|
537
|
+
"On Windows, ensure the application has write permissions to the user data directory"
|
|
538
|
+
)
|
|
539
|
+
logger.error(f"Attempted path: {config_dir}")
|
|
417
540
|
|
|
418
541
|
|
|
419
542
|
# Set up a basic logger for config
|
|
420
543
|
logger = logging.getLogger("Config")
|
|
421
544
|
logger.setLevel(logging.INFO)
|
|
422
|
-
|
|
423
|
-
handler.
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
545
|
+
if not logger.handlers:
|
|
546
|
+
handler = logging.StreamHandler()
|
|
547
|
+
handler.setFormatter(
|
|
548
|
+
logging.Formatter(
|
|
549
|
+
fmt="%(asctime)s %(levelname)s:%(name)s:%(message)s",
|
|
550
|
+
datefmt="%Y-%m-%d %H:%M:%S %z",
|
|
551
|
+
)
|
|
427
552
|
)
|
|
428
|
-
)
|
|
429
|
-
logger.
|
|
553
|
+
logger.addHandler(handler)
|
|
554
|
+
logger.propagate = False
|
|
430
555
|
|
|
431
556
|
# Initialize empty config
|
|
432
557
|
relay_config = {}
|
|
@@ -626,16 +751,16 @@ def set_config(module, passed_config):
|
|
|
626
751
|
|
|
627
752
|
def load_config(config_file=None, args=None):
|
|
628
753
|
"""
|
|
629
|
-
Load the application configuration from a file or from environment variables.
|
|
754
|
+
Load the application configuration from a YAML file or from environment variables.
|
|
630
755
|
|
|
631
|
-
If config_file is provided and exists,
|
|
756
|
+
If config_file is provided and exists, that file is read and parsed as YAML; otherwise the function searches candidate locations returned by get_config_paths(args) and loads the first readable YAML file found. Empty or null YAML is treated as an empty dict. After loading, environment-derived overrides are merged via apply_env_config_overrides(). The function updates the module-level relay_config and config_path.
|
|
632
757
|
|
|
633
758
|
Parameters:
|
|
634
|
-
config_file (str, optional): Path to a specific YAML configuration file. If None,
|
|
635
|
-
args: Parsed command-line arguments
|
|
759
|
+
config_file (str, optional): Path to a specific YAML configuration file to load. If None, candidate paths from get_config_paths(args) are used.
|
|
760
|
+
args: Parsed command-line arguments forwarded to get_config_paths() to influence the search order.
|
|
636
761
|
|
|
637
762
|
Returns:
|
|
638
|
-
dict: The resulting configuration dictionary.
|
|
763
|
+
dict: The resulting configuration dictionary. If no configuration is found or a file read/parse error occurs, returns an empty dict.
|
|
639
764
|
"""
|
|
640
765
|
global relay_config, config_path
|
|
641
766
|
|
|
@@ -643,7 +768,7 @@ def load_config(config_file=None, args=None):
|
|
|
643
768
|
if config_file and os.path.isfile(config_file):
|
|
644
769
|
# Store the config path but don't log it yet - will be logged by main.py
|
|
645
770
|
try:
|
|
646
|
-
with open(config_file, "r") as f:
|
|
771
|
+
with open(config_file, "r", encoding="utf-8") as f:
|
|
647
772
|
relay_config = yaml.load(f, Loader=SafeLoader)
|
|
648
773
|
config_path = config_file
|
|
649
774
|
# Treat empty/null YAML files as an empty config dictionary
|
|
@@ -652,8 +777,8 @@ def load_config(config_file=None, args=None):
|
|
|
652
777
|
# Apply environment variable overrides
|
|
653
778
|
relay_config = apply_env_config_overrides(relay_config)
|
|
654
779
|
return relay_config
|
|
655
|
-
except (yaml.YAMLError, PermissionError, OSError)
|
|
656
|
-
logger.
|
|
780
|
+
except (yaml.YAMLError, PermissionError, OSError):
|
|
781
|
+
logger.exception(f"Error loading config file {config_file}")
|
|
657
782
|
return {}
|
|
658
783
|
|
|
659
784
|
# Otherwise, search for a config file
|
|
@@ -665,7 +790,7 @@ def load_config(config_file=None, args=None):
|
|
|
665
790
|
config_path = path
|
|
666
791
|
# Store the config path but don't log it yet - will be logged by main.py
|
|
667
792
|
try:
|
|
668
|
-
with open(config_path, "r") as f:
|
|
793
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
669
794
|
relay_config = yaml.load(f, Loader=SafeLoader)
|
|
670
795
|
# Treat empty/null YAML files as an empty config dictionary
|
|
671
796
|
if relay_config is None:
|
|
@@ -673,8 +798,8 @@ def load_config(config_file=None, args=None):
|
|
|
673
798
|
# Apply environment variable overrides
|
|
674
799
|
relay_config = apply_env_config_overrides(relay_config)
|
|
675
800
|
return relay_config
|
|
676
|
-
except (yaml.YAMLError, PermissionError, OSError)
|
|
677
|
-
logger.
|
|
801
|
+
except (yaml.YAMLError, PermissionError, OSError):
|
|
802
|
+
logger.exception(f"Error loading config file {path}")
|
|
678
803
|
continue # Try the next config path
|
|
679
804
|
|
|
680
805
|
# No config file found - try to use environment variables only
|
|
@@ -696,22 +821,24 @@ def load_config(config_file=None, args=None):
|
|
|
696
821
|
|
|
697
822
|
def validate_yaml_syntax(config_content, config_path):
|
|
698
823
|
"""
|
|
699
|
-
Validate YAML
|
|
700
|
-
|
|
701
|
-
Performs lightweight line-based checks for
|
|
702
|
-
and non-standard boolean words like 'yes'/'no') and then
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
824
|
+
Validate YAML text for syntax and common style issues, parse it with PyYAML, and return results.
|
|
825
|
+
|
|
826
|
+
Performs lightweight line-based checks for frequent mistakes (using '=' instead of ':'
|
|
827
|
+
for mappings and non-standard boolean words like 'yes'/'no' or 'on'/'off') and then
|
|
828
|
+
attempts to parse the content with yaml.safe_load. If only style warnings are found,
|
|
829
|
+
parsing is considered successful and warnings are returned; if parsing fails or true
|
|
830
|
+
syntax errors are detected, a detailed error message is returned that references
|
|
831
|
+
config_path to identify the source.
|
|
832
|
+
|
|
706
833
|
Parameters:
|
|
707
834
|
config_content (str): Raw YAML text to validate.
|
|
708
|
-
config_path (str): Path used in error messages to identify the source
|
|
709
|
-
|
|
835
|
+
config_path (str): Path or label used in error messages to identify the source of the content.
|
|
836
|
+
|
|
710
837
|
Returns:
|
|
711
838
|
tuple:
|
|
712
|
-
is_valid (bool): True if
|
|
713
|
-
|
|
714
|
-
parsed_config (
|
|
839
|
+
is_valid (bool): True if YAML parsed successfully (style warnings allowed), False on syntax/parsing error.
|
|
840
|
+
message (str|None): Human-readable warnings (when parsing succeeded with style issues) or a detailed error description (when parsing failed). None when parsing succeeded without issues.
|
|
841
|
+
parsed_config (object|None): The Python object produced by yaml.safe_load on success; None when parsing failed.
|
|
715
842
|
"""
|
|
716
843
|
lines = config_content.split("\n")
|
|
717
844
|
|
|
@@ -731,8 +858,8 @@ def validate_yaml_syntax(config_content, config_path):
|
|
|
731
858
|
|
|
732
859
|
# Check for non-standard boolean values (style warning)
|
|
733
860
|
bool_pattern = r":\s*(yes|no|on|off|Yes|No|YES|NO)\s*$"
|
|
734
|
-
|
|
735
|
-
|
|
861
|
+
match = re.search(bool_pattern, line)
|
|
862
|
+
if match:
|
|
736
863
|
non_standard_bool = match.group(1)
|
|
737
864
|
syntax_issues.append(
|
|
738
865
|
f"Line {line_num}: Style warning - Consider using 'true' or 'false' instead of '{non_standard_bool}' for clarity - {line.strip()}"
|
mmrelay/constants/app.py
CHANGED
|
@@ -10,8 +10,8 @@ APP_NAME = "mmrelay"
|
|
|
10
10
|
APP_AUTHOR = None # No author directory for platformdirs
|
|
11
11
|
|
|
12
12
|
# Application display names
|
|
13
|
-
APP_DISPLAY_NAME = "
|
|
14
|
-
APP_FULL_NAME = "Meshtastic Matrix Relay"
|
|
13
|
+
APP_DISPLAY_NAME = "MMRelay"
|
|
14
|
+
APP_FULL_NAME = "MMRelay - Meshtastic <=> Matrix Relay"
|
|
15
15
|
|
|
16
16
|
# Matrix client identification
|
|
17
17
|
MATRIX_DEVICE_NAME = "MMRelay"
|