mmrelay 1.2.6__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.
Files changed (50) hide show
  1. mmrelay/__init__.py +5 -0
  2. mmrelay/__main__.py +29 -0
  3. mmrelay/cli.py +2013 -0
  4. mmrelay/cli_utils.py +746 -0
  5. mmrelay/config.py +956 -0
  6. mmrelay/constants/__init__.py +65 -0
  7. mmrelay/constants/app.py +29 -0
  8. mmrelay/constants/config.py +78 -0
  9. mmrelay/constants/database.py +22 -0
  10. mmrelay/constants/formats.py +20 -0
  11. mmrelay/constants/messages.py +45 -0
  12. mmrelay/constants/network.py +45 -0
  13. mmrelay/constants/plugins.py +42 -0
  14. mmrelay/constants/queue.py +20 -0
  15. mmrelay/db_runtime.py +269 -0
  16. mmrelay/db_utils.py +1017 -0
  17. mmrelay/e2ee_utils.py +400 -0
  18. mmrelay/log_utils.py +274 -0
  19. mmrelay/main.py +439 -0
  20. mmrelay/matrix_utils.py +3091 -0
  21. mmrelay/meshtastic_utils.py +1245 -0
  22. mmrelay/message_queue.py +647 -0
  23. mmrelay/plugin_loader.py +1933 -0
  24. mmrelay/plugins/__init__.py +3 -0
  25. mmrelay/plugins/base_plugin.py +638 -0
  26. mmrelay/plugins/debug_plugin.py +30 -0
  27. mmrelay/plugins/drop_plugin.py +127 -0
  28. mmrelay/plugins/health_plugin.py +64 -0
  29. mmrelay/plugins/help_plugin.py +79 -0
  30. mmrelay/plugins/map_plugin.py +353 -0
  31. mmrelay/plugins/mesh_relay_plugin.py +222 -0
  32. mmrelay/plugins/nodes_plugin.py +92 -0
  33. mmrelay/plugins/ping_plugin.py +128 -0
  34. mmrelay/plugins/telemetry_plugin.py +179 -0
  35. mmrelay/plugins/weather_plugin.py +312 -0
  36. mmrelay/runtime_utils.py +35 -0
  37. mmrelay/setup_utils.py +828 -0
  38. mmrelay/tools/__init__.py +27 -0
  39. mmrelay/tools/mmrelay.service +19 -0
  40. mmrelay/tools/sample-docker-compose-prebuilt.yaml +30 -0
  41. mmrelay/tools/sample-docker-compose.yaml +30 -0
  42. mmrelay/tools/sample.env +10 -0
  43. mmrelay/tools/sample_config.yaml +120 -0
  44. mmrelay/windows_utils.py +346 -0
  45. mmrelay-1.2.6.dist-info/METADATA +145 -0
  46. mmrelay-1.2.6.dist-info/RECORD +50 -0
  47. mmrelay-1.2.6.dist-info/WHEEL +5 -0
  48. mmrelay-1.2.6.dist-info/entry_points.txt +2 -0
  49. mmrelay-1.2.6.dist-info/licenses/LICENSE +675 -0
  50. mmrelay-1.2.6.dist-info/top_level.txt +1 -0
@@ -0,0 +1,27 @@
1
+ """Tools and resources for MMRelay."""
2
+
3
+ import importlib.resources
4
+
5
+
6
+ def get_sample_config_path():
7
+ """
8
+ Provide the filesystem path to the package's sample configuration file.
9
+
10
+ Returns:
11
+ path (str): Path to `sample_config.yaml` located in the `mmrelay.tools` package.
12
+ """
13
+ return str(
14
+ importlib.resources.files("mmrelay.tools").joinpath("sample_config.yaml")
15
+ )
16
+
17
+
18
+ def get_service_template_path():
19
+ """
20
+ Return the filesystem path to the mmrelay.service template bundled with the package.
21
+
22
+ Locate the `mmrelay.service` resource inside the `mmrelay.tools` package and return its filesystem path as a string.
23
+
24
+ Returns:
25
+ path (str): Filesystem path to the `mmrelay.service` template within the `mmrelay.tools` package.
26
+ """
27
+ return str(importlib.resources.files("mmrelay.tools").joinpath("mmrelay.service"))
@@ -0,0 +1,19 @@
1
+ [Unit]
2
+ Description=MMRelay - Meshtastic <=> Matrix Relay
3
+ After=network-online.target
4
+ Wants=network-online.target
5
+
6
+ [Service]
7
+ Type=simple
8
+ # The mmrelay binary can be installed via pipx or pip
9
+ ExecStart=%h/.local/bin/mmrelay --config %h/.mmrelay/config.yaml --logfile %h/.mmrelay/logs/mmrelay.log
10
+ WorkingDirectory=%h/.mmrelay
11
+ Restart=on-failure
12
+ RestartSec=10
13
+ Environment=PYTHONUNBUFFERED=1
14
+ Environment=LANG=C.UTF-8
15
+ # Ensure both pipx and pip environments are properly loaded
16
+ Environment=PATH=%h/.local/bin:%h/.local/pipx/venvs/mmrelay/bin:/usr/local/bin:/usr/bin:/bin
17
+
18
+ [Install]
19
+ WantedBy=default.target
@@ -0,0 +1,30 @@
1
+ services:
2
+ mmrelay:
3
+ image: ghcr.io/jeremiah-k/mmrelay:latest
4
+ container_name: meshtastic-matrix-relay
5
+ restart: unless-stopped
6
+ stop_grace_period: 30s
7
+ user: "${UID:-1000}:${GID:-1000}"
8
+ environment:
9
+ - TZ=UTC # Set timezone (PYTHONUNBUFFERED and MPLCONFIGDIR are set in Dockerfile)
10
+
11
+ volumes:
12
+ # Mount your config directory - create ~/.mmrelay/config.yaml first
13
+ # See docs/DOCKER.md for setup instructions
14
+ # For non-SELinux systems (most common):
15
+ - ${MMRELAY_HOME:-$HOME}/.mmrelay/config.yaml:/app/config.yaml:ro
16
+ - ${MMRELAY_HOME:-$HOME}/.mmrelay:/app/data
17
+ # For SELinux systems (RHEL/CentOS/Fedora), add :Z flag to prevent permission denied errors:
18
+ # - ${MMRELAY_HOME:-$HOME}/.mmrelay/config.yaml:/app/config.yaml:ro,Z
19
+ # - ${MMRELAY_HOME:-$HOME}/.mmrelay:/app/data:Z
20
+
21
+ # For BLE connections, uncomment these lines (Linux only):
22
+ # - /var/run/dbus:/var/run/dbus:ro
23
+
24
+ # For BLE connections, uncomment these options (Linux only). See DOCKER.md for alternatives.
25
+ # network_mode: host
26
+ # security_opt:
27
+ # - apparmor=unconfined # Recommended for BLE to allow DBus communication
28
+ # privileged: true # Alternative if apparmor=unconfined is not acceptable
29
+
30
+ # Tip: For correct permissions and paths, ensure UID, GID, and MMRELAY_HOME are set in a .env file or exported
@@ -0,0 +1,30 @@
1
+ services:
2
+ mmrelay:
3
+ build: .
4
+ container_name: mmrelay # Updated for consistent branding
5
+ restart: unless-stopped
6
+ stop_grace_period: 30s
7
+ user: "${UID:-1000}:${GID:-1000}"
8
+ environment:
9
+ - TZ=UTC # Set timezone (PYTHONUNBUFFERED and MPLCONFIGDIR are set in Dockerfile)
10
+
11
+ volumes:
12
+ # Mount your config directory - create ~/.mmrelay/config.yaml first
13
+ # Run 'make config' to set up the files
14
+ # For non-SELinux systems (most common):
15
+ - ${MMRELAY_HOME:-$HOME}/.mmrelay/config.yaml:/app/config.yaml:ro
16
+ - ${MMRELAY_HOME:-$HOME}/.mmrelay:/app/data
17
+ # For SELinux systems (RHEL/CentOS/Fedora), add :Z flag to prevent permission denied errors:
18
+ # - ${MMRELAY_HOME:-$HOME}/.mmrelay/config.yaml:/app/config.yaml:ro,Z
19
+ # - ${MMRELAY_HOME:-$HOME}/.mmrelay:/app/data:Z
20
+
21
+ # For BLE connections, uncomment these lines (Linux only):
22
+ # - /var/run/dbus:/var/run/dbus:ro
23
+
24
+ # For BLE connections, uncomment these options (Linux only). See DOCKER.md for alternatives.
25
+ # network_mode: host
26
+ # security_opt:
27
+ # - apparmor=unconfined # Recommended for BLE to allow DBus communication
28
+ # privileged: true # Alternative if apparmor=unconfined is not acceptable
29
+
30
+ # Tip: For correct permissions and paths, ensure UID, GID, and MMRELAY_HOME are set in a .env file or exported
@@ -0,0 +1,10 @@
1
+ # Docker Compose environment variables
2
+ # Customize these paths if needed
3
+
4
+ # Base directory for mmrelay data
5
+ # This will be expanded by your shell when docker compose runs
6
+ MMRELAY_HOME=$HOME
7
+
8
+ # Preferred editor for config editing
9
+ # Will be set automatically when you select an editor via 'make edit'
10
+ EDITOR=nano
@@ -0,0 +1,120 @@
1
+ matrix:
2
+ homeserver: https://example.matrix.org
3
+ # Modern authentication using password (recommended)
4
+ # MMRelay will automatically create secure credentials.json on startup
5
+ password: your_matrix_password_here # Set your Matrix account password
6
+ # Security: After first successful start, remove this password from the file.
7
+ # Linux/macOS: chmod 600 ~/.mmrelay/config.yaml
8
+ # Windows: restrict file access to your user (e.g., via file Properties → Security)
9
+ # Never commit this file with a password to version control.
10
+ bot_user_id: "@botuser:example.matrix.org"
11
+
12
+ # End-to-End Encryption (E2EE) configuration
13
+ # E2EE is automatically enabled when using password-based authentication AND E2EE dependencies are installed (Linux/macOS only)
14
+ # NOTE: E2EE is not available on Windows due to dependency limitations
15
+ #
16
+ # SETUP INSTRUCTIONS:
17
+ # 1. Install E2EE dependencies: pipx install 'mmrelay[e2e]' (Linux/macOS only)
18
+ # 2. Set your password above and run MMRelay
19
+ # 3. MMRelay will automatically create credentials.json with E2EE support
20
+ # 4. For interactive setup, use: mmrelay auth login
21
+ #
22
+ #e2ee:
23
+ # # Optional: When credentials.json is present, MMRelay auto-enables E2EE.
24
+ # # Configure this section only if you want to override defaults (e.g., store_path).
25
+ # enabled: false # Explicit toggle if you need to force-enable/disable (usually not needed)
26
+ # store_path: ~/.mmrelay/store # Optional path for encryption keys storage
27
+
28
+ # Message prefix customization (Meshtastic → Matrix direction)
29
+ #prefix_enabled: true # Enable prefixes on messages from mesh (e.g., "[Alice/MyMesh]: message")
30
+ #prefix_format: "[{long}/{mesh}]: " # Default format. Variables: {long1-20}, {long}, {short}, {mesh1-20}, {mesh}
31
+
32
+ matrix_rooms: # Needs at least 1 room & channel, but supports all Meshtastic channels
33
+ - id: "#someroomalias:example.matrix.org" # Matrix room aliases & IDs supported
34
+ meshtastic_channel: 0
35
+ - id: "!someroomid:example.matrix.org"
36
+ meshtastic_channel: 2
37
+
38
+ meshtastic:
39
+ connection_type: tcp # Choose either "tcp", "serial", or "ble"
40
+ host: meshtastic.local # Only used when connection is "tcp"
41
+ serial_port: /dev/ttyUSB0 # Only used when connection is "serial"
42
+ ble_address: AA:BB:CC:DD:EE:FF # Only used when connection is "ble" - Uses either an address or name from a `meshtastic --ble-scan`
43
+ meshnet_name: Your Meshnet Name # This is displayed in full on Matrix, but is truncated when sent to a Meshnet
44
+ message_interactions: # Configure reactions and replies (both require message storage in database)
45
+ reactions: false # Enable reaction relaying between platforms
46
+ replies: false # Enable reply relaying between platforms
47
+
48
+ # Connection health monitoring configuration
49
+ #health_check:
50
+ # enabled: true # Enable/disable periodic health checks (default: true)
51
+ # heartbeat_interval: 60 # Interval in seconds between health checks (default: 60)
52
+ # # Note: BLE connections use real-time disconnection detection and skip periodic checks
53
+ # # Legacy: heartbeat_interval at meshtastic level still supported but deprecated
54
+
55
+ # Additional configuration options (commented out with defaults)
56
+ broadcast_enabled: true # Must be set to true to enable Matrix to Meshtastic messages
57
+ #detection_sensor: true # Must be set to true to forward messages of Meshtastic's detection sensor module
58
+ #message_delay: 2.2 # Delay in seconds between messages sent to mesh (minimum: 2.0 due to firmware)
59
+
60
+ # Message prefix customization (Matrix → Meshtastic direction)
61
+ #prefix_enabled: true # Enable username prefixes on messages sent to mesh (e.g., "Alice[M]: message")
62
+ #prefix_format: "{display5}[M]: " # Default format. See ADVANCED_CONFIGURATION.md for all variables.
63
+
64
+ logging:
65
+ level: info
66
+ #log_to_file: true # Set to true to enable file logging
67
+ #filename: ~/.mmrelay/logs/mmrelay.log # Default location if log_to_file is true
68
+ #max_log_size: 10485760 # 10 MB default if omitted
69
+ #backup_count: 1 # Keeps 1 backup as the default if omitted
70
+ #color_enabled: true # Set to false to disable colored console output
71
+
72
+ # Component-specific debug logging (useful for troubleshooting)
73
+ # When disabled (false or omitted), external library logs are completely suppressed
74
+ # When enabled, you can use: true (DEBUG level) or specify level: "debug", "info", "warning", "error"
75
+ #debug:
76
+ # matrix_nio: false # Disable matrix-nio logging (default: completely suppressed)
77
+ # #matrix_nio: true # Enable matrix-nio debug logging
78
+ # #matrix_nio: "warning" # Enable matrix-nio warning+ logging
79
+ # bleak: false # Disable BLE (bleak) logging (default: completely suppressed)
80
+ # meshtastic: false # Disable meshtastic library logging (default: completely suppressed)
81
+
82
+ #database:
83
+ # path: ~/.mmrelay/data/meshtastic.sqlite # Default location
84
+ # enable_wal: true # Enable Write-Ahead Logging for better concurrency
85
+ # busy_timeout_ms: 5000 # Milliseconds to wait if the database is busy
86
+ # pragmas:
87
+ # synchronous: NORMAL
88
+ # temp_store: MEMORY
89
+ # msg_map: # The message map is necessary for the relay_reactions functionality. If `relay_reactions` is set to false, nothing will be saved to the message map.
90
+ # msgs_to_keep: 500 # If set to 0, it will not delete any messages; Defaults to 500
91
+ # wipe_on_restart: true # Clears out the message map when the relay is restarted; Defaults to False
92
+
93
+ # These are core Plugins - Note: Some plugins are experimental and some need maintenance.
94
+ plugins:
95
+ ping:
96
+ active: true
97
+ #channels: [2,3,5] # List of channels the plugin will respond to; DMs are always processed if the plugin is active
98
+ weather:
99
+ active: true
100
+ units: imperial # Options: metric, imperial - Default is metric
101
+ #channels: [] # Empty list, will only respond to DMs
102
+ nodes:
103
+ active: true
104
+ # Does not need to specify channels, as it's a Matrix-only plugin
105
+
106
+ #community-plugins:
107
+ # sample_plugin:
108
+ # active: true
109
+ # repository: https://github.com/username/sample_plugin.git
110
+ # tag: master
111
+ # advanced_plugin:
112
+ # active: false
113
+ # repository: https://github.com/username/advanced_plugin.git
114
+ # tag: v1.2.0
115
+
116
+ #custom-plugins:
117
+ # my_custom_plugin:
118
+ # active: true
119
+ # another_custom_plugin:
120
+ # active: false
@@ -0,0 +1,346 @@
1
+ """
2
+ Windows-specific utilities for MMRelay.
3
+
4
+ This module provides Windows-specific functionality and workarounds
5
+ for better compatibility and user experience on Windows systems.
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ from typing import Optional
11
+
12
+ from mmrelay.constants.app import WINDOWS_PLATFORM
13
+
14
+
15
+ def is_windows() -> bool:
16
+ """
17
+ Return True if the current process is running on a Windows platform.
18
+
19
+ Checks common platform indicators (os.name == "nt" or sys.platform == WINDOWS_PLATFORM)
20
+ and returns a boolean accordingly.
21
+
22
+ Returns:
23
+ bool: True when running on Windows, otherwise False.
24
+ """
25
+ return os.name == "nt" or sys.platform == WINDOWS_PLATFORM
26
+
27
+
28
+ def setup_windows_console() -> None:
29
+ """
30
+ Configure the Windows console for UTF-8 output and ANSI (VT100) color support.
31
+
32
+ Best-effort operation: on Windows this attempts to set stdout/stderr encoding to UTF-8
33
+ (if supported) and enable Virtual Terminal Processing so ANSI color sequences and
34
+ Unicode render correctly. No-op on non-Windows platforms; failures are silently ignored.
35
+ """
36
+ if not is_windows():
37
+ return
38
+
39
+ try:
40
+ # Enable UTF-8 output on Windows
41
+ if hasattr(sys.stdout, "reconfigure"):
42
+ sys.stdout.reconfigure(encoding="utf-8")
43
+ if hasattr(sys.stderr, "reconfigure"):
44
+ sys.stderr.reconfigure(encoding="utf-8")
45
+
46
+ # Enable ANSI color codes on Windows 10+
47
+ import ctypes
48
+
49
+ kernel32 = ctypes.windll.kernel32
50
+ ENABLE_VTP = 0x0004 # ENABLE_VIRTUAL_TERMINAL_PROCESSING
51
+ for handle in (-11, -12): # STD_OUTPUT_HANDLE, STD_ERROR_HANDLE
52
+ h = kernel32.GetStdHandle(handle)
53
+ if h is not None and h != -1:
54
+ mode = ctypes.c_uint()
55
+ if kernel32.GetConsoleMode(h, ctypes.byref(mode)):
56
+ kernel32.SetConsoleMode(h, mode.value | ENABLE_VTP)
57
+ except (OSError, AttributeError):
58
+ # If console setup fails, continue without it
59
+ # This is expected on non-Windows systems or older Windows versions
60
+ return
61
+
62
+
63
+ def get_windows_error_message(error: Exception) -> str:
64
+ """
65
+ Return a Windows-tailored, user-friendly message for common filesystem and network exceptions.
66
+
67
+ If not running on Windows this returns str(error) unchanged. For Windows, the function recognizes
68
+ permission errors (PermissionError or OSError with EACCES/EPERM), missing-file errors
69
+ (FileNotFoundError or OSError with ENOENT), and common network-related OSErrors
70
+ (EHOSTUNREACH, ENETDOWN, ENETUNREACH, ECONNREFUSED, ETIMEDOUT) and returns guidance specific
71
+ to each case (possible causes and suggested next steps). For other exceptions it returns
72
+ str(error).
73
+
74
+ Parameters:
75
+ error (Exception): The exception to translate into a user-facing message.
76
+
77
+ Returns:
78
+ str: A user-oriented error message; either a Windows-specific guidance string or the
79
+ original exception string for unhandled cases or non-Windows platforms.
80
+ """
81
+ if not is_windows():
82
+ return str(error)
83
+
84
+ import errno as _errno
85
+
86
+ # Use exception types and errno codes for more robust error detection
87
+ if isinstance(error, PermissionError) or (
88
+ isinstance(error, OSError) and error.errno in {_errno.EACCES, _errno.EPERM}
89
+ ):
90
+ return (
91
+ f"Permission denied: {error}\n"
92
+ "This may be caused by:\n"
93
+ "• Antivirus software blocking the operation\n"
94
+ "• Windows User Account Control (UAC) restrictions\n"
95
+ "• File being used by another process\n"
96
+ "Try running as administrator or check antivirus settings."
97
+ )
98
+ elif isinstance(error, FileNotFoundError) or (
99
+ isinstance(error, OSError) and error.errno in {_errno.ENOENT}
100
+ ):
101
+ return (
102
+ f"File not found: {error}\n"
103
+ "This may be caused by:\n"
104
+ "• Incorrect file path (check for spaces or special characters)\n"
105
+ "• File moved or deleted by antivirus software\n"
106
+ "• Network drive disconnection\n"
107
+ "Verify the file path and check antivirus quarantine."
108
+ )
109
+ elif isinstance(error, ConnectionError) or (
110
+ isinstance(error, OSError)
111
+ and error.errno
112
+ in {
113
+ _errno.EHOSTUNREACH,
114
+ _errno.ENETDOWN,
115
+ _errno.ENETUNREACH,
116
+ _errno.ECONNREFUSED,
117
+ _errno.ETIMEDOUT,
118
+ }
119
+ ):
120
+ return (
121
+ f"Network error: {error}\n"
122
+ "This may be caused by:\n"
123
+ "• Windows Firewall blocking the connection\n"
124
+ "• Antivirus software blocking network access\n"
125
+ "• VPN or proxy configuration issues\n"
126
+ "Check firewall settings and antivirus network protection."
127
+ )
128
+ else:
129
+ return str(error)
130
+
131
+
132
+ def check_windows_requirements() -> Optional[str]:
133
+ """
134
+ Report Windows environment compatibility issues as a multi-line warning string.
135
+
136
+ When running on Windows, performs these checks and accumulates user-facing warnings:
137
+ - Python version is older than 3.10.
138
+ - Process does not appear to be running inside a virtual environment (venv/pipx).
139
+ - Current working directory path length exceeds 200 characters.
140
+
141
+ Returns:
142
+ Optional[str]: Multi-line warning message prefixed with "Windows compatibility warnings:" and bullet points for each detected issue; `None` if no issues are detected or when not running on Windows.
143
+ """
144
+ if not is_windows():
145
+ return None
146
+
147
+ warnings = []
148
+
149
+ # Check Python version for Windows compatibility
150
+ if sys.version_info < (3, 10):
151
+ warnings.append("Python 3.10+ is required for this application")
152
+
153
+ # Check if running in a virtual environment
154
+ if not hasattr(sys, "real_prefix") and not (
155
+ hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix
156
+ ):
157
+ warnings.append(
158
+ "Consider using a virtual environment (venv) or pipx for better isolation"
159
+ )
160
+
161
+ # Check for common Windows path issues
162
+ if len(os.getcwd()) > 200:
163
+ warnings.append(
164
+ "Current directory path is very long - this may cause issues on Windows"
165
+ )
166
+
167
+ if warnings:
168
+ return "Windows compatibility warnings:\n• " + "\n• ".join(warnings)
169
+
170
+ return None
171
+
172
+
173
+ def test_config_generation_windows(args=None) -> dict:
174
+ """
175
+ Run Windows-only diagnostics for MMRelay configuration generation.
176
+
177
+ Performs four checks and returns a dictionary with per-test results and an overall status:
178
+ - sample_config_path: verifies mmrelay.tools.get_sample_config_path() exists.
179
+ - importlib_resources: attempts to read mmrelay.tools/sample_config.yaml via importlib.resources.
180
+ - config_paths: calls mmrelay.config.get_config_paths(args) and reports the returned paths.
181
+ - directory_creation: ensures parent directories for the config paths exist (creates them if missing).
182
+
183
+ Parameters:
184
+ args (optional): Forwarded to mmrelay.config.get_config_paths; typically CLI-style arguments or None.
185
+
186
+ Returns:
187
+ dict: Diagnostic results with these keys:
188
+ - sample_config_path, importlib_resources, config_paths, directory_creation:
189
+ dict objects with "status" ("ok" or "error") and "details" (string).
190
+ - overall_status: one of "ok" (no errors), "partial" (1–2 errors), or "error" (3+ errors).
191
+ If called on a non-Windows platform, returns {"error": "This function is only for Windows systems"}.
192
+
193
+ Notes:
194
+ - The function does not raise on expected failures; errors are reported in the returned dict.
195
+ - Directory creation test may create directories on disk when missing.
196
+ """
197
+ if not is_windows():
198
+ return {"error": "This function is only for Windows systems"}
199
+
200
+ results = {
201
+ "sample_config_path": {"status": "unknown", "details": ""},
202
+ "importlib_resources": {"status": "unknown", "details": ""},
203
+ "config_paths": {"status": "unknown", "details": ""},
204
+ "directory_creation": {"status": "unknown", "details": ""},
205
+ "overall_status": "unknown",
206
+ }
207
+
208
+ try:
209
+ # Test 1: Sample config path
210
+ try:
211
+ from mmrelay.tools import get_sample_config_path
212
+
213
+ sample_path = get_sample_config_path()
214
+ if os.path.exists(sample_path):
215
+ results["sample_config_path"] = {
216
+ "status": "ok",
217
+ "details": f"Found at: {sample_path}",
218
+ }
219
+ else:
220
+ results["sample_config_path"] = {
221
+ "status": "error",
222
+ "details": f"Not found at: {sample_path}",
223
+ }
224
+ except (
225
+ ImportError,
226
+ OSError,
227
+ FileNotFoundError,
228
+ AttributeError,
229
+ TypeError,
230
+ ) as e:
231
+ results["sample_config_path"] = {"status": "error", "details": str(e)}
232
+
233
+ # Test 2: importlib.resources fallback
234
+ try:
235
+ import importlib.resources
236
+
237
+ content = (
238
+ importlib.resources.files("mmrelay.tools")
239
+ .joinpath("sample_config.yaml")
240
+ .read_text()
241
+ )
242
+ results["importlib_resources"] = {
243
+ "status": "ok",
244
+ "details": f"Content length: {len(content)} chars",
245
+ }
246
+ except (ImportError, OSError, FileNotFoundError) as e:
247
+ results["importlib_resources"] = {"status": "error", "details": str(e)}
248
+
249
+ # Test 3: Config paths
250
+ try:
251
+ from mmrelay.config import get_config_paths
252
+
253
+ paths = get_config_paths(args)
254
+ results["config_paths"] = {"status": "ok", "details": f"Paths: {paths}"}
255
+ except (ImportError, OSError) as e:
256
+ results["config_paths"] = {"status": "error", "details": str(e)}
257
+
258
+ # Test 4: Directory creation
259
+ try:
260
+ from mmrelay.config import get_config_paths
261
+
262
+ paths = get_config_paths(args)
263
+ created_dirs = []
264
+ for path in paths:
265
+ dir_path = os.path.dirname(path)
266
+ if not os.path.exists(dir_path):
267
+ os.makedirs(dir_path, exist_ok=True)
268
+ created_dirs.append(dir_path)
269
+ results["directory_creation"] = {
270
+ "status": "ok",
271
+ "details": f"Created: {created_dirs}",
272
+ }
273
+ except OSError as e:
274
+ results["directory_creation"] = {"status": "error", "details": str(e)}
275
+
276
+ # Determine overall status
277
+ error_count = sum(
278
+ 1
279
+ for r in results.values()
280
+ if isinstance(r, dict) and r.get("status") == "error"
281
+ )
282
+ if error_count == 0:
283
+ results["overall_status"] = "ok"
284
+ elif error_count < 3: # If at least one fallback works
285
+ results["overall_status"] = "partial"
286
+ else:
287
+ results["overall_status"] = "error"
288
+
289
+ except OSError as e:
290
+ results["overall_status"] = "error"
291
+ results["error"] = str(e)
292
+
293
+ return results
294
+
295
+
296
+ def get_windows_install_guidance() -> str:
297
+ """
298
+ Return a Windows-specific installation and troubleshooting guide as a formatted string.
299
+
300
+ The returned text contains recommended installation methods, common Windows-specific problems with actionable remedies, and troubleshooting tips for configuration and runtime issues. It is safe to display directly to end users or include in logs/help output.
301
+
302
+ Returns:
303
+ str: Multiline guidance text suited for Windows users.
304
+ """
305
+ return """
306
+ Windows Installation & Troubleshooting Guide:
307
+
308
+ 📦 Recommended Installation:
309
+ pipx install mmrelay
310
+ (pipx provides better isolation and fewer conflicts)
311
+
312
+ 🔧 If pipx is not available:
313
+ pip install --user mmrelay
314
+ (installs to user directory, avoiding system conflicts)
315
+
316
+ ⚠️ Common Windows Issues:
317
+
318
+ 1. "ModuleNotFoundError: No module named 'pkg_resources'"
319
+ Solution: pip install --upgrade setuptools
320
+ Alternative: Use 'python -m mmrelay' instead of 'mmrelay'
321
+
322
+ 2. "Access denied" or permission errors
323
+ Solution: Run command prompt as administrator
324
+ Or: Use --user flag with pip
325
+
326
+ 3. "SSL certificate verify failed"
327
+ Solution: Update certificates or use --trusted-host flag
328
+
329
+ 4. Antivirus blocking installation/execution
330
+ Solution: Add Python and pip to antivirus exclusions
331
+
332
+ 5. Long path issues
333
+ Solution: Enable long path support in Windows 10+
334
+ Or: Use shorter installation directory
335
+
336
+ 6. Config generation fails
337
+ Solution: Check if sample_config.yaml is accessible
338
+ Alternative: Manually create config file from documentation
339
+
340
+ 🆘 Need Help?
341
+ • Check Windows Event Viewer for detailed error logs
342
+ • Temporarily disable antivirus for testing
343
+ • Use Windows PowerShell instead of Command Prompt
344
+ • Consider using Windows Subsystem for Linux (WSL)
345
+ • Test config generation: 'python -m mmrelay config diagnose'
346
+ """