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/cli.py
CHANGED
|
@@ -5,22 +5,66 @@ Command-line interface handling for the Meshtastic Matrix Relay.
|
|
|
5
5
|
import argparse
|
|
6
6
|
import importlib.resources
|
|
7
7
|
import os
|
|
8
|
+
import shutil
|
|
8
9
|
import sys
|
|
9
10
|
|
|
10
|
-
import yaml
|
|
11
|
-
from yaml.loader import SafeLoader
|
|
12
|
-
|
|
13
11
|
# Import version from package
|
|
14
12
|
from mmrelay import __version__
|
|
13
|
+
from mmrelay.cli_utils import (
|
|
14
|
+
get_command,
|
|
15
|
+
get_deprecation_warning,
|
|
16
|
+
msg_for_e2ee_support,
|
|
17
|
+
msg_or_run_auth_login,
|
|
18
|
+
msg_run_auth_login,
|
|
19
|
+
msg_setup_auth,
|
|
20
|
+
msg_setup_authentication,
|
|
21
|
+
msg_suggest_generate_config,
|
|
22
|
+
)
|
|
23
|
+
from mmrelay.config import (
|
|
24
|
+
get_config_paths,
|
|
25
|
+
set_secure_file_permissions,
|
|
26
|
+
validate_yaml_syntax,
|
|
27
|
+
)
|
|
28
|
+
from mmrelay.constants.app import WINDOWS_PLATFORM
|
|
29
|
+
from mmrelay.constants.config import (
|
|
30
|
+
CONFIG_KEY_ACCESS_TOKEN,
|
|
31
|
+
CONFIG_KEY_BOT_USER_ID,
|
|
32
|
+
CONFIG_KEY_HOMESERVER,
|
|
33
|
+
CONFIG_SECTION_MATRIX,
|
|
34
|
+
CONFIG_SECTION_MESHTASTIC,
|
|
35
|
+
)
|
|
36
|
+
from mmrelay.constants.network import (
|
|
37
|
+
CONFIG_KEY_BLE_ADDRESS,
|
|
38
|
+
CONFIG_KEY_CONNECTION_TYPE,
|
|
39
|
+
CONFIG_KEY_HOST,
|
|
40
|
+
CONFIG_KEY_SERIAL_PORT,
|
|
41
|
+
CONNECTION_TYPE_BLE,
|
|
42
|
+
CONNECTION_TYPE_NETWORK,
|
|
43
|
+
CONNECTION_TYPE_SERIAL,
|
|
44
|
+
CONNECTION_TYPE_TCP,
|
|
45
|
+
)
|
|
15
46
|
from mmrelay.tools import get_sample_config_path
|
|
16
47
|
|
|
48
|
+
# =============================================================================
|
|
49
|
+
# CLI Argument Parsing and Command Handling
|
|
50
|
+
# =============================================================================
|
|
51
|
+
|
|
17
52
|
|
|
18
53
|
def parse_arguments():
|
|
19
54
|
"""
|
|
20
|
-
Parse command-line arguments.
|
|
55
|
+
Parse command-line arguments for the Meshtastic Matrix Relay CLI.
|
|
56
|
+
|
|
57
|
+
Builds a modern grouped CLI with subcommands for config (generate, check), auth (login, status),
|
|
58
|
+
and service (install), while preserving hidden legacy flags (--generate-config, --install-service,
|
|
59
|
+
--check-config, --auth) for backward compatibility. Supports global options: --config,
|
|
60
|
+
--data-dir, --log-level, --logfile, and --version.
|
|
61
|
+
|
|
62
|
+
Unknown arguments are ignored when running outside of test environments (parsed via
|
|
63
|
+
parse_known_args); a warning is printed if unknown args are present and the process does not
|
|
64
|
+
appear to be a test run.
|
|
21
65
|
|
|
22
66
|
Returns:
|
|
23
|
-
argparse.Namespace: The parsed
|
|
67
|
+
argparse.Namespace: The parsed arguments namespace.
|
|
24
68
|
"""
|
|
25
69
|
parser = argparse.ArgumentParser(
|
|
26
70
|
description="Meshtastic Matrix Relay - Bridge between Meshtastic and Matrix"
|
|
@@ -43,45 +87,111 @@ def parse_arguments():
|
|
|
43
87
|
default=None,
|
|
44
88
|
)
|
|
45
89
|
parser.add_argument("--version", action="store_true", help="Show version and exit")
|
|
90
|
+
# Deprecated flags (hidden from help but still functional)
|
|
46
91
|
parser.add_argument(
|
|
47
92
|
"--generate-config",
|
|
48
93
|
action="store_true",
|
|
49
|
-
help=
|
|
94
|
+
help=argparse.SUPPRESS,
|
|
50
95
|
)
|
|
51
96
|
parser.add_argument(
|
|
52
97
|
"--install-service",
|
|
53
98
|
action="store_true",
|
|
54
|
-
help=
|
|
99
|
+
help=argparse.SUPPRESS,
|
|
55
100
|
)
|
|
56
101
|
parser.add_argument(
|
|
57
102
|
"--check-config",
|
|
58
103
|
action="store_true",
|
|
59
|
-
help=
|
|
104
|
+
help=argparse.SUPPRESS,
|
|
105
|
+
)
|
|
106
|
+
parser.add_argument(
|
|
107
|
+
"--auth",
|
|
108
|
+
action="store_true",
|
|
109
|
+
help=argparse.SUPPRESS,
|
|
60
110
|
)
|
|
61
111
|
|
|
62
|
-
#
|
|
63
|
-
|
|
64
|
-
if sys.platform == "win32":
|
|
65
|
-
parser.add_argument(
|
|
66
|
-
"config_path", nargs="?", help=argparse.SUPPRESS, default=None
|
|
67
|
-
)
|
|
112
|
+
# Add grouped subcommands for modern CLI interface
|
|
113
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
68
114
|
|
|
69
|
-
|
|
115
|
+
# CONFIG group
|
|
116
|
+
config_parser = subparsers.add_parser(
|
|
117
|
+
"config",
|
|
118
|
+
help="Configuration management",
|
|
119
|
+
description="Manage configuration files and validation",
|
|
120
|
+
)
|
|
121
|
+
config_subparsers = config_parser.add_subparsers(
|
|
122
|
+
dest="config_command", help="Config commands", required=True
|
|
123
|
+
)
|
|
124
|
+
config_subparsers.add_parser(
|
|
125
|
+
"generate",
|
|
126
|
+
help="Create sample config.yaml file",
|
|
127
|
+
description="Generate a sample configuration file with default settings",
|
|
128
|
+
)
|
|
129
|
+
config_subparsers.add_parser(
|
|
130
|
+
"check",
|
|
131
|
+
help="Validate configuration file",
|
|
132
|
+
description="Check configuration file syntax and completeness",
|
|
133
|
+
)
|
|
70
134
|
|
|
71
|
-
#
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
and
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
135
|
+
# AUTH group
|
|
136
|
+
auth_parser = subparsers.add_parser(
|
|
137
|
+
"auth",
|
|
138
|
+
help="Authentication management",
|
|
139
|
+
description="Manage Matrix authentication and credentials",
|
|
140
|
+
)
|
|
141
|
+
auth_subparsers = auth_parser.add_subparsers(
|
|
142
|
+
dest="auth_command", help="Auth commands"
|
|
143
|
+
)
|
|
144
|
+
auth_subparsers.add_parser(
|
|
145
|
+
"login",
|
|
146
|
+
help="Authenticate with Matrix",
|
|
147
|
+
description="Set up Matrix authentication for E2EE support",
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
auth_subparsers.add_parser(
|
|
151
|
+
"status",
|
|
152
|
+
help="Check authentication status",
|
|
153
|
+
description="Display current Matrix authentication status",
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
logout_parser = auth_subparsers.add_parser(
|
|
157
|
+
"logout",
|
|
158
|
+
help="Log out and clear all sessions",
|
|
159
|
+
description="Clear all Matrix authentication data and E2EE store",
|
|
160
|
+
)
|
|
161
|
+
logout_parser.add_argument(
|
|
162
|
+
"--password",
|
|
163
|
+
nargs="?",
|
|
164
|
+
const="",
|
|
165
|
+
help="Password for verification. If no value provided, will prompt securely.",
|
|
166
|
+
type=str,
|
|
167
|
+
)
|
|
168
|
+
logout_parser.add_argument(
|
|
169
|
+
"-y",
|
|
170
|
+
"--yes",
|
|
171
|
+
action="store_true",
|
|
172
|
+
help="Do not prompt for confirmation (useful for non-interactive environments)",
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# SERVICE group
|
|
176
|
+
service_parser = subparsers.add_parser(
|
|
177
|
+
"service",
|
|
178
|
+
help="Service management",
|
|
179
|
+
description="Manage systemd user service for MMRelay",
|
|
180
|
+
)
|
|
181
|
+
service_subparsers = service_parser.add_subparsers(
|
|
182
|
+
dest="service_command", help="Service commands", required=True
|
|
183
|
+
)
|
|
184
|
+
service_subparsers.add_parser(
|
|
185
|
+
"install",
|
|
186
|
+
help="Install systemd user service",
|
|
187
|
+
description="Install or update the systemd user service for MMRelay",
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Use parse_known_args to handle unknown arguments gracefully (e.g., pytest args)
|
|
191
|
+
args, unknown = parser.parse_known_args()
|
|
192
|
+
# If there are unknown arguments and we're not in a test environment, warn about them
|
|
193
|
+
if unknown and not any("pytest" in arg or "test" in arg for arg in sys.argv):
|
|
194
|
+
print(f"Warning: Unknown arguments ignored: {unknown}")
|
|
85
195
|
|
|
86
196
|
return args
|
|
87
197
|
|
|
@@ -100,20 +210,499 @@ def print_version():
|
|
|
100
210
|
"""
|
|
101
211
|
Print the version in a simple format.
|
|
102
212
|
"""
|
|
103
|
-
print(f"MMRelay
|
|
213
|
+
print(f"MMRelay version {__version__}")
|
|
104
214
|
|
|
105
215
|
|
|
106
|
-
def
|
|
216
|
+
def _validate_e2ee_dependencies():
|
|
107
217
|
"""
|
|
108
|
-
Check
|
|
218
|
+
Check whether end-to-end encryption (E2EE) runtime dependencies are available.
|
|
109
219
|
|
|
110
|
-
|
|
111
|
-
|
|
220
|
+
Performs a platform check and attempts to import required packages (python-olm, nio.crypto.OlmDevice,
|
|
221
|
+
and nio.store.SqliteStore). Prints a short user-facing status message and guidance.
|
|
112
222
|
|
|
113
223
|
Returns:
|
|
114
|
-
bool: True if the
|
|
224
|
+
bool: True if the platform supports E2EE and all required dependencies can be imported;
|
|
225
|
+
False if running on an unsupported platform (Windows) or if any dependency is missing.
|
|
226
|
+
"""
|
|
227
|
+
if sys.platform == WINDOWS_PLATFORM:
|
|
228
|
+
print("❌ Error: E2EE is not supported on Windows")
|
|
229
|
+
print(" Reason: python-olm library requires native C libraries")
|
|
230
|
+
print(" Solution: Use Linux or macOS for E2EE support")
|
|
231
|
+
return False
|
|
232
|
+
|
|
233
|
+
# Check if E2EE dependencies are available
|
|
234
|
+
try:
|
|
235
|
+
import olm # noqa: F401
|
|
236
|
+
from nio.crypto import OlmDevice # noqa: F401
|
|
237
|
+
from nio.store import SqliteStore # noqa: F401
|
|
238
|
+
|
|
239
|
+
print("✅ E2EE dependencies are installed")
|
|
240
|
+
return True
|
|
241
|
+
except ImportError:
|
|
242
|
+
print("❌ Error: E2EE enabled but dependencies not installed")
|
|
243
|
+
print(" Install E2EE support: pipx install 'mmrelay[e2e]'")
|
|
244
|
+
return False
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _validate_credentials_json(config_path):
|
|
248
|
+
"""
|
|
249
|
+
Validate that a credentials.json file exists next to the given config and contains the required Matrix authentication fields.
|
|
250
|
+
|
|
251
|
+
Searches for credentials.json in the same directory as config_path, then falls back to the application's base directory. If found, the file is parsed as JSON and must include non-empty values for: "homeserver", "access_token", "user_id", and "device_id".
|
|
252
|
+
|
|
253
|
+
Parameters:
|
|
254
|
+
config_path (str): Path to the configuration file used to determine the primary search directory for credentials.json.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
bool: True if a valid credentials.json was found and contains all required fields; False otherwise. When invalid or missing fields are detected the function prints a short error and guidance to run the auth login flow.
|
|
258
|
+
"""
|
|
259
|
+
try:
|
|
260
|
+
import json
|
|
261
|
+
|
|
262
|
+
# Look for credentials.json in the same directory as the config file
|
|
263
|
+
config_dir = os.path.dirname(config_path)
|
|
264
|
+
credentials_path = os.path.join(config_dir, "credentials.json")
|
|
265
|
+
|
|
266
|
+
if not os.path.exists(credentials_path):
|
|
267
|
+
# Also try the standard location
|
|
268
|
+
from mmrelay.config import get_base_dir
|
|
269
|
+
|
|
270
|
+
standard_credentials_path = os.path.join(get_base_dir(), "credentials.json")
|
|
271
|
+
if os.path.exists(standard_credentials_path):
|
|
272
|
+
credentials_path = standard_credentials_path
|
|
273
|
+
else:
|
|
274
|
+
return False
|
|
275
|
+
|
|
276
|
+
# Load and validate credentials
|
|
277
|
+
with open(credentials_path, "r") as f:
|
|
278
|
+
credentials = json.load(f)
|
|
279
|
+
|
|
280
|
+
# Check for required fields
|
|
281
|
+
required_fields = ["homeserver", "access_token", "user_id", "device_id"]
|
|
282
|
+
missing_fields = [
|
|
283
|
+
field
|
|
284
|
+
for field in required_fields
|
|
285
|
+
if field not in credentials or not credentials[field]
|
|
286
|
+
]
|
|
287
|
+
|
|
288
|
+
if missing_fields:
|
|
289
|
+
print(
|
|
290
|
+
f"❌ Error: credentials.json missing required fields: {', '.join(missing_fields)}"
|
|
291
|
+
)
|
|
292
|
+
print(f" {msg_run_auth_login()}")
|
|
293
|
+
return False
|
|
294
|
+
|
|
295
|
+
return True
|
|
296
|
+
except Exception as e:
|
|
297
|
+
print(f"❌ Error: Could not validate credentials.json: {e}")
|
|
298
|
+
return False
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _validate_matrix_authentication(config_path, matrix_section):
|
|
302
|
+
"""
|
|
303
|
+
Determine whether Matrix authentication is configured and usable.
|
|
304
|
+
|
|
305
|
+
Checks for a valid credentials.json (located relative to the provided config path) and, if not present,
|
|
306
|
+
falls back to an access_token in the provided matrix_section. Returns True when authentication
|
|
307
|
+
information is found and usable; returns False when no authentication is configured.
|
|
308
|
+
|
|
309
|
+
Parameters:
|
|
310
|
+
config_path (str | os.PathLike): Path to the application's YAML config file; used to locate a
|
|
311
|
+
credentials.json candidate in the same directory or standard locations.
|
|
312
|
+
matrix_section (Mapping | None): The parsed "matrix" configuration section (mapping-like). If
|
|
313
|
+
provided, an "access_token" key will be considered as a valid fallback when credentials.json
|
|
314
|
+
is absent.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
bool: True if a valid authentication method (credentials.json or access_token) is available,
|
|
318
|
+
False otherwise.
|
|
319
|
+
|
|
320
|
+
Notes:
|
|
321
|
+
- The function prefers credentials.json over an access_token if both are present.
|
|
322
|
+
- The function emits user-facing status messages describing which authentication source is used
|
|
323
|
+
and whether E2EE support is available.
|
|
324
|
+
"""
|
|
325
|
+
has_valid_credentials = _validate_credentials_json(config_path)
|
|
326
|
+
has_access_token = matrix_section and "access_token" in matrix_section
|
|
327
|
+
|
|
328
|
+
if has_valid_credentials:
|
|
329
|
+
print("✅ Using credentials.json for Matrix authentication")
|
|
330
|
+
if sys.platform != WINDOWS_PLATFORM:
|
|
331
|
+
print(" E2EE support available (if enabled)")
|
|
332
|
+
return True
|
|
333
|
+
|
|
334
|
+
elif has_access_token:
|
|
335
|
+
print("✅ Using access_token for Matrix authentication")
|
|
336
|
+
print(f" {msg_for_e2ee_support()}")
|
|
337
|
+
return True
|
|
338
|
+
|
|
339
|
+
else:
|
|
340
|
+
print("❌ Error: No Matrix authentication configured")
|
|
341
|
+
print(f" {msg_setup_auth()}")
|
|
342
|
+
return False
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _validate_e2ee_config(config, matrix_section, config_path):
|
|
346
|
+
"""
|
|
347
|
+
Validate end-to-end encryption (E2EE) configuration and Matrix authentication for the given config.
|
|
348
|
+
|
|
349
|
+
Performs these checks:
|
|
350
|
+
- Confirms Matrix authentication is available (via credentials.json or matrix access token); returns False if authentication is missing or invalid.
|
|
351
|
+
- If no matrix section is present, treats E2EE as not configured and returns True.
|
|
352
|
+
- If E2EE/encryption is enabled in the matrix config, verifies platform/dependency support and inspects the configured store path. If the store directory does not yet exist, a note is printed indicating it will be created.
|
|
353
|
+
|
|
354
|
+
Parameters:
|
|
355
|
+
config_path (str): Path to the active configuration file (used to locate credentials.json and related auth artifacts).
|
|
356
|
+
matrix_section (dict | None): The "matrix" subsection of the parsed config (may be None or empty).
|
|
357
|
+
config (dict): Full parsed configuration (unused for most checks but kept for consistency with caller signature).
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
bool: True if configuration and required authentication/dependencies are valid (or E2EE is not configured); False if validation fails.
|
|
361
|
+
|
|
362
|
+
Side effects:
|
|
363
|
+
Prints informational or error messages about authentication, dependency checks, and E2EE store path status.
|
|
364
|
+
"""
|
|
365
|
+
# First validate authentication
|
|
366
|
+
if not _validate_matrix_authentication(config_path, matrix_section):
|
|
367
|
+
return False
|
|
368
|
+
|
|
369
|
+
# Check for E2EE configuration
|
|
370
|
+
if not matrix_section:
|
|
371
|
+
return True # No matrix section means no E2EE config to validate
|
|
372
|
+
|
|
373
|
+
e2ee_config = matrix_section.get("e2ee", {})
|
|
374
|
+
encryption_config = matrix_section.get("encryption", {}) # Legacy support
|
|
375
|
+
|
|
376
|
+
e2ee_enabled = e2ee_config.get("enabled", False) or encryption_config.get(
|
|
377
|
+
"enabled", False
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
if e2ee_enabled:
|
|
381
|
+
# Platform and dependency check
|
|
382
|
+
if not _validate_e2ee_dependencies():
|
|
383
|
+
return False
|
|
384
|
+
|
|
385
|
+
# Store path validation
|
|
386
|
+
store_path = e2ee_config.get("store_path") or encryption_config.get(
|
|
387
|
+
"store_path"
|
|
388
|
+
)
|
|
389
|
+
if store_path:
|
|
390
|
+
expanded_path = os.path.expanduser(store_path)
|
|
391
|
+
if not os.path.exists(os.path.dirname(expanded_path)):
|
|
392
|
+
print(f"ℹ️ Note: E2EE store directory will be created: {expanded_path}")
|
|
393
|
+
|
|
394
|
+
print("✅ E2EE configuration is valid")
|
|
395
|
+
|
|
396
|
+
return True
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _analyze_e2ee_setup(config, config_path):
|
|
400
|
+
"""
|
|
401
|
+
Analyze local E2EE readiness without contacting Matrix.
|
|
402
|
+
|
|
403
|
+
Performs an offline inspection of the environment and configuration to determine
|
|
404
|
+
whether end-to-end encryption (E2EE) can be used. Checks platform support
|
|
405
|
+
(Windows is considered unsupported), presence of required Python dependencies
|
|
406
|
+
(olm and selected nio components), whether E2EE is enabled in the provided
|
|
407
|
+
config, and whether a credentials.json is available adjacent to the supplied
|
|
408
|
+
config_path or in the standard base directory.
|
|
409
|
+
|
|
410
|
+
Parameters:
|
|
411
|
+
config (dict): Parsed configuration (typically from config.yaml). Only the
|
|
412
|
+
"matrix" section is consulted to detect E2EE/encryption enablement.
|
|
413
|
+
config_path (str): Path to the configuration file used to locate a
|
|
414
|
+
credentials.json sibling; also used to resolve an alternate standard
|
|
415
|
+
credentials location.
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
dict: Analysis summary with these keys:
|
|
419
|
+
- config_enabled (bool): True if E2EE/encryption is enabled in config.
|
|
420
|
+
- dependencies_available (bool): True if required E2EE packages are
|
|
421
|
+
importable.
|
|
422
|
+
- credentials_available (bool): True if a usable credentials.json was
|
|
423
|
+
found.
|
|
424
|
+
- platform_supported (bool): False on unsupported platforms (Windows).
|
|
425
|
+
- overall_status (str): One of "ready", "disabled", "not_supported",
|
|
426
|
+
"incomplete", or "unknown" describing the combined readiness.
|
|
427
|
+
- recommendations (list): Human-actionable strings suggesting fixes or
|
|
428
|
+
next steps (e.g., enable E2EE in config, install dependencies, run
|
|
429
|
+
auth login).
|
|
430
|
+
"""
|
|
431
|
+
analysis = {
|
|
432
|
+
"config_enabled": False,
|
|
433
|
+
"dependencies_available": False,
|
|
434
|
+
"credentials_available": False,
|
|
435
|
+
"platform_supported": True,
|
|
436
|
+
"overall_status": "unknown",
|
|
437
|
+
"recommendations": [],
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
# Check platform support
|
|
441
|
+
if sys.platform == WINDOWS_PLATFORM:
|
|
442
|
+
analysis["platform_supported"] = False
|
|
443
|
+
analysis["recommendations"].append(
|
|
444
|
+
"E2EE is not supported on Windows. Use Linux/macOS for E2EE support."
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
# Check dependencies
|
|
448
|
+
try:
|
|
449
|
+
import olm # noqa: F401
|
|
450
|
+
from nio.crypto import OlmDevice # noqa: F401
|
|
451
|
+
from nio.store import SqliteStore # noqa: F401
|
|
452
|
+
|
|
453
|
+
analysis["dependencies_available"] = True
|
|
454
|
+
except ImportError:
|
|
455
|
+
analysis["dependencies_available"] = False
|
|
456
|
+
analysis["recommendations"].append(
|
|
457
|
+
"Install E2EE dependencies: pipx install 'mmrelay[e2e]'"
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
# Check config setting
|
|
461
|
+
matrix_section = config.get("matrix", {})
|
|
462
|
+
e2ee_config = matrix_section.get("e2ee", {})
|
|
463
|
+
encryption_config = matrix_section.get("encryption", {}) # Legacy support
|
|
464
|
+
analysis["config_enabled"] = e2ee_config.get(
|
|
465
|
+
"enabled", False
|
|
466
|
+
) or encryption_config.get("enabled", False)
|
|
467
|
+
|
|
468
|
+
if not analysis["config_enabled"]:
|
|
469
|
+
analysis["recommendations"].append(
|
|
470
|
+
"Enable E2EE in config.yaml under matrix section: e2ee: enabled: true"
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
# Check credentials (same logic as _validate_credentials_json)
|
|
474
|
+
config_dir = os.path.dirname(config_path)
|
|
475
|
+
credentials_path = os.path.join(config_dir, "credentials.json")
|
|
476
|
+
|
|
477
|
+
if not os.path.exists(credentials_path):
|
|
478
|
+
# Also try the standard location
|
|
479
|
+
from mmrelay.config import get_base_dir
|
|
480
|
+
|
|
481
|
+
standard_credentials_path = os.path.join(get_base_dir(), "credentials.json")
|
|
482
|
+
if os.path.exists(standard_credentials_path):
|
|
483
|
+
credentials_path = standard_credentials_path
|
|
484
|
+
|
|
485
|
+
analysis["credentials_available"] = os.path.exists(credentials_path)
|
|
486
|
+
|
|
487
|
+
if not analysis["credentials_available"]:
|
|
488
|
+
analysis["recommendations"].append(
|
|
489
|
+
"Set up Matrix authentication: mmrelay auth login"
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
# Determine overall status based on setup only
|
|
493
|
+
if not analysis["platform_supported"]:
|
|
494
|
+
analysis["overall_status"] = "not_supported"
|
|
495
|
+
elif (
|
|
496
|
+
analysis["config_enabled"]
|
|
497
|
+
and analysis["dependencies_available"]
|
|
498
|
+
and analysis["credentials_available"]
|
|
499
|
+
):
|
|
500
|
+
analysis["overall_status"] = "ready"
|
|
501
|
+
elif not analysis["config_enabled"]:
|
|
502
|
+
analysis["overall_status"] = "disabled"
|
|
503
|
+
else:
|
|
504
|
+
analysis["overall_status"] = "incomplete"
|
|
505
|
+
|
|
506
|
+
return analysis
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def _print_unified_e2ee_analysis(e2ee_status):
|
|
510
|
+
"""
|
|
511
|
+
Print a concise, user-facing analysis of E2EE readiness from a centralized status object.
|
|
512
|
+
|
|
513
|
+
This formats and prints the platform support, dependency availability, configuration enabled state,
|
|
514
|
+
authentication (credentials.json) presence, and the overall status. If the overall status is not
|
|
515
|
+
"ready", prints actionable fix instructions obtained from mmrelay.e2ee_utils.get_e2ee_fix_instructions.
|
|
516
|
+
|
|
517
|
+
Parameters:
|
|
518
|
+
e2ee_status (dict): Status dictionary as returned by get_e2ee_status(config, config_path).
|
|
519
|
+
Expected keys:
|
|
520
|
+
- platform_supported (bool)
|
|
521
|
+
- dependencies_installed (bool)
|
|
522
|
+
- enabled (bool)
|
|
523
|
+
- credentials_available (bool)
|
|
524
|
+
- overall_status (str)
|
|
525
|
+
"""
|
|
526
|
+
print("\n🔐 E2EE Configuration Analysis:")
|
|
527
|
+
|
|
528
|
+
# Platform support
|
|
529
|
+
if e2ee_status["platform_supported"]:
|
|
530
|
+
print("✅ Platform: E2EE supported")
|
|
531
|
+
else:
|
|
532
|
+
print("❌ Platform: E2EE not supported on Windows")
|
|
533
|
+
|
|
534
|
+
# Dependencies
|
|
535
|
+
if e2ee_status["dependencies_installed"]:
|
|
536
|
+
print("✅ Dependencies: E2EE dependencies installed")
|
|
537
|
+
else:
|
|
538
|
+
print("❌ Dependencies: E2EE dependencies not fully installed")
|
|
539
|
+
|
|
540
|
+
# Configuration
|
|
541
|
+
if e2ee_status["enabled"]:
|
|
542
|
+
print("✅ Configuration: E2EE enabled")
|
|
543
|
+
else:
|
|
544
|
+
print("❌ Configuration: E2EE disabled")
|
|
545
|
+
|
|
546
|
+
# Authentication
|
|
547
|
+
if e2ee_status["credentials_available"]:
|
|
548
|
+
print("✅ Authentication: credentials.json found")
|
|
549
|
+
else:
|
|
550
|
+
print("❌ Authentication: credentials.json not found")
|
|
551
|
+
|
|
552
|
+
# Overall status
|
|
553
|
+
print(f"\n📊 Overall Status: {e2ee_status['overall_status'].upper()}")
|
|
554
|
+
|
|
555
|
+
# Show fix instructions if needed
|
|
556
|
+
if e2ee_status["overall_status"] != "ready":
|
|
557
|
+
from mmrelay.e2ee_utils import get_e2ee_fix_instructions
|
|
558
|
+
|
|
559
|
+
instructions = get_e2ee_fix_instructions(e2ee_status)
|
|
560
|
+
print("\n🔧 To fix E2EE issues:")
|
|
561
|
+
for instruction in instructions:
|
|
562
|
+
print(f" {instruction}")
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def _print_e2ee_analysis(analysis):
|
|
566
|
+
"""
|
|
567
|
+
Print a user-facing analysis of end-to-end encryption (E2EE) readiness to standard output.
|
|
568
|
+
|
|
569
|
+
Parameters:
|
|
570
|
+
analysis (dict): Analysis results with the following keys:
|
|
571
|
+
- dependencies_available (bool): True if required E2EE dependencies (e.g., python-olm) are present.
|
|
572
|
+
- credentials_available (bool): True if a usable credentials.json was found.
|
|
573
|
+
- platform_supported (bool): True if the current platform supports E2EE (Windows is considered unsupported).
|
|
574
|
+
- config_enabled (bool): True if E2EE is enabled in the configuration.
|
|
575
|
+
- overall_status (str): One of "ready", "disabled", "not_supported", or "incomplete" indicating the aggregated readiness.
|
|
576
|
+
- recommendations (list[str]): User-facing remediation steps or suggestions (may be empty).
|
|
577
|
+
|
|
578
|
+
Returns:
|
|
579
|
+
None
|
|
580
|
+
|
|
581
|
+
Notes:
|
|
582
|
+
- This function only prints a human-readable report and does not modify state.
|
|
583
|
+
"""
|
|
584
|
+
print("\n🔐 E2EE Configuration Analysis:")
|
|
585
|
+
|
|
586
|
+
# Current settings
|
|
587
|
+
print("\n📋 Current Settings:")
|
|
588
|
+
|
|
589
|
+
# Dependencies
|
|
590
|
+
if analysis["dependencies_available"]:
|
|
591
|
+
print(" ✅ Dependencies: Installed (python-olm available)")
|
|
592
|
+
else:
|
|
593
|
+
print(" ❌ Dependencies: Missing (python-olm not installed)")
|
|
594
|
+
|
|
595
|
+
# Credentials
|
|
596
|
+
if analysis["credentials_available"]:
|
|
597
|
+
print(" ✅ Authentication: Ready (credentials.json found)")
|
|
598
|
+
else:
|
|
599
|
+
print(" ❌ Authentication: Missing (no credentials.json)")
|
|
600
|
+
|
|
601
|
+
# Platform
|
|
602
|
+
if not analysis["platform_supported"]:
|
|
603
|
+
print(" ❌ Platform: Windows (E2EE not supported)")
|
|
604
|
+
else:
|
|
605
|
+
print(" ✅ Platform: Supported")
|
|
606
|
+
|
|
607
|
+
# Config setting
|
|
608
|
+
if analysis["config_enabled"]:
|
|
609
|
+
print(" ✅ Configuration: ENABLED (e2ee.enabled: true)")
|
|
610
|
+
else:
|
|
611
|
+
print(" ❌ Configuration: DISABLED (e2ee.enabled: false)")
|
|
612
|
+
|
|
613
|
+
# Predicted behavior
|
|
614
|
+
print("\n🚨 PREDICTED BEHAVIOR:")
|
|
615
|
+
if analysis["overall_status"] == "ready":
|
|
616
|
+
print(" ✅ E2EE is fully configured and ready")
|
|
617
|
+
print(" ✅ Encrypted rooms will receive encrypted messages")
|
|
618
|
+
print(" ✅ Unencrypted rooms will receive normal messages")
|
|
619
|
+
elif analysis["overall_status"] == "disabled":
|
|
620
|
+
print(" ⚠️ E2EE is disabled in configuration")
|
|
621
|
+
print(" ❌ Messages to encrypted rooms will be BLOCKED")
|
|
622
|
+
print(" ✅ Messages to unencrypted rooms will work normally")
|
|
623
|
+
elif analysis["overall_status"] == "not_supported":
|
|
624
|
+
print(" ❌ E2EE not supported on Windows")
|
|
625
|
+
print(" ❌ Messages to encrypted rooms will be BLOCKED")
|
|
626
|
+
else:
|
|
627
|
+
print(" ⚠️ E2EE setup incomplete - some issues need to be resolved")
|
|
628
|
+
print(" ❌ Messages to encrypted rooms may be BLOCKED")
|
|
629
|
+
|
|
630
|
+
print(
|
|
631
|
+
"\n💡 Note: Room encryption status will be checked when mmrelay connects to Matrix"
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
# Recommendations
|
|
635
|
+
if analysis["recommendations"]:
|
|
636
|
+
print("\n🔧 TO FIX:")
|
|
637
|
+
for i, rec in enumerate(analysis["recommendations"], 1):
|
|
638
|
+
print(f" {i}. {rec}")
|
|
639
|
+
|
|
640
|
+
if analysis["overall_status"] == "ready":
|
|
641
|
+
print(
|
|
642
|
+
"\n✅ E2EE setup is complete! Run 'mmrelay' to start with E2EE support."
|
|
643
|
+
)
|
|
644
|
+
else:
|
|
645
|
+
print(
|
|
646
|
+
"\n⚠️ After fixing issues above, run 'mmrelay config check' again to verify."
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def _print_environment_summary():
|
|
651
|
+
"""
|
|
652
|
+
Print a concise environment summary including platform, Python version, and Matrix E2EE capability.
|
|
653
|
+
|
|
654
|
+
Provides:
|
|
655
|
+
- Platform and Python version.
|
|
656
|
+
- Whether E2EE is supported on the current platform (Windows is reported as not supported).
|
|
657
|
+
- Whether the `olm` dependency is installed when E2EE is supported, and a brief installation hint if missing.
|
|
658
|
+
|
|
659
|
+
This function writes human-facing lines to standard output and returns None.
|
|
660
|
+
"""
|
|
661
|
+
print("\n🖥️ Environment Summary:")
|
|
662
|
+
print(f" Platform: {sys.platform}")
|
|
663
|
+
print(f" Python: {sys.version.split()[0]}")
|
|
664
|
+
|
|
665
|
+
# E2EE capability check
|
|
666
|
+
if sys.platform == WINDOWS_PLATFORM:
|
|
667
|
+
print(" E2EE Support: ❌ Not available (Windows limitation)")
|
|
668
|
+
print(" Matrix Support: ✅ Available")
|
|
669
|
+
else:
|
|
670
|
+
try:
|
|
671
|
+
import olm # noqa: F401
|
|
672
|
+
from nio.crypto import OlmDevice # noqa: F401
|
|
673
|
+
from nio.store import SqliteStore # noqa: F401
|
|
674
|
+
|
|
675
|
+
print(" E2EE Support: ✅ Available and installed")
|
|
676
|
+
except ImportError:
|
|
677
|
+
print(" E2EE Support: ⚠️ Available but not installed")
|
|
678
|
+
print(" Install: pipx install 'mmrelay[e2e]'")
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def check_config(args=None):
|
|
682
|
+
"""
|
|
683
|
+
Validate the application's YAML configuration file and its required sections.
|
|
684
|
+
|
|
685
|
+
Performs these checks:
|
|
686
|
+
- Locates the first existing config file from get_config_paths(args) (parses CLI args if args is None).
|
|
687
|
+
- Verifies YAML syntax and reports syntax errors or style warnings.
|
|
688
|
+
- Ensures the config is non-empty.
|
|
689
|
+
- Validates Matrix authentication: accepts credentials supplied via credentials.json or requires a matrix section with homeserver, access_token, and bot_user_id when credentials.json is absent.
|
|
690
|
+
- Validates end-to-end-encryption (E2EE) configuration and dependencies.
|
|
691
|
+
- Ensures matrix_rooms exists, is a non-empty list, and each room is a dict containing an id.
|
|
692
|
+
- Validates the meshtastic section: requires connection_type and the connection-specific fields (serial_port for serial, host for tcp/network, ble_address for ble). Warns about deprecated connection types.
|
|
693
|
+
- Validates optional meshtastic fields and types (broadcast_enabled, detection_sensor, message_delay >= 2.0, meshnet_name) and reports missing optional settings as guidance.
|
|
694
|
+
- Warns if a deprecated db section is present.
|
|
695
|
+
- Prints a short environment summary on success.
|
|
696
|
+
|
|
697
|
+
Side effects:
|
|
698
|
+
- Prints errors, warnings, and status messages to stdout.
|
|
699
|
+
|
|
700
|
+
Parameters:
|
|
701
|
+
args (argparse.Namespace | None): Parsed CLI arguments. If None, CLI arguments will be parsed internally.
|
|
702
|
+
|
|
703
|
+
Returns:
|
|
704
|
+
bool: True if a configuration file was found and passed all checks; False otherwise.
|
|
115
705
|
"""
|
|
116
|
-
from mmrelay.config import get_config_paths
|
|
117
706
|
|
|
118
707
|
# If args is None, parse them now
|
|
119
708
|
if args is None:
|
|
@@ -129,20 +718,56 @@ def check_config(args=None):
|
|
|
129
718
|
print(f"Found configuration file at: {config_path}")
|
|
130
719
|
try:
|
|
131
720
|
with open(config_path, "r") as f:
|
|
132
|
-
|
|
721
|
+
config_content = f.read()
|
|
722
|
+
|
|
723
|
+
# Validate YAML syntax first
|
|
724
|
+
is_valid, message, config = validate_yaml_syntax(
|
|
725
|
+
config_content, config_path
|
|
726
|
+
)
|
|
727
|
+
if not is_valid:
|
|
728
|
+
print(f"YAML Syntax Error:\n{message}")
|
|
729
|
+
return False
|
|
730
|
+
elif message: # Warnings
|
|
731
|
+
print(f"YAML Style Warnings:\n{message}\n")
|
|
133
732
|
|
|
134
733
|
# Check if config is empty
|
|
135
734
|
if not config:
|
|
136
|
-
print(
|
|
735
|
+
print(
|
|
736
|
+
"Error: Configuration file is empty or contains only comments"
|
|
737
|
+
)
|
|
137
738
|
return False
|
|
138
739
|
|
|
139
|
-
# Check
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
740
|
+
# Check if we have valid credentials.json first
|
|
741
|
+
has_valid_credentials = _validate_credentials_json(config_path)
|
|
742
|
+
|
|
743
|
+
# Check matrix section requirements based on credentials.json availability
|
|
744
|
+
if has_valid_credentials:
|
|
745
|
+
# With credentials.json, no matrix section fields are required
|
|
746
|
+
# (homeserver, access_token, user_id, device_id all come from credentials.json)
|
|
747
|
+
if CONFIG_SECTION_MATRIX not in config:
|
|
748
|
+
# Create empty matrix section if missing - no fields required
|
|
749
|
+
config[CONFIG_SECTION_MATRIX] = {}
|
|
750
|
+
matrix_section = config[CONFIG_SECTION_MATRIX]
|
|
751
|
+
required_matrix_fields = (
|
|
752
|
+
[]
|
|
753
|
+
) # No fields required from config when using credentials.json
|
|
754
|
+
else:
|
|
755
|
+
# Without credentials.json, require full matrix section
|
|
756
|
+
if CONFIG_SECTION_MATRIX not in config:
|
|
757
|
+
print("Error: Missing 'matrix' section in config")
|
|
758
|
+
print(
|
|
759
|
+
" Either add matrix section with access_token and bot_user_id,"
|
|
760
|
+
)
|
|
761
|
+
print(f" {msg_or_run_auth_login()}")
|
|
762
|
+
return False
|
|
763
|
+
|
|
764
|
+
matrix_section = config[CONFIG_SECTION_MATRIX]
|
|
765
|
+
required_matrix_fields = [
|
|
766
|
+
CONFIG_KEY_HOMESERVER,
|
|
767
|
+
CONFIG_KEY_ACCESS_TOKEN,
|
|
768
|
+
CONFIG_KEY_BOT_USER_ID,
|
|
769
|
+
]
|
|
143
770
|
|
|
144
|
-
matrix_section = config["matrix"]
|
|
145
|
-
required_matrix_fields = ["homeserver", "access_token", "bot_user_id"]
|
|
146
771
|
missing_matrix_fields = [
|
|
147
772
|
field
|
|
148
773
|
for field in required_matrix_fields
|
|
@@ -150,11 +775,39 @@ def check_config(args=None):
|
|
|
150
775
|
]
|
|
151
776
|
|
|
152
777
|
if missing_matrix_fields:
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
778
|
+
if has_valid_credentials:
|
|
779
|
+
print(
|
|
780
|
+
f"Error: Missing required fields in 'matrix' section: {', '.join(missing_matrix_fields)}"
|
|
781
|
+
)
|
|
782
|
+
print(
|
|
783
|
+
" Note: credentials.json provides authentication; no matrix.* fields are required in config"
|
|
784
|
+
)
|
|
785
|
+
else:
|
|
786
|
+
print(
|
|
787
|
+
f"Error: Missing required fields in 'matrix' section: {', '.join(missing_matrix_fields)}"
|
|
788
|
+
)
|
|
789
|
+
print(f" {msg_setup_authentication()}")
|
|
156
790
|
return False
|
|
157
791
|
|
|
792
|
+
# Perform comprehensive E2EE analysis using centralized utilities
|
|
793
|
+
try:
|
|
794
|
+
from mmrelay.e2ee_utils import (
|
|
795
|
+
get_e2ee_status,
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
e2ee_status = get_e2ee_status(config, config_path)
|
|
799
|
+
_print_unified_e2ee_analysis(e2ee_status)
|
|
800
|
+
|
|
801
|
+
# Check if there are critical E2EE issues
|
|
802
|
+
if not e2ee_status.get("platform_supported", True):
|
|
803
|
+
print("\n⚠️ Warning: E2EE is not supported on Windows")
|
|
804
|
+
print(" Messages to encrypted rooms will be blocked")
|
|
805
|
+
except Exception as e:
|
|
806
|
+
print(f"\n⚠️ Could not perform E2EE analysis: {e}")
|
|
807
|
+
print(" Falling back to basic E2EE validation...")
|
|
808
|
+
if not _validate_e2ee_config(config, matrix_section, config_path):
|
|
809
|
+
return False
|
|
810
|
+
|
|
158
811
|
# Check matrix_rooms section
|
|
159
812
|
if "matrix_rooms" not in config or not config["matrix_rooms"]:
|
|
160
813
|
print("Error: Missing or empty 'matrix_rooms' section in config")
|
|
@@ -178,64 +831,126 @@ def check_config(args=None):
|
|
|
178
831
|
return False
|
|
179
832
|
|
|
180
833
|
# Check meshtastic section
|
|
181
|
-
if
|
|
834
|
+
if CONFIG_SECTION_MESHTASTIC not in config:
|
|
182
835
|
print("Error: Missing 'meshtastic' section in config")
|
|
183
836
|
return False
|
|
184
837
|
|
|
185
|
-
meshtastic_section = config[
|
|
838
|
+
meshtastic_section = config[CONFIG_SECTION_MESHTASTIC]
|
|
186
839
|
if "connection_type" not in meshtastic_section:
|
|
187
840
|
print("Error: Missing 'connection_type' in 'meshtastic' section")
|
|
188
841
|
return False
|
|
189
842
|
|
|
190
|
-
connection_type = meshtastic_section[
|
|
191
|
-
if connection_type not in [
|
|
843
|
+
connection_type = meshtastic_section[CONFIG_KEY_CONNECTION_TYPE]
|
|
844
|
+
if connection_type not in [
|
|
845
|
+
CONNECTION_TYPE_TCP,
|
|
846
|
+
CONNECTION_TYPE_SERIAL,
|
|
847
|
+
CONNECTION_TYPE_BLE,
|
|
848
|
+
CONNECTION_TYPE_NETWORK,
|
|
849
|
+
]:
|
|
192
850
|
print(
|
|
193
|
-
f"Error: Invalid 'connection_type': {connection_type}. Must be
|
|
851
|
+
f"Error: Invalid 'connection_type': {connection_type}. Must be "
|
|
852
|
+
f"'{CONNECTION_TYPE_TCP}', '{CONNECTION_TYPE_SERIAL}', '{CONNECTION_TYPE_BLE}'"
|
|
853
|
+
f" or '{CONNECTION_TYPE_NETWORK}' (deprecated)"
|
|
194
854
|
)
|
|
195
855
|
return False
|
|
196
856
|
|
|
197
857
|
# Check for deprecated connection_type
|
|
198
|
-
if connection_type ==
|
|
858
|
+
if connection_type == CONNECTION_TYPE_NETWORK:
|
|
199
859
|
print(
|
|
200
860
|
"\nWarning: 'network' connection_type is deprecated. Please use 'tcp' instead."
|
|
201
861
|
)
|
|
202
862
|
print(
|
|
203
|
-
"
|
|
863
|
+
"This option still works but may be removed in future versions.\n"
|
|
204
864
|
)
|
|
205
865
|
|
|
206
866
|
# Check connection-specific fields
|
|
207
867
|
if (
|
|
208
|
-
connection_type ==
|
|
209
|
-
and
|
|
868
|
+
connection_type == CONNECTION_TYPE_SERIAL
|
|
869
|
+
and CONFIG_KEY_SERIAL_PORT not in meshtastic_section
|
|
210
870
|
):
|
|
211
871
|
print("Error: Missing 'serial_port' for 'serial' connection type")
|
|
212
872
|
return False
|
|
213
873
|
|
|
214
874
|
if (
|
|
215
|
-
connection_type in [
|
|
216
|
-
and
|
|
875
|
+
connection_type in [CONNECTION_TYPE_TCP, CONNECTION_TYPE_NETWORK]
|
|
876
|
+
and CONFIG_KEY_HOST not in meshtastic_section
|
|
217
877
|
):
|
|
218
878
|
print("Error: Missing 'host' for 'tcp' connection type")
|
|
219
879
|
return False
|
|
220
880
|
|
|
221
|
-
if
|
|
881
|
+
if (
|
|
882
|
+
connection_type == CONNECTION_TYPE_BLE
|
|
883
|
+
and CONFIG_KEY_BLE_ADDRESS not in meshtastic_section
|
|
884
|
+
):
|
|
222
885
|
print("Error: Missing 'ble_address' for 'ble' connection type")
|
|
223
886
|
return False
|
|
224
887
|
|
|
888
|
+
# Check for other important optional configurations and provide guidance
|
|
889
|
+
optional_configs = {
|
|
890
|
+
"broadcast_enabled": {
|
|
891
|
+
"type": bool,
|
|
892
|
+
"description": "Enable Matrix to Meshtastic message forwarding (required for two-way communication)",
|
|
893
|
+
},
|
|
894
|
+
"detection_sensor": {
|
|
895
|
+
"type": bool,
|
|
896
|
+
"description": "Enable forwarding of Meshtastic detection sensor messages",
|
|
897
|
+
},
|
|
898
|
+
"message_delay": {
|
|
899
|
+
"type": (int, float),
|
|
900
|
+
"description": "Delay in seconds between messages sent to mesh (minimum: 2.0)",
|
|
901
|
+
},
|
|
902
|
+
"meshnet_name": {
|
|
903
|
+
"type": str,
|
|
904
|
+
"description": "Name displayed for your meshnet in Matrix messages",
|
|
905
|
+
},
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
warnings = []
|
|
909
|
+
for option, config_info in optional_configs.items():
|
|
910
|
+
if option in meshtastic_section:
|
|
911
|
+
value = meshtastic_section[option]
|
|
912
|
+
expected_type = config_info["type"]
|
|
913
|
+
if not isinstance(value, expected_type):
|
|
914
|
+
if isinstance(expected_type, tuple):
|
|
915
|
+
type_name = " or ".join(
|
|
916
|
+
t.__name__ for t in expected_type
|
|
917
|
+
)
|
|
918
|
+
else:
|
|
919
|
+
type_name = (
|
|
920
|
+
expected_type.__name__
|
|
921
|
+
if hasattr(expected_type, "__name__")
|
|
922
|
+
else str(expected_type)
|
|
923
|
+
)
|
|
924
|
+
print(
|
|
925
|
+
f"Error: '{option}' must be of type {type_name}, got: {value}"
|
|
926
|
+
)
|
|
927
|
+
return False
|
|
928
|
+
|
|
929
|
+
# Special validation for message_delay
|
|
930
|
+
if option == "message_delay" and value < 2.0:
|
|
931
|
+
print(
|
|
932
|
+
f"Error: 'message_delay' must be at least 2.0 seconds (firmware limitation), got: {value}"
|
|
933
|
+
)
|
|
934
|
+
return False
|
|
935
|
+
else:
|
|
936
|
+
warnings.append(f" - {option}: {config_info['description']}")
|
|
937
|
+
|
|
938
|
+
if warnings:
|
|
939
|
+
print("\nOptional configurations not found (using defaults):")
|
|
940
|
+
for warning in warnings:
|
|
941
|
+
print(warning)
|
|
942
|
+
|
|
225
943
|
# Check for deprecated db section
|
|
226
944
|
if "db" in config:
|
|
227
945
|
print(
|
|
228
946
|
"\nWarning: 'db' section is deprecated. Please use 'database' instead."
|
|
229
947
|
)
|
|
230
948
|
print(
|
|
231
|
-
"
|
|
949
|
+
"This option still works but may be removed in future versions.\n"
|
|
232
950
|
)
|
|
233
951
|
|
|
234
|
-
print("Configuration file is valid!")
|
|
952
|
+
print("\n✅ Configuration file is valid!")
|
|
235
953
|
return True
|
|
236
|
-
except yaml.YAMLError as e:
|
|
237
|
-
print(f"Error parsing YAML in {config_path}: {e}")
|
|
238
|
-
return False
|
|
239
954
|
except Exception as e:
|
|
240
955
|
print(f"Error checking configuration: {e}")
|
|
241
956
|
return False
|
|
@@ -243,41 +958,295 @@ def check_config(args=None):
|
|
|
243
958
|
print("Error: No configuration file found in any of the following locations:")
|
|
244
959
|
for path in config_paths:
|
|
245
960
|
print(f" - {path}")
|
|
246
|
-
print("\
|
|
961
|
+
print(f"\n{msg_suggest_generate_config()}")
|
|
247
962
|
return False
|
|
248
963
|
|
|
249
964
|
|
|
250
965
|
def main():
|
|
251
|
-
"""
|
|
966
|
+
"""
|
|
967
|
+
Entry point for the MMRelay command-line interface; parses arguments, dispatches commands, and returns an appropriate process exit code.
|
|
968
|
+
|
|
969
|
+
This function:
|
|
970
|
+
- Parses CLI arguments (modern grouped subcommands and hidden legacy flags).
|
|
971
|
+
- If a modern subcommand is provided, dispatches to the grouped subcommand handlers.
|
|
972
|
+
- If legacy flags are present, emits deprecation warnings and executes the corresponding legacy behavior (config check/generate, service install, auth, version).
|
|
973
|
+
- If no command flags are present, attempts to run the main runtime.
|
|
974
|
+
- Catches and reports import or unexpected errors and maps success/failure to exit codes.
|
|
252
975
|
|
|
253
976
|
Returns:
|
|
254
|
-
int: Exit code (0
|
|
977
|
+
int: Exit code (0 on success, non-zero on failure).
|
|
255
978
|
"""
|
|
256
|
-
|
|
979
|
+
try:
|
|
980
|
+
args = parse_arguments()
|
|
257
981
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
982
|
+
# Handle subcommands first (modern interface)
|
|
983
|
+
if hasattr(args, "command") and args.command:
|
|
984
|
+
return handle_subcommand(args)
|
|
261
985
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
986
|
+
# Handle legacy flags (with deprecation warnings)
|
|
987
|
+
if args.check_config:
|
|
988
|
+
print(get_deprecation_warning("--check-config"))
|
|
989
|
+
return 0 if check_config(args) else 1
|
|
265
990
|
|
|
266
|
-
|
|
991
|
+
if args.install_service:
|
|
992
|
+
print(get_deprecation_warning("--install-service"))
|
|
993
|
+
try:
|
|
994
|
+
from mmrelay.setup_utils import install_service
|
|
267
995
|
|
|
268
|
-
|
|
269
|
-
|
|
996
|
+
return 0 if install_service() else 1
|
|
997
|
+
except ImportError as e:
|
|
998
|
+
print(f"Error importing setup utilities: {e}")
|
|
999
|
+
return 1
|
|
1000
|
+
|
|
1001
|
+
if args.generate_config:
|
|
1002
|
+
print(get_deprecation_warning("--generate-config"))
|
|
1003
|
+
return 0 if generate_sample_config() else 1
|
|
1004
|
+
|
|
1005
|
+
if args.version:
|
|
1006
|
+
print_version()
|
|
1007
|
+
return 0
|
|
1008
|
+
|
|
1009
|
+
if args.auth:
|
|
1010
|
+
print(get_deprecation_warning("--auth"))
|
|
1011
|
+
return handle_auth_command(args)
|
|
1012
|
+
|
|
1013
|
+
# If no command was specified, run the main functionality
|
|
1014
|
+
try:
|
|
1015
|
+
from mmrelay.main import run_main
|
|
1016
|
+
|
|
1017
|
+
return run_main(args)
|
|
1018
|
+
except ImportError as e:
|
|
1019
|
+
print(f"Error importing main module: {e}")
|
|
1020
|
+
return 1
|
|
1021
|
+
|
|
1022
|
+
except Exception as e:
|
|
1023
|
+
print(f"Unexpected error: {e}")
|
|
1024
|
+
return 1
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
def handle_subcommand(args):
|
|
1028
|
+
"""
|
|
1029
|
+
Dispatch the modern grouped CLI subcommand to its handler and return an exit code.
|
|
1030
|
+
|
|
1031
|
+
Parameters:
|
|
1032
|
+
args (argparse.Namespace): Parsed CLI arguments (as produced by parse_arguments()). Must have a `command` attribute with one of: "config", "auth", or "service".
|
|
1033
|
+
|
|
1034
|
+
Returns:
|
|
1035
|
+
int: Process exit code — 0 on success, non-zero on error or unknown command.
|
|
1036
|
+
"""
|
|
1037
|
+
if args.command == "config":
|
|
1038
|
+
return handle_config_command(args)
|
|
1039
|
+
elif args.command == "auth":
|
|
1040
|
+
return handle_auth_command(args)
|
|
1041
|
+
elif args.command == "service":
|
|
1042
|
+
return handle_service_command(args)
|
|
1043
|
+
else:
|
|
1044
|
+
print(f"Unknown command: {args.command}")
|
|
1045
|
+
return 1
|
|
1046
|
+
|
|
1047
|
+
|
|
1048
|
+
def handle_config_command(args):
|
|
1049
|
+
"""
|
|
1050
|
+
Dispatch the 'config' subgroup commands: "generate" and "check".
|
|
1051
|
+
|
|
1052
|
+
If `args.config_command` is "generate", writes a sample config to the default location.
|
|
1053
|
+
If "check", validates the configuration referenced by `args` (see check_config).
|
|
1054
|
+
|
|
1055
|
+
Parameters:
|
|
1056
|
+
args (argparse.Namespace): Parsed CLI namespace with a `config_command` attribute.
|
|
1057
|
+
|
|
1058
|
+
Returns:
|
|
1059
|
+
int: Process exit code (0 on success, 1 on failure or unknown subcommand).
|
|
1060
|
+
"""
|
|
1061
|
+
if args.config_command == "generate":
|
|
270
1062
|
return 0 if generate_sample_config() else 1
|
|
1063
|
+
elif args.config_command == "check":
|
|
1064
|
+
return 0 if check_config(args) else 1
|
|
1065
|
+
else:
|
|
1066
|
+
print(f"Unknown config command: {args.config_command}")
|
|
1067
|
+
return 1
|
|
271
1068
|
|
|
272
|
-
# Handle --version
|
|
273
|
-
if args.version:
|
|
274
|
-
print_version()
|
|
275
|
-
return 0
|
|
276
1069
|
|
|
277
|
-
|
|
278
|
-
|
|
1070
|
+
def handle_auth_command(args):
|
|
1071
|
+
"""
|
|
1072
|
+
Dispatch the "auth" CLI subcommand to the appropriate handler.
|
|
279
1073
|
|
|
280
|
-
|
|
1074
|
+
If args.auth_command is "status" calls handle_auth_status; if "logout" calls handle_auth_logout;
|
|
1075
|
+
any other value (or missing attribute) defaults to handle_auth_login.
|
|
1076
|
+
|
|
1077
|
+
Parameters:
|
|
1078
|
+
args (argparse.Namespace): Parsed CLI arguments. Expected to optionally provide `auth_command`
|
|
1079
|
+
with one of "login", "status", or "logout".
|
|
1080
|
+
|
|
1081
|
+
Returns:
|
|
1082
|
+
int: Exit code from the invoked handler (0 = success, non-zero = failure).
|
|
1083
|
+
"""
|
|
1084
|
+
if hasattr(args, "auth_command"):
|
|
1085
|
+
if args.auth_command == "status":
|
|
1086
|
+
return handle_auth_status(args)
|
|
1087
|
+
elif args.auth_command == "logout":
|
|
1088
|
+
return handle_auth_logout(args)
|
|
1089
|
+
else:
|
|
1090
|
+
# Default to login for auth login command
|
|
1091
|
+
return handle_auth_login(args)
|
|
1092
|
+
else:
|
|
1093
|
+
# Default to login for legacy --auth
|
|
1094
|
+
return handle_auth_login(args)
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
def handle_auth_login(args):
|
|
1098
|
+
"""
|
|
1099
|
+
Run the interactive Matrix bot login flow and return a CLI-style exit code.
|
|
1100
|
+
|
|
1101
|
+
Runs the login_matrix_bot coroutine to perform authentication for the Matrix/E2EE bot and prints a short header. Returns 0 on successful authentication; returns 1 if the login fails, is cancelled by the user (KeyboardInterrupt), or an unexpected error occurs.
|
|
1102
|
+
|
|
1103
|
+
Parameters:
|
|
1104
|
+
args: Parsed command-line arguments (not used by this handler).
|
|
1105
|
+
"""
|
|
1106
|
+
import asyncio
|
|
1107
|
+
|
|
1108
|
+
from mmrelay.matrix_utils import login_matrix_bot
|
|
1109
|
+
|
|
1110
|
+
# Show header
|
|
1111
|
+
print("Matrix Bot Authentication for E2EE")
|
|
1112
|
+
print("===================================")
|
|
1113
|
+
|
|
1114
|
+
try:
|
|
1115
|
+
# For now, use the existing login function
|
|
1116
|
+
result = asyncio.run(login_matrix_bot())
|
|
1117
|
+
return 0 if result else 1
|
|
1118
|
+
except KeyboardInterrupt:
|
|
1119
|
+
print("\nAuthentication cancelled by user.")
|
|
1120
|
+
return 1
|
|
1121
|
+
except Exception as e:
|
|
1122
|
+
print(f"\nError during authentication: {e}")
|
|
1123
|
+
return 1
|
|
1124
|
+
|
|
1125
|
+
|
|
1126
|
+
def handle_auth_status(args):
|
|
1127
|
+
"""
|
|
1128
|
+
Show Matrix authentication status by locating and reading a credentials.json file.
|
|
1129
|
+
|
|
1130
|
+
Searches candidate config directories derived from the provided parsed-arguments namespace for a credentials.json file. If found and readable, prints the file path and the homeserver, user_id, and device_id values. If the file is unreadable or not found, prints guidance to run the authentication flow.
|
|
1131
|
+
|
|
1132
|
+
Parameters:
|
|
1133
|
+
args (argparse.Namespace): Parsed CLI arguments used to determine the list of config paths to search.
|
|
1134
|
+
|
|
1135
|
+
Returns:
|
|
1136
|
+
int: Exit code — 0 if a readable credentials.json was found, 1 otherwise.
|
|
1137
|
+
"""
|
|
1138
|
+
import json
|
|
1139
|
+
import os
|
|
1140
|
+
|
|
1141
|
+
from mmrelay.config import get_config_paths
|
|
1142
|
+
|
|
1143
|
+
print("Matrix Authentication Status")
|
|
1144
|
+
print("============================")
|
|
1145
|
+
|
|
1146
|
+
# Check for credentials.json
|
|
1147
|
+
config_paths = get_config_paths(args)
|
|
1148
|
+
for config_path in config_paths:
|
|
1149
|
+
config_dir = os.path.dirname(config_path)
|
|
1150
|
+
credentials_path = os.path.join(config_dir, "credentials.json")
|
|
1151
|
+
if os.path.exists(credentials_path):
|
|
1152
|
+
try:
|
|
1153
|
+
with open(credentials_path, "r") as f:
|
|
1154
|
+
credentials = json.load(f)
|
|
1155
|
+
|
|
1156
|
+
print(f"✅ Found credentials.json at: {credentials_path}")
|
|
1157
|
+
print(f" Homeserver: {credentials.get('homeserver', 'Unknown')}")
|
|
1158
|
+
print(f" User ID: {credentials.get('user_id', 'Unknown')}")
|
|
1159
|
+
print(f" Device ID: {credentials.get('device_id', 'Unknown')}")
|
|
1160
|
+
return 0
|
|
1161
|
+
except Exception as e:
|
|
1162
|
+
print(f"❌ Error reading credentials.json: {e}")
|
|
1163
|
+
return 1
|
|
1164
|
+
|
|
1165
|
+
print("❌ No credentials.json found")
|
|
1166
|
+
print(f"Run '{get_command('auth_login')}' to authenticate")
|
|
1167
|
+
return 1
|
|
1168
|
+
|
|
1169
|
+
|
|
1170
|
+
def handle_auth_logout(args):
|
|
1171
|
+
"""
|
|
1172
|
+
Log out the bot from Matrix and remove local session artifacts.
|
|
1173
|
+
|
|
1174
|
+
Prompts for a verification password (unless provided via args.password), warns if the password was supplied on the command line, asks for confirmation unless args.yes is True, and then performs the logout by calling the logout_matrix_bot routine. On success the function returns 0; on failure or cancellation it returns 1. KeyboardInterrupt is treated as a cancellation and returns 1.
|
|
1175
|
+
|
|
1176
|
+
Parameters:
|
|
1177
|
+
args (argparse.Namespace): CLI arguments. Relevant attributes:
|
|
1178
|
+
password (str | None): If provided and non-empty, used as the verification password.
|
|
1179
|
+
If provided as an empty string or omitted, the function will prompt securely.
|
|
1180
|
+
yes (bool): If True, skip the interactive confirmation prompt.
|
|
1181
|
+
"""
|
|
1182
|
+
import asyncio
|
|
1183
|
+
|
|
1184
|
+
from mmrelay.cli_utils import logout_matrix_bot
|
|
1185
|
+
|
|
1186
|
+
# Show header
|
|
1187
|
+
print("Matrix Bot Logout")
|
|
1188
|
+
print("=================")
|
|
1189
|
+
print()
|
|
1190
|
+
print("This will log out from Matrix and clear all local session data:")
|
|
1191
|
+
print("• Remove credentials.json")
|
|
1192
|
+
print("• Clear E2EE encryption store")
|
|
1193
|
+
print("• Invalidate Matrix access token")
|
|
1194
|
+
print()
|
|
1195
|
+
|
|
1196
|
+
try:
|
|
1197
|
+
# Handle password input
|
|
1198
|
+
password = getattr(args, "password", None)
|
|
1199
|
+
|
|
1200
|
+
if password is None or password == "":
|
|
1201
|
+
# No --password flag or --password with no value, prompt securely
|
|
1202
|
+
import getpass
|
|
1203
|
+
|
|
1204
|
+
password = getpass.getpass("Enter Matrix password for verification: ")
|
|
1205
|
+
else:
|
|
1206
|
+
# --password VALUE provided, warn about security
|
|
1207
|
+
print(
|
|
1208
|
+
"⚠️ Warning: Supplying password as argument exposes it in shell history and process list."
|
|
1209
|
+
)
|
|
1210
|
+
print(
|
|
1211
|
+
" For better security, use --password without a value to prompt securely."
|
|
1212
|
+
)
|
|
1213
|
+
|
|
1214
|
+
# Confirm the action unless forced
|
|
1215
|
+
if not getattr(args, "yes", False):
|
|
1216
|
+
confirm = input("Are you sure you want to logout? (y/N): ").lower().strip()
|
|
1217
|
+
if not confirm.startswith("y"):
|
|
1218
|
+
print("Logout cancelled.")
|
|
1219
|
+
return 0
|
|
1220
|
+
|
|
1221
|
+
# Run the logout process
|
|
1222
|
+
result = asyncio.run(logout_matrix_bot(password=password))
|
|
1223
|
+
return 0 if result else 1
|
|
1224
|
+
except KeyboardInterrupt:
|
|
1225
|
+
print("\nLogout cancelled by user.")
|
|
1226
|
+
return 1
|
|
1227
|
+
except Exception as e:
|
|
1228
|
+
print(f"\nError during logout: {e}")
|
|
1229
|
+
return 1
|
|
1230
|
+
|
|
1231
|
+
|
|
1232
|
+
def handle_service_command(args):
|
|
1233
|
+
"""
|
|
1234
|
+
Handle service-related CLI subcommands.
|
|
1235
|
+
|
|
1236
|
+
Currently supports the "install" subcommand which attempts to import and run mmrelay.setup_utils.install_service.
|
|
1237
|
+
Returns 0 on success, 1 on failure or for unknown subcommands. Prints an error message if setup utilities cannot be imported.
|
|
1238
|
+
"""
|
|
1239
|
+
if args.service_command == "install":
|
|
1240
|
+
try:
|
|
1241
|
+
from mmrelay.setup_utils import install_service
|
|
1242
|
+
|
|
1243
|
+
return 0 if install_service() else 1
|
|
1244
|
+
except ImportError as e:
|
|
1245
|
+
print(f"Error importing setup utilities: {e}")
|
|
1246
|
+
return 1
|
|
1247
|
+
else:
|
|
1248
|
+
print(f"Unknown service command: {args.service_command}")
|
|
1249
|
+
return 1
|
|
281
1250
|
|
|
282
1251
|
|
|
283
1252
|
if __name__ == "__main__":
|
|
@@ -287,14 +1256,18 @@ if __name__ == "__main__":
|
|
|
287
1256
|
|
|
288
1257
|
|
|
289
1258
|
def handle_cli_commands(args):
|
|
290
|
-
"""
|
|
1259
|
+
"""
|
|
1260
|
+
Handle legacy CLI flags (--version, --install-service, --generate-config, --check-config).
|
|
1261
|
+
|
|
1262
|
+
This helper processes backward-compatible flags and may call sys.exit() for flags that perform an immediate action
|
|
1263
|
+
(e.g., install service, check config). Prefer the modern grouped subcommands (e.g., `mmrelay config`, `mmrelay auth`)
|
|
1264
|
+
when available.
|
|
291
1265
|
|
|
292
|
-
|
|
293
|
-
args:
|
|
1266
|
+
Parameters:
|
|
1267
|
+
args (argparse.Namespace): Parsed command-line arguments produced by parse_arguments().
|
|
294
1268
|
|
|
295
1269
|
Returns:
|
|
296
|
-
bool: True if a command was handled
|
|
297
|
-
False if normal execution should continue.
|
|
1270
|
+
bool: True if a legacy command was handled (the process may have already exited), False to continue normal flow.
|
|
298
1271
|
"""
|
|
299
1272
|
# Handle --version
|
|
300
1273
|
if args.version:
|
|
@@ -332,15 +1305,16 @@ def handle_cli_commands(args):
|
|
|
332
1305
|
|
|
333
1306
|
|
|
334
1307
|
def generate_sample_config():
|
|
335
|
-
"""Generate a sample config.yaml file.
|
|
336
|
-
|
|
337
|
-
Returns:
|
|
338
|
-
bool: True if the config was generated successfully, False otherwise.
|
|
339
1308
|
"""
|
|
1309
|
+
Generate a sample configuration file at the highest-priority config path if no config already exists.
|
|
340
1310
|
|
|
341
|
-
|
|
1311
|
+
If an existing config file is found in any candidate path (from get_config_paths()), this function aborts and prints its location. Otherwise it creates a sample config at the first candidate path. Sources tried, in order, are:
|
|
1312
|
+
- the path returned by get_sample_config_path(),
|
|
1313
|
+
- the packaged resource mmrelay.tools:sample_config.yaml via importlib.resources,
|
|
1314
|
+
- a set of fallback filesystem locations relative to the package and current working directory.
|
|
342
1315
|
|
|
343
|
-
|
|
1316
|
+
On success the sample is written to disk and (on Unix-like systems) secure file permissions are applied (owner read/write, 0o600). Returns True when a sample config is successfully generated and False on any error or if a config already exists.
|
|
1317
|
+
"""
|
|
344
1318
|
|
|
345
1319
|
# Get the first config path (highest priority)
|
|
346
1320
|
config_paths = get_config_paths()
|
|
@@ -362,22 +1336,28 @@ def generate_sample_config():
|
|
|
362
1336
|
# No config file exists, generate one in the first location
|
|
363
1337
|
target_path = config_paths[0]
|
|
364
1338
|
|
|
365
|
-
#
|
|
366
|
-
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
|
1339
|
+
# Directory should already exist from get_config_paths() call
|
|
367
1340
|
|
|
368
1341
|
# Use the helper function to get the sample config path
|
|
369
1342
|
sample_config_path = get_sample_config_path()
|
|
370
1343
|
|
|
371
1344
|
if os.path.exists(sample_config_path):
|
|
372
1345
|
# Copy the sample config file to the target path
|
|
373
|
-
import shutil
|
|
374
1346
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
1347
|
+
try:
|
|
1348
|
+
shutil.copy2(sample_config_path, target_path)
|
|
1349
|
+
|
|
1350
|
+
# Set secure permissions on Unix systems (600 - owner read/write)
|
|
1351
|
+
set_secure_file_permissions(target_path)
|
|
1352
|
+
|
|
1353
|
+
print(f"Generated sample config file at: {target_path}")
|
|
1354
|
+
print(
|
|
1355
|
+
"\nEdit this file with your Matrix and Meshtastic settings before running mmrelay."
|
|
1356
|
+
)
|
|
1357
|
+
return True
|
|
1358
|
+
except (IOError, OSError) as e:
|
|
1359
|
+
print(f"Error copying sample config file: {e}")
|
|
1360
|
+
return False
|
|
381
1361
|
|
|
382
1362
|
# If the helper function failed, try using importlib.resources directly
|
|
383
1363
|
try:
|
|
@@ -392,6 +1372,9 @@ def generate_sample_config():
|
|
|
392
1372
|
with open(target_path, "w") as f:
|
|
393
1373
|
f.write(sample_config_content)
|
|
394
1374
|
|
|
1375
|
+
# Set secure permissions on Unix systems (600 - owner read/write)
|
|
1376
|
+
set_secure_file_permissions(target_path)
|
|
1377
|
+
|
|
395
1378
|
print(f"Generated sample config file at: {target_path}")
|
|
396
1379
|
print(
|
|
397
1380
|
"\nEdit this file with your Matrix and Meshtastic settings before running mmrelay."
|
|
@@ -418,12 +1401,16 @@ def generate_sample_config():
|
|
|
418
1401
|
|
|
419
1402
|
for path in sample_config_paths:
|
|
420
1403
|
if os.path.exists(path):
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
1404
|
+
try:
|
|
1405
|
+
shutil.copy(path, target_path)
|
|
1406
|
+
print(f"Generated sample config file at: {target_path}")
|
|
1407
|
+
print(
|
|
1408
|
+
"\nEdit this file with your Matrix and Meshtastic settings before running mmrelay."
|
|
1409
|
+
)
|
|
1410
|
+
return True
|
|
1411
|
+
except (IOError, OSError) as e:
|
|
1412
|
+
print(f"Error copying sample config file from {path}: {e}")
|
|
1413
|
+
return False
|
|
427
1414
|
|
|
428
1415
|
print("Error: Could not find sample_config.yaml")
|
|
429
1416
|
return False
|