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/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
- Verifies the provided Matrix password by performing a temporary login, then attempts to
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 applicable) completed successfully;
565
- False on failure. If matrix-nio is not installed, prints an error and returns False.
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.error(f"Error during logout process: {e}")
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
- Returns the directory for storing application data files.
124
- Creates the directory if it doesn't exist.
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
- # Use platformdirs default for Windows
131
- data_dir = platformdirs.user_data_dir(APP_NAME, APP_AUTHOR)
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
- Returns the directory for storing log files.
166
- Creates the directory if it doesn't exist.
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
- # Use platformdirs default for Windows
173
- log_dir = platformdirs.user_log_dir(APP_NAME, APP_AUTHOR)
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 directory used for storing E2EE data (e.g., encryption keys), creating it if missing.
182
-
183
- On Linux/macOS the directory is "<base_dir>/store" where base_dir is returned by get_base_dir().
184
- On Windows the directory is under the platform user data directory for the app (user_data_dir(APP_NAME, APP_AUTHOR)/store).
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
- # Use platformdirs default for Windows
194
- store_dir = os.path.join(
195
- platformdirs.user_data_dir(APP_NAME, APP_AUTHOR), "store"
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
- Load database configuration from environment variables.
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-variable-derived overrides to a configuration dictionary.
423
+ Apply environment-derived configuration overrides to a configuration dictionary.
339
424
 
340
- If `config` is falsy a new dict is created. Environment values from the Meshtastic, logging,
341
- and database loaders are merged into the corresponding top-level sections ("meshtastic",
342
- "logging", "database"); existing keys in those sections are updated with environment-supplied
343
- values while other keys are left intact.
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; may be None or an empty value.
431
+ config (dict | None): Base configuration to update.
347
432
 
348
433
  Returns:
349
- dict: The resulting configuration dictionary with environment overrides applied.
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"Loaded credentials from {credentials_path}")
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) as e:
394
- logger.error(f"Error loading credentials.json: {e}")
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
- Write the provided Matrix credentials dict to "credentials.json" in the application's base config directory and apply secure file permissions (Unix 0o600) to the file.
401
-
402
- If writing or permission changes fail, an error is logged; exceptions are not propagated.
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
- with open(credentials_path, "w") as f:
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"Saved credentials to {credentials_path}")
415
- except (OSError, PermissionError) as e:
416
- logger.error(f"Error saving credentials.json: {e}")
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
- handler = logging.StreamHandler()
423
- handler.setFormatter(
424
- logging.Formatter(
425
- fmt="%(asctime)s %(levelname)s:%(name)s:%(message)s",
426
- datefmt="%Y-%m-%d %H:%M:%S %z",
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.addHandler(handler)
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, load and parse it as YAML. Otherwise search prioritized locations returned by get_config_paths(args) and load the first readable YAML file found. After loading (or when no file is found) environment-variable-derived overrides are merged into the configuration via apply_env_config_overrides(). The function updates the module-level relay_config and config_path.
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, the function searches default locations.
635
- args: Parsed command-line arguments used to influence the search order (passed to get_config_paths).
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. Returns an empty dict on read/parse errors or if no configuration is provided via files or environment.
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) as e:
656
- logger.error(f"Error loading config file {config_file}: {e}")
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) as e:
677
- logger.error(f"Error loading config file {path}: {e}")
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 content and return parsing results plus human-readable syntax feedback.
700
-
701
- Performs lightweight line-based checks for common mistakes (unclosed quotes, use of '=' instead of ':',
702
- and non-standard boolean words like 'yes'/'no') and then attempts to parse the content with PyYAML.
703
- If only style warnings are found the parser result is returned with warnings; if syntax errors are detected
704
- or YAML parsing fails, a detailed error message is returned.
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 file.
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 parsing succeeded (even if style warnings exist), False on syntax/parsing error.
713
- error_message (str|None): Human-readable warnings or error details. None when parsing succeeded with no issues.
714
- parsed_config (dict|list|None): The parsed YAML structure on success; None when parsing failed.
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
- if re.search(bool_pattern, line):
735
- match = re.search(bool_pattern, line)
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 = "M<>M Relay"
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"