mmrelay 1.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 ADDED
@@ -0,0 +1,9 @@
1
+ """
2
+ Meshtastic Matrix Relay - Bridge between Meshtastic mesh networks and Matrix chat rooms.
3
+ """
4
+
5
+ import os
6
+
7
+ # Get version from environment variable if available (set by GitHub Actions)
8
+ # Otherwise, use a default version
9
+ __version__ = os.environ.get("GITHUB_REF_NAME", "1.0.0")
mmrelay/cli.py ADDED
@@ -0,0 +1,384 @@
1
+ """
2
+ Command-line interface handling for the Meshtastic Matrix Relay.
3
+ """
4
+
5
+ import argparse
6
+ import os
7
+ import sys
8
+
9
+ import yaml
10
+ from yaml.loader import SafeLoader
11
+
12
+ # Import version from package
13
+ from mmrelay import __version__
14
+
15
+
16
+ def parse_arguments():
17
+ """
18
+ Parse command-line arguments.
19
+
20
+ Returns:
21
+ argparse.Namespace: The parsed command-line arguments
22
+ """
23
+ parser = argparse.ArgumentParser(
24
+ description="Meshtastic Matrix Relay - Bridge between Meshtastic and Matrix"
25
+ )
26
+ parser.add_argument("--config", help="Path to config file", default=None)
27
+ parser.add_argument(
28
+ "--data-dir",
29
+ help="Base directory for all data (logs, database, plugins)",
30
+ default=None,
31
+ )
32
+ parser.add_argument(
33
+ "--log-level",
34
+ choices=["error", "warning", "info", "debug"],
35
+ help="Set logging level",
36
+ default=None,
37
+ )
38
+ parser.add_argument(
39
+ "--logfile",
40
+ help="Path to log file (can be overridden by --data-dir)",
41
+ default=None,
42
+ )
43
+ parser.add_argument("--version", action="store_true", help="Show version and exit")
44
+ parser.add_argument(
45
+ "--generate-config",
46
+ action="store_true",
47
+ help="Generate a sample config.yaml file",
48
+ )
49
+ parser.add_argument(
50
+ "--install-service",
51
+ action="store_true",
52
+ help="Install or update the systemd user service",
53
+ )
54
+ parser.add_argument(
55
+ "--check-config",
56
+ action="store_true",
57
+ help="Check if the configuration file is valid",
58
+ )
59
+
60
+ # Windows-specific handling for backward compatibility
61
+ # On Windows, add a positional argument for the config file path
62
+ if sys.platform == "win32":
63
+ parser.add_argument(
64
+ "config_path", nargs="?", help=argparse.SUPPRESS, default=None
65
+ )
66
+
67
+ args = parser.parse_args()
68
+
69
+ # If on Windows and a positional config path is provided but --config is not, use the positional one
70
+ if (
71
+ sys.platform == "win32"
72
+ and hasattr(args, "config_path")
73
+ and args.config_path
74
+ and not args.config
75
+ ):
76
+ args.config = args.config_path
77
+ # Print a deprecation warning
78
+ print("Warning: Using positional argument for config file is deprecated.")
79
+ print(f"Please use --config {args.config_path} instead.")
80
+ # Remove the positional argument from sys.argv to avoid issues with other argument parsers
81
+ if args.config_path in sys.argv:
82
+ sys.argv.remove(args.config_path)
83
+
84
+ return args
85
+
86
+
87
+ def get_version():
88
+ """
89
+ Returns the current version of the application.
90
+
91
+ Returns:
92
+ str: The version string
93
+ """
94
+ return __version__
95
+
96
+
97
+ def check_config(args=None):
98
+ """
99
+ Check if the configuration file is valid.
100
+
101
+ Args:
102
+ args: The parsed command-line arguments
103
+
104
+ Returns:
105
+ bool: True if the configuration is valid, False otherwise.
106
+ """
107
+ from mmrelay.config import get_config_paths
108
+
109
+ # If args is None, parse them now
110
+ if args is None:
111
+ args = parse_arguments()
112
+
113
+ config_paths = get_config_paths(args)
114
+ config_path = None
115
+
116
+ # Try each config path in order until we find one that exists
117
+ for path in config_paths:
118
+ if os.path.isfile(path):
119
+ config_path = path
120
+ print(f"Found configuration file at: {config_path}")
121
+ try:
122
+ with open(config_path, "r") as f:
123
+ config = yaml.load(f, Loader=SafeLoader)
124
+
125
+ # Check if config is empty
126
+ if not config:
127
+ print("Error: Configuration file is empty or invalid")
128
+ return False
129
+
130
+ # Check matrix section
131
+ if "matrix" not in config:
132
+ print("Error: Missing 'matrix' section in config")
133
+ return False
134
+
135
+ matrix_section = config["matrix"]
136
+ required_matrix_fields = ["homeserver", "access_token", "bot_user_id"]
137
+ missing_matrix_fields = [
138
+ field
139
+ for field in required_matrix_fields
140
+ if field not in matrix_section
141
+ ]
142
+
143
+ if missing_matrix_fields:
144
+ print(
145
+ f"Error: Missing required fields in 'matrix' section: {', '.join(missing_matrix_fields)}"
146
+ )
147
+ return False
148
+
149
+ # Check matrix_rooms section
150
+ if "matrix_rooms" not in config or not config["matrix_rooms"]:
151
+ print("Error: Missing or empty 'matrix_rooms' section in config")
152
+ return False
153
+
154
+ if not isinstance(config["matrix_rooms"], list):
155
+ print("Error: 'matrix_rooms' must be a list")
156
+ return False
157
+
158
+ for i, room in enumerate(config["matrix_rooms"]):
159
+ if not isinstance(room, dict):
160
+ print(
161
+ f"Error: Room {i+1} in 'matrix_rooms' must be a dictionary"
162
+ )
163
+ return False
164
+
165
+ if "id" not in room:
166
+ print(
167
+ f"Error: Room {i+1} in 'matrix_rooms' is missing the 'id' field"
168
+ )
169
+ return False
170
+
171
+ # Check meshtastic section
172
+ if "meshtastic" not in config:
173
+ print("Error: Missing 'meshtastic' section in config")
174
+ return False
175
+
176
+ meshtastic_section = config["meshtastic"]
177
+ if "connection_type" not in meshtastic_section:
178
+ print("Error: Missing 'connection_type' in 'meshtastic' section")
179
+ return False
180
+
181
+ connection_type = meshtastic_section["connection_type"]
182
+ if connection_type not in ["tcp", "serial", "ble", "network"]:
183
+ print(
184
+ f"Error: Invalid 'connection_type': {connection_type}. Must be 'tcp', 'serial', or 'ble'"
185
+ )
186
+ return False
187
+
188
+ # Check for deprecated connection_type
189
+ if connection_type == "network":
190
+ print(
191
+ "\nWarning: 'network' connection_type is deprecated. Please use 'tcp' instead."
192
+ )
193
+ print(
194
+ "See ANNOUNCEMENT.md for more information about deprecated options.\n"
195
+ )
196
+
197
+ # Check connection-specific fields
198
+ if (
199
+ connection_type == "serial"
200
+ and "serial_port" not in meshtastic_section
201
+ ):
202
+ print("Error: Missing 'serial_port' for 'serial' connection type")
203
+ return False
204
+
205
+ if (
206
+ connection_type in ["tcp", "network"]
207
+ and "host" not in meshtastic_section
208
+ ):
209
+ print("Error: Missing 'host' for 'tcp' connection type")
210
+ return False
211
+
212
+ if connection_type == "ble" and "ble_address" not in meshtastic_section:
213
+ print("Error: Missing 'ble_address' for 'ble' connection type")
214
+ return False
215
+
216
+ # Check for deprecated db section
217
+ if "db" in config:
218
+ print(
219
+ "\nWarning: 'db' section is deprecated. Please use 'database' instead."
220
+ )
221
+ print(
222
+ "See ANNOUNCEMENT.md for more information about deprecated options.\n"
223
+ )
224
+
225
+ print("Configuration file is valid!")
226
+ return True
227
+ except yaml.YAMLError as e:
228
+ print(f"Error parsing YAML in {config_path}: {e}")
229
+ return False
230
+ except Exception as e:
231
+ print(f"Error checking configuration: {e}")
232
+ return False
233
+
234
+ print("Error: No configuration file found in any of the following locations:")
235
+ for path in config_paths:
236
+ print(f" - {path}")
237
+ print("\nRun 'mmrelay --generate-config' to generate a sample configuration file.")
238
+ return False
239
+
240
+
241
+ def main():
242
+ """Entry point for CLI commands.
243
+
244
+ Returns:
245
+ int: Exit code (0 for success, non-zero for failure)
246
+ """
247
+ args = parse_arguments()
248
+
249
+ # Handle --check-config
250
+ if args.check_config:
251
+ return 0 if check_config(args) else 1
252
+
253
+ # Handle --install-service
254
+ if args.install_service:
255
+ from mmrelay.setup_utils import install_service
256
+
257
+ return 0 if install_service() else 1
258
+
259
+ # Handle --generate-config
260
+ if args.generate_config:
261
+ return 0 if generate_sample_config() else 1
262
+
263
+ # Handle --version
264
+ if args.version:
265
+ print(f"mmrelay {get_version()}")
266
+ return 0
267
+
268
+ # If no command was specified, run the main functionality
269
+ from mmrelay.main import run_main
270
+
271
+ return run_main(args)
272
+
273
+
274
+ if __name__ == "__main__":
275
+ import sys
276
+
277
+ sys.exit(main())
278
+
279
+
280
+ def handle_cli_commands(args):
281
+ """Handle CLI commands like --generate-config, --install-service, and --check-config.
282
+
283
+ Args:
284
+ args: The parsed command-line arguments
285
+
286
+ Returns:
287
+ bool: True if a command was handled and the program should exit,
288
+ False if normal execution should continue.
289
+ """
290
+ # Handle --version
291
+ if args.version:
292
+ print(f"mmrelay {get_version()}")
293
+ return True
294
+
295
+ # Handle --install-service
296
+ if args.install_service:
297
+ from mmrelay.setup_utils import install_service
298
+
299
+ success = install_service()
300
+ import sys
301
+
302
+ sys.exit(0 if success else 1)
303
+
304
+ # Handle --generate-config
305
+ if args.generate_config:
306
+ if generate_sample_config():
307
+ # Exit with success if config was generated
308
+ return True
309
+ else:
310
+ # Exit with error if config generation failed
311
+ import sys
312
+
313
+ sys.exit(1)
314
+
315
+ # Handle --check-config
316
+ if args.check_config:
317
+ import sys
318
+
319
+ sys.exit(0 if check_config() else 1)
320
+
321
+ # No commands were handled
322
+ return False
323
+
324
+
325
+ def generate_sample_config():
326
+ """Generate a sample config.yaml file.
327
+
328
+ Returns:
329
+ bool: True if the config was generated successfully, False otherwise.
330
+ """
331
+
332
+ import shutil
333
+
334
+ from mmrelay.config import get_config_paths
335
+
336
+ # Get the first config path (highest priority)
337
+ config_paths = get_config_paths()
338
+
339
+ # Check if any config file exists
340
+ existing_config = None
341
+ for path in config_paths:
342
+ if os.path.isfile(path):
343
+ existing_config = path
344
+ break
345
+
346
+ if existing_config:
347
+ print(f"A config file already exists at: {existing_config}")
348
+ print(
349
+ "Use --config to specify a different location if you want to generate a new one."
350
+ )
351
+ return False
352
+
353
+ # No config file exists, generate one in the first location
354
+ target_path = config_paths[0]
355
+
356
+ # Ensure the directory exists
357
+ os.makedirs(os.path.dirname(target_path), exist_ok=True)
358
+
359
+ # Try to find the sample config file
360
+ # First, check in the package directory
361
+ package_dir = os.path.dirname(__file__)
362
+ sample_config_path = os.path.join(
363
+ os.path.dirname(os.path.dirname(package_dir)), "sample_config.yaml"
364
+ )
365
+
366
+ # If not found, try the repository root
367
+ if not os.path.exists(sample_config_path):
368
+ repo_root = os.path.dirname(os.path.dirname(__file__))
369
+ sample_config_path = os.path.join(repo_root, "sample_config.yaml")
370
+
371
+ # If still not found, try the current directory
372
+ if not os.path.exists(sample_config_path):
373
+ sample_config_path = os.path.join(os.getcwd(), "sample_config.yaml")
374
+
375
+ if os.path.exists(sample_config_path):
376
+ shutil.copy(sample_config_path, target_path)
377
+ print(f"Generated sample config file at: {target_path}")
378
+ print(
379
+ "\nEdit this file with your Matrix and Meshtastic settings before running mmrelay."
380
+ )
381
+ return True
382
+ else:
383
+ print("Error: Could not find sample_config.yaml")
384
+ return False
mmrelay/config.py ADDED
@@ -0,0 +1,218 @@
1
+ import logging
2
+ import os
3
+ import sys
4
+
5
+ import platformdirs
6
+ import yaml
7
+ from yaml.loader import SafeLoader
8
+
9
+ # Define custom base directory for Unix systems
10
+ APP_NAME = "mmrelay"
11
+ APP_AUTHOR = None # No author directory
12
+
13
+
14
+ # Global variable to store the custom data directory
15
+ custom_data_dir = None
16
+
17
+
18
+ # Custom base directory for Unix systems
19
+ def get_base_dir():
20
+ """Returns the base directory for all application files.
21
+
22
+ If a custom data directory has been set via --data-dir, that will be used.
23
+ Otherwise, defaults to ~/.mmrelay on Unix systems or the appropriate
24
+ platformdirs location on Windows.
25
+ """
26
+ # If a custom data directory has been set, use that
27
+ if custom_data_dir:
28
+ return custom_data_dir
29
+
30
+ if sys.platform in ["linux", "darwin"]:
31
+ # Use ~/.mmrelay for Linux and Mac
32
+ return os.path.expanduser(os.path.join("~", "." + APP_NAME))
33
+ else:
34
+ # Use platformdirs default for Windows
35
+ return platformdirs.user_data_dir(APP_NAME, APP_AUTHOR)
36
+
37
+
38
+ def get_app_path():
39
+ """
40
+ Returns the base directory of the application, whether running from source or as an executable.
41
+ """
42
+ if getattr(sys, "frozen", False):
43
+ # Running in a bundle (PyInstaller)
44
+ return os.path.dirname(sys.executable)
45
+ else:
46
+ # Running in a normal Python environment
47
+ return os.path.dirname(os.path.abspath(__file__))
48
+
49
+
50
+ def get_config_paths(args=None):
51
+ """
52
+ Returns a list of possible config file paths in order of priority:
53
+ 1. Command line argument (if provided)
54
+ 2. User config directory (~/.mmrelay/config/ on Linux)
55
+ 3. Current directory (for backward compatibility)
56
+ 4. Application directory (for backward compatibility)
57
+
58
+ Args:
59
+ args: The parsed command-line arguments
60
+ """
61
+ paths = []
62
+
63
+ # Check command line arguments for config path
64
+ if args and args.config:
65
+ paths.append(os.path.abspath(args.config))
66
+
67
+ # Check user config directory (preferred location)
68
+ if sys.platform in ["linux", "darwin"]:
69
+ # Use ~/.mmrelay/ for Linux and Mac
70
+ user_config_dir = get_base_dir()
71
+ else:
72
+ # Use platformdirs default for Windows
73
+ user_config_dir = platformdirs.user_config_dir(APP_NAME, APP_AUTHOR)
74
+
75
+ os.makedirs(user_config_dir, exist_ok=True)
76
+ user_config_path = os.path.join(user_config_dir, "config.yaml")
77
+ paths.append(user_config_path)
78
+
79
+ # Check current directory (for backward compatibility)
80
+ current_dir_config = os.path.join(os.getcwd(), "config.yaml")
81
+ paths.append(current_dir_config)
82
+
83
+ # Check application directory (for backward compatibility)
84
+ app_dir_config = os.path.join(get_app_path(), "config.yaml")
85
+ paths.append(app_dir_config)
86
+
87
+ return paths
88
+
89
+
90
+ def get_data_dir():
91
+ """
92
+ Returns the directory for storing application data files.
93
+ Creates the directory if it doesn't exist.
94
+ """
95
+ if sys.platform in ["linux", "darwin"]:
96
+ # Use ~/.mmrelay/data/ for Linux and Mac
97
+ data_dir = os.path.join(get_base_dir(), "data")
98
+ else:
99
+ # Use platformdirs default for Windows
100
+ data_dir = platformdirs.user_data_dir(APP_NAME, APP_AUTHOR)
101
+
102
+ os.makedirs(data_dir, exist_ok=True)
103
+ return data_dir
104
+
105
+
106
+ def get_log_dir():
107
+ """
108
+ Returns the directory for storing log files.
109
+ Creates the directory if it doesn't exist.
110
+ """
111
+ if sys.platform in ["linux", "darwin"]:
112
+ # Use ~/.mmrelay/logs/ for Linux and Mac
113
+ log_dir = os.path.join(get_base_dir(), "logs")
114
+ else:
115
+ # Use platformdirs default for Windows
116
+ log_dir = platformdirs.user_log_dir(APP_NAME, APP_AUTHOR)
117
+
118
+ os.makedirs(log_dir, exist_ok=True)
119
+ return log_dir
120
+
121
+
122
+ # Set up a basic logger for config
123
+ logger = logging.getLogger("Config")
124
+ logger.setLevel(logging.INFO)
125
+ handler = logging.StreamHandler()
126
+ handler.setFormatter(
127
+ logging.Formatter(
128
+ fmt="%(asctime)s %(levelname)s:%(name)s:%(message)s",
129
+ datefmt="%Y-%m-%d %H:%M:%S %z",
130
+ )
131
+ )
132
+ logger.addHandler(handler)
133
+
134
+ # Initialize empty config
135
+ relay_config = {}
136
+ config_path = None
137
+
138
+
139
+ def set_config(module, passed_config):
140
+ """
141
+ Set the configuration for a module.
142
+
143
+ Args:
144
+ module: The module to set the configuration for
145
+ passed_config: The configuration dictionary to use
146
+
147
+ Returns:
148
+ The updated config
149
+ """
150
+ # Set the module's config variable
151
+ module.config = passed_config
152
+
153
+ # Handle module-specific setup based on module name
154
+ module_name = module.__name__.split(".")[-1]
155
+
156
+ if module_name == "matrix_utils":
157
+ # Set Matrix-specific configuration
158
+ if hasattr(module, "matrix_homeserver") and "matrix" in passed_config:
159
+ module.matrix_homeserver = passed_config["matrix"]["homeserver"]
160
+ module.matrix_rooms = passed_config["matrix_rooms"]
161
+ module.matrix_access_token = passed_config["matrix"]["access_token"]
162
+ module.bot_user_id = passed_config["matrix"]["bot_user_id"]
163
+
164
+ elif module_name == "meshtastic_utils":
165
+ # Set Meshtastic-specific configuration
166
+ if hasattr(module, "matrix_rooms") and "matrix_rooms" in passed_config:
167
+ module.matrix_rooms = passed_config["matrix_rooms"]
168
+
169
+ # If the module still has a setup_config function, call it for backward compatibility
170
+ if hasattr(module, "setup_config") and callable(module.setup_config):
171
+ module.setup_config()
172
+
173
+ return passed_config
174
+
175
+
176
+ def load_config(config_file=None, args=None):
177
+ """Load the configuration from the specified file or search for it.
178
+
179
+ Args:
180
+ config_file (str, optional): Path to the config file. If None, search for it.
181
+ args: The parsed command-line arguments
182
+
183
+ Returns:
184
+ dict: The loaded configuration
185
+ """
186
+ global relay_config, config_path
187
+
188
+ # If a specific config file was provided, use it
189
+ if config_file and os.path.isfile(config_file):
190
+ logger.info(f"Loading configuration from: {config_file}")
191
+ with open(config_file, "r") as f:
192
+ relay_config = yaml.load(f, Loader=SafeLoader)
193
+ config_path = config_file
194
+ return relay_config
195
+
196
+ # Otherwise, search for a config file
197
+ config_paths = get_config_paths(args)
198
+
199
+ # Try each config path in order until we find one that exists
200
+ for path in config_paths:
201
+ if os.path.isfile(path):
202
+ config_path = path
203
+ logger.info(f"Loading configuration from: {config_path}")
204
+ with open(config_path, "r") as f:
205
+ relay_config = yaml.load(f, Loader=SafeLoader)
206
+ logger.info(f"Loaded configuration with keys: {list(relay_config.keys())}")
207
+ return relay_config
208
+
209
+ # No config file found
210
+ logger.error("Configuration file not found in any of the following locations:")
211
+ for path in config_paths:
212
+ logger.error(f" - {path}")
213
+ logger.error("Using empty configuration. This will likely cause errors.")
214
+ logger.error(
215
+ "Run 'mmrelay --generate-config' to generate a sample configuration file."
216
+ )
217
+
218
+ return relay_config