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_utils.py
ADDED
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI utilities and command registry.
|
|
3
|
+
|
|
4
|
+
This module provides a centralized registry of all CLI commands to ensure
|
|
5
|
+
consistency across error messages, help text, and documentation. It's separate
|
|
6
|
+
from cli.py to avoid circular dependencies when other modules need to reference
|
|
7
|
+
CLI commands.
|
|
8
|
+
|
|
9
|
+
It also contains CLI-specific functions that need to interact with users
|
|
10
|
+
via print statements (as opposed to library functions that should only log).
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
from mmrelay.cli_utils import get_command, suggest_command, logout_matrix_bot
|
|
14
|
+
|
|
15
|
+
# Get a command string
|
|
16
|
+
cmd = get_command('generate_config') # Returns "mmrelay config generate"
|
|
17
|
+
|
|
18
|
+
# Generate suggestion messages
|
|
19
|
+
msg = suggest_command('generate_config', 'to create a sample configuration')
|
|
20
|
+
|
|
21
|
+
# CLI functions (can use print statements)
|
|
22
|
+
result = await logout_matrix_bot(password="user_password")
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import asyncio
|
|
26
|
+
import logging
|
|
27
|
+
import os
|
|
28
|
+
import ssl
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
import certifi
|
|
32
|
+
except ImportError:
|
|
33
|
+
certifi = None
|
|
34
|
+
|
|
35
|
+
# Import Matrix-related modules for logout functionality
|
|
36
|
+
try:
|
|
37
|
+
from nio import AsyncClient
|
|
38
|
+
from nio.exceptions import (
|
|
39
|
+
LocalProtocolError,
|
|
40
|
+
LocalTransportError,
|
|
41
|
+
RemoteProtocolError,
|
|
42
|
+
RemoteTransportError,
|
|
43
|
+
)
|
|
44
|
+
from nio.responses import LoginError, LogoutError
|
|
45
|
+
|
|
46
|
+
# Create aliases for backward compatibility
|
|
47
|
+
NioLoginError = LoginError
|
|
48
|
+
NioLogoutError = LogoutError
|
|
49
|
+
NioLocalTransportError = LocalTransportError
|
|
50
|
+
NioRemoteTransportError = RemoteTransportError
|
|
51
|
+
NioLocalProtocolError = LocalProtocolError
|
|
52
|
+
NioRemoteProtocolError = RemoteProtocolError
|
|
53
|
+
except ImportError:
|
|
54
|
+
# Handle case where matrix-nio is not installed
|
|
55
|
+
AsyncClient = None
|
|
56
|
+
LoginError = Exception
|
|
57
|
+
LogoutError = Exception
|
|
58
|
+
LocalTransportError = Exception
|
|
59
|
+
RemoteTransportError = Exception
|
|
60
|
+
LocalProtocolError = Exception
|
|
61
|
+
RemoteProtocolError = Exception
|
|
62
|
+
# Create aliases for backward compatibility
|
|
63
|
+
NioLoginError = Exception
|
|
64
|
+
NioLogoutError = Exception
|
|
65
|
+
NioLocalTransportError = Exception
|
|
66
|
+
NioRemoteTransportError = Exception
|
|
67
|
+
NioLocalProtocolError = Exception
|
|
68
|
+
NioRemoteProtocolError = Exception
|
|
69
|
+
|
|
70
|
+
# Import mmrelay modules - avoid circular imports by importing inside functions
|
|
71
|
+
|
|
72
|
+
logger = logging.getLogger(__name__)
|
|
73
|
+
|
|
74
|
+
# Command registry - single source of truth for CLI command syntax
|
|
75
|
+
CLI_COMMANDS = {
|
|
76
|
+
# Config commands
|
|
77
|
+
"generate_config": "mmrelay config generate",
|
|
78
|
+
"check_config": "mmrelay config check",
|
|
79
|
+
# Auth commands
|
|
80
|
+
"auth_login": "mmrelay auth login",
|
|
81
|
+
"auth_status": "mmrelay auth status",
|
|
82
|
+
# Service commands
|
|
83
|
+
"service_install": "mmrelay service install",
|
|
84
|
+
# Main commands
|
|
85
|
+
"start_relay": "mmrelay",
|
|
86
|
+
"show_version": "mmrelay --version",
|
|
87
|
+
"show_help": "mmrelay --help",
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# Deprecation mappings - maps old flags to new command keys
|
|
91
|
+
DEPRECATED_COMMANDS = {
|
|
92
|
+
"--generate-config": "generate_config",
|
|
93
|
+
"--check-config": "check_config",
|
|
94
|
+
"--install-service": "service_install",
|
|
95
|
+
"--auth": "auth_login",
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def get_command(command_key):
|
|
100
|
+
"""Get the current command syntax for a given command key.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
command_key (str): The command key (e.g., 'generate_config')
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
str: The current command syntax (e.g., 'mmrelay config generate')
|
|
107
|
+
|
|
108
|
+
Raises:
|
|
109
|
+
KeyError: If the command key is not found in the registry
|
|
110
|
+
"""
|
|
111
|
+
if command_key not in CLI_COMMANDS:
|
|
112
|
+
raise KeyError(f"Unknown CLI command key: {command_key}")
|
|
113
|
+
return CLI_COMMANDS[command_key]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def get_deprecation_warning(old_flag):
|
|
117
|
+
"""
|
|
118
|
+
Return a user-facing deprecation warning for a deprecated CLI flag.
|
|
119
|
+
|
|
120
|
+
Looks up a replacement command for the given deprecated flag in DEPRECATED_COMMANDS.
|
|
121
|
+
If a replacement exists, the returned message suggests the full new command (resolved
|
|
122
|
+
via get_command). Otherwise it returns a generic guidance message pointing the user
|
|
123
|
+
to `mmrelay --help`.
|
|
124
|
+
|
|
125
|
+
Parameters:
|
|
126
|
+
old_flag (str): Deprecated flag (e.g., '--generate-config').
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
str: Formatted deprecation warning message.
|
|
130
|
+
"""
|
|
131
|
+
new_command_key = DEPRECATED_COMMANDS.get(old_flag)
|
|
132
|
+
if new_command_key:
|
|
133
|
+
new_command = get_command(new_command_key)
|
|
134
|
+
return f"Warning: {old_flag} is deprecated. Use '{new_command}' instead."
|
|
135
|
+
return f"Warning: {old_flag} is deprecated. Run 'mmrelay --help' to see the current commands."
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def suggest_command(command_key, purpose):
|
|
139
|
+
"""
|
|
140
|
+
Return a concise suggestion message that tells the user which CLI command to run.
|
|
141
|
+
|
|
142
|
+
Parameters:
|
|
143
|
+
command_key (str): Key used to look up the full CLI command in the registry.
|
|
144
|
+
purpose (str): Short phrase describing why to run the command (should start with "to", e.g. "to validate your configuration").
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
str: Formatted suggestion like "Run '<command>' {purpose}."
|
|
148
|
+
"""
|
|
149
|
+
command = get_command(command_key)
|
|
150
|
+
return f"Run '{command}' {purpose}."
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def require_command(command_key, purpose):
|
|
154
|
+
"""
|
|
155
|
+
Return a user-facing requirement message that instructs running a registered CLI command.
|
|
156
|
+
|
|
157
|
+
Parameters:
|
|
158
|
+
command_key (str): Key used to look up the command in the CLI registry.
|
|
159
|
+
purpose (str): Short purpose phrase (typically begins with "to"), e.g. "to generate a sample configuration file".
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
str: Formatted message like "Please run '<full command>' {purpose}."
|
|
163
|
+
|
|
164
|
+
Raises:
|
|
165
|
+
KeyError: If `command_key` is not found in the command registry.
|
|
166
|
+
"""
|
|
167
|
+
command = get_command(command_key)
|
|
168
|
+
return f"Please run '{command}' {purpose}."
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def retry_command(command_key, context=""):
|
|
172
|
+
"""
|
|
173
|
+
Return a user-facing retry message instructing the user to run the given CLI command again.
|
|
174
|
+
|
|
175
|
+
Parameters:
|
|
176
|
+
command_key (str): Key from CLI_COMMANDS that identifies the command to show.
|
|
177
|
+
context (str): Optional trailing context to append to the message (e.g., "after fixing X").
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
str: Formatted message, either "Try running '<command>' again." or "Try running '<command>' again {context}."
|
|
181
|
+
"""
|
|
182
|
+
command = get_command(command_key)
|
|
183
|
+
if context:
|
|
184
|
+
return f"Try running '{command}' again {context}."
|
|
185
|
+
else:
|
|
186
|
+
return f"Try running '{command}' again."
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def validate_command(command_key, purpose):
|
|
190
|
+
"""
|
|
191
|
+
Return a user-facing validation message that references a registered CLI command.
|
|
192
|
+
|
|
193
|
+
command_key should be a key from the module's command registry (e.g. "check_config"); purpose is a short phrase describing the validation action (e.g. "to validate your configuration"). Returns a string like: "Use '<full-command>' {purpose}."
|
|
194
|
+
"""
|
|
195
|
+
command = get_command(command_key)
|
|
196
|
+
return f"Use '{command}' {purpose}."
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# Common message templates for frequently used commands
|
|
200
|
+
def msg_suggest_generate_config():
|
|
201
|
+
"""
|
|
202
|
+
Return a standardized user-facing suggestion to generate a sample configuration file.
|
|
203
|
+
|
|
204
|
+
This message references the configured "generate_config" CLI command and is suitable for prompts and help text.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
str: A sentence instructing the user to run the generate-config command to generate a sample configuration file (e.g., "Run 'mmrelay config generate' to generate a sample configuration file.").
|
|
208
|
+
"""
|
|
209
|
+
return suggest_command("generate_config", "to generate a sample configuration file")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def msg_suggest_check_config():
|
|
213
|
+
"""
|
|
214
|
+
Return a standardized suggestion prompting the user to validate their configuration.
|
|
215
|
+
|
|
216
|
+
This helper builds the user-visible message that tells users how to validate their config (e.g. by running the configured "check_config" CLI command).
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
str: A full sentence suggesting the user run the config validation command.
|
|
220
|
+
"""
|
|
221
|
+
return validate_command("check_config", "to validate your configuration")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def msg_require_auth_login():
|
|
225
|
+
"""
|
|
226
|
+
Return a standard instruction asking the user to run the authentication command.
|
|
227
|
+
|
|
228
|
+
This produces a formatted message that tells the user to run the configured "auth_login" CLI command
|
|
229
|
+
to set up credentials.json or to add a Matrix section to config.yaml.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
str: A user-facing instruction string.
|
|
233
|
+
"""
|
|
234
|
+
return require_command(
|
|
235
|
+
"auth_login", "to set up credentials.json, or add matrix section to config.yaml"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def msg_retry_auth_login():
|
|
240
|
+
"""Standard message suggesting auth retry."""
|
|
241
|
+
return retry_command("auth_login")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def msg_run_auth_login():
|
|
245
|
+
"""
|
|
246
|
+
Return a user-facing message that instructs running the auth login command to (re)generate credentials.
|
|
247
|
+
|
|
248
|
+
The message prompts the user to run the authentication/login command again so new credentials (including a device_id) are created.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
str: Formatted instruction string for running the auth login command.
|
|
252
|
+
"""
|
|
253
|
+
return msg_regenerate_credentials()
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def msg_for_e2ee_support():
|
|
257
|
+
"""
|
|
258
|
+
Return a user-facing instruction to run the authentication command required for E2EE support.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
str: A formatted message instructing the user to run the configured `auth_login` CLI command to enable end-to-end encryption (E2EE) support.
|
|
262
|
+
"""
|
|
263
|
+
return f"For E2EE support: run '{get_command('auth_login')}'"
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def msg_setup_auth():
|
|
267
|
+
"""
|
|
268
|
+
Return a standard instruction directing the user to run the authentication setup command.
|
|
269
|
+
|
|
270
|
+
The message is formatted as "Setup: <command>", where <command> is the current CLI syntax for the "auth_login" command resolved from the command registry.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
str: Formatted setup instruction pointing to the auth login CLI command.
|
|
274
|
+
"""
|
|
275
|
+
return f"Setup: {get_command('auth_login')}"
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def msg_or_run_auth_login():
|
|
279
|
+
"""
|
|
280
|
+
Return a short suggestion offering the `auth_login` command as an alternative to setup.
|
|
281
|
+
|
|
282
|
+
This function formats and returns a user-facing message that tells the caller to
|
|
283
|
+
run the configured `auth_login` CLI command to create or set up credentials.json.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
str: A message of the form "or run '<command>' to set up credentials.json".
|
|
287
|
+
"""
|
|
288
|
+
return f"or run '{get_command('auth_login')}' to set up credentials.json"
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def msg_setup_authentication():
|
|
292
|
+
"""Standard message for authentication setup."""
|
|
293
|
+
return f"Setup authentication: {get_command('auth_login')}"
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def msg_regenerate_credentials():
|
|
297
|
+
"""
|
|
298
|
+
Return a standardized instruction prompting the user to re-run the authentication command to regenerate credentials that include a `device_id`.
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
str: Message instructing the user to run the auth login command again to produce new credentials containing a `device_id`.
|
|
302
|
+
"""
|
|
303
|
+
return f"Please run '{get_command('auth_login')}' again to generate new credentials that include a device_id."
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# Helper functions moved from matrix_utils to break circular dependency
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _create_ssl_context():
|
|
310
|
+
"""
|
|
311
|
+
Create an SSLContext for Matrix client connections, preferring certifi's CA bundle when available.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
ssl.SSLContext | None: An SSLContext configured with certifi's CA file if certifi is present, otherwise the system default SSLContext. Returns None only if context creation fails.
|
|
315
|
+
"""
|
|
316
|
+
try:
|
|
317
|
+
if certifi:
|
|
318
|
+
return ssl.create_default_context(cafile=certifi.where())
|
|
319
|
+
else:
|
|
320
|
+
return ssl.create_default_context()
|
|
321
|
+
except Exception as e:
|
|
322
|
+
logger.warning(
|
|
323
|
+
f"Failed to create certifi-backed SSL context, falling back to system default: {e}"
|
|
324
|
+
)
|
|
325
|
+
try:
|
|
326
|
+
return ssl.create_default_context()
|
|
327
|
+
except Exception as fallback_e:
|
|
328
|
+
logger.error(f"Failed to create system default SSL context: {fallback_e}")
|
|
329
|
+
return None
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _cleanup_local_session_data():
|
|
333
|
+
"""
|
|
334
|
+
Remove local Matrix session artifacts: credentials.json and any E2EE store directories.
|
|
335
|
+
|
|
336
|
+
This cleans up the on-disk session state used by the Matrix client. It removes:
|
|
337
|
+
- the credentials file at <base_dir>/credentials.json (if present), and
|
|
338
|
+
- E2EE store directories: the default store dir returned by get_e2ee_store_dir()
|
|
339
|
+
plus any user-configured overrides found in the loaded config under
|
|
340
|
+
matrix.e2ee.store_path or matrix.encryption.store_path.
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
bool: True if all targeted files/directories were removed successfully;
|
|
344
|
+
False if any removal failed (for example due to permissions). The
|
|
345
|
+
function makes a best-effort attempt and will still try all removals
|
|
346
|
+
even if some fail.
|
|
347
|
+
"""
|
|
348
|
+
import shutil
|
|
349
|
+
|
|
350
|
+
from mmrelay.config import get_base_dir, get_e2ee_store_dir
|
|
351
|
+
|
|
352
|
+
logger.info("Clearing local session data...")
|
|
353
|
+
success = True
|
|
354
|
+
|
|
355
|
+
# Remove credentials.json
|
|
356
|
+
config_dir = get_base_dir()
|
|
357
|
+
credentials_path = os.path.join(config_dir, "credentials.json")
|
|
358
|
+
|
|
359
|
+
if os.path.exists(credentials_path):
|
|
360
|
+
try:
|
|
361
|
+
os.remove(credentials_path)
|
|
362
|
+
logger.info(f"Removed credentials file: {credentials_path}")
|
|
363
|
+
except (OSError, PermissionError) as e:
|
|
364
|
+
logger.error(f"Failed to remove credentials file: {e}")
|
|
365
|
+
success = False
|
|
366
|
+
else:
|
|
367
|
+
logger.info("No credentials file found to remove")
|
|
368
|
+
|
|
369
|
+
# Clear E2EE store directory (default and any configured override)
|
|
370
|
+
candidate_store_paths = {get_e2ee_store_dir()}
|
|
371
|
+
try:
|
|
372
|
+
from mmrelay.config import load_config
|
|
373
|
+
|
|
374
|
+
cfg = load_config(args=None) or {}
|
|
375
|
+
matrix_cfg = cfg.get("matrix", {})
|
|
376
|
+
for section in ("e2ee", "encryption"):
|
|
377
|
+
override = os.path.expanduser(
|
|
378
|
+
matrix_cfg.get(section, {}).get("store_path", "")
|
|
379
|
+
)
|
|
380
|
+
if override:
|
|
381
|
+
candidate_store_paths.add(override)
|
|
382
|
+
except Exception as e:
|
|
383
|
+
logger.debug(
|
|
384
|
+
f"Could not resolve configured E2EE store path: {type(e).__name__}"
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
any_store_found = False
|
|
388
|
+
for store_path in sorted(candidate_store_paths):
|
|
389
|
+
if os.path.exists(store_path):
|
|
390
|
+
any_store_found = True
|
|
391
|
+
try:
|
|
392
|
+
shutil.rmtree(store_path)
|
|
393
|
+
logger.info(f"Removed E2EE store directory: {store_path}")
|
|
394
|
+
except (OSError, PermissionError) as e:
|
|
395
|
+
logger.error(
|
|
396
|
+
f"Failed to remove E2EE store directory '{store_path}': {e}"
|
|
397
|
+
)
|
|
398
|
+
success = False
|
|
399
|
+
if not any_store_found:
|
|
400
|
+
logger.info("No E2EE store directory found to remove")
|
|
401
|
+
|
|
402
|
+
if success:
|
|
403
|
+
logger.info("✅ Logout completed successfully!")
|
|
404
|
+
logger.info("All Matrix sessions and local data have been cleared.")
|
|
405
|
+
logger.info("Run 'mmrelay auth login' to authenticate again.")
|
|
406
|
+
else:
|
|
407
|
+
logger.warning("Logout completed with some errors.")
|
|
408
|
+
logger.warning("Some files may not have been removed due to permission issues.")
|
|
409
|
+
|
|
410
|
+
return success
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
# CLI-specific functions (can use print statements for user interaction)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _handle_matrix_error(exception: Exception, context: str, log_level: str = "error"):
|
|
417
|
+
"""
|
|
418
|
+
Classify a Matrix-related exception and emit user-facing and logged messages.
|
|
419
|
+
|
|
420
|
+
Determines whether the provided exception represents credential, network,
|
|
421
|
+
server, or other errors (using known nio exception types or message inspection),
|
|
422
|
+
chooses messages appropriate to the given context (verification vs non-verification),
|
|
423
|
+
logs them at the specified level ("error" or "warning"), prints concise feedback
|
|
424
|
+
for CLI users, and signals the exception was handled.
|
|
425
|
+
|
|
426
|
+
Parameters:
|
|
427
|
+
exception: The exception instance to classify and report.
|
|
428
|
+
context: Short context string describing the operation (e.g., "Password verification",
|
|
429
|
+
"Server logout"); used to select phrasing and to detect verification flows.
|
|
430
|
+
log_level: Logging level to use; accepted values are "error" (default) or "warning".
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
bool: Always returns True to indicate the exception was handled and reported.
|
|
434
|
+
"""
|
|
435
|
+
log_func = logger.error if log_level == "error" else logger.warning
|
|
436
|
+
emoji = "❌" if log_level == "error" else "⚠️ "
|
|
437
|
+
is_verification = "verification" in context.lower()
|
|
438
|
+
|
|
439
|
+
# Determine error category and details
|
|
440
|
+
error_category = None
|
|
441
|
+
error_detail = None
|
|
442
|
+
|
|
443
|
+
# Handle specific Matrix-nio exceptions
|
|
444
|
+
if isinstance(exception, (NioLoginError, NioLogoutError)) and hasattr(
|
|
445
|
+
exception, "status_code"
|
|
446
|
+
):
|
|
447
|
+
if (
|
|
448
|
+
hasattr(exception, "errcode") and exception.errcode == "M_FORBIDDEN"
|
|
449
|
+
) or exception.status_code == 401:
|
|
450
|
+
error_category = "credentials"
|
|
451
|
+
elif exception.status_code in [500, 502, 503]:
|
|
452
|
+
error_category = "server"
|
|
453
|
+
else:
|
|
454
|
+
error_category = "other"
|
|
455
|
+
error_detail = str(exception.status_code)
|
|
456
|
+
# Handle network/transport exceptions
|
|
457
|
+
elif isinstance(
|
|
458
|
+
exception,
|
|
459
|
+
(
|
|
460
|
+
NioLocalTransportError,
|
|
461
|
+
NioRemoteTransportError,
|
|
462
|
+
NioLocalProtocolError,
|
|
463
|
+
NioRemoteProtocolError,
|
|
464
|
+
),
|
|
465
|
+
):
|
|
466
|
+
error_category = "network"
|
|
467
|
+
else:
|
|
468
|
+
# Fallback to string matching for unknown exceptions
|
|
469
|
+
error_msg = str(exception).lower()
|
|
470
|
+
if "forbidden" in error_msg or "401" in error_msg:
|
|
471
|
+
error_category = "credentials"
|
|
472
|
+
elif (
|
|
473
|
+
"network" in error_msg
|
|
474
|
+
or "connection" in error_msg
|
|
475
|
+
or "timeout" in error_msg
|
|
476
|
+
):
|
|
477
|
+
error_category = "network"
|
|
478
|
+
elif (
|
|
479
|
+
"server" in error_msg
|
|
480
|
+
or "500" in error_msg
|
|
481
|
+
or "502" in error_msg
|
|
482
|
+
or "503" in error_msg
|
|
483
|
+
):
|
|
484
|
+
error_category = "server"
|
|
485
|
+
else:
|
|
486
|
+
error_category = "other"
|
|
487
|
+
error_detail = type(exception).__name__
|
|
488
|
+
|
|
489
|
+
# Generate appropriate messages based on category and context
|
|
490
|
+
if error_category == "credentials":
|
|
491
|
+
if is_verification:
|
|
492
|
+
log_func(f"{context} failed: Invalid credentials.")
|
|
493
|
+
log_func("Please check your username and password.")
|
|
494
|
+
print(f"{emoji} {context} failed: Invalid credentials.")
|
|
495
|
+
print("Please check your username and password.")
|
|
496
|
+
else:
|
|
497
|
+
log_func(
|
|
498
|
+
f"{context} failed due to invalid token (already logged out?), proceeding with local cleanup."
|
|
499
|
+
)
|
|
500
|
+
print(
|
|
501
|
+
f"{emoji} {context} failed due to invalid token (already logged out?), proceeding with local cleanup."
|
|
502
|
+
)
|
|
503
|
+
elif error_category == "network":
|
|
504
|
+
if is_verification:
|
|
505
|
+
log_func(f"{context} failed: Network connection error.")
|
|
506
|
+
log_func(
|
|
507
|
+
"Please check your internet connection and Matrix server availability."
|
|
508
|
+
)
|
|
509
|
+
print(f"{emoji} {context} failed: Network connection error.")
|
|
510
|
+
print(
|
|
511
|
+
"Please check your internet connection and Matrix server availability."
|
|
512
|
+
)
|
|
513
|
+
else:
|
|
514
|
+
log_func(
|
|
515
|
+
f"{context} failed due to network issues, proceeding with local cleanup."
|
|
516
|
+
)
|
|
517
|
+
print(
|
|
518
|
+
f"{emoji} {context} failed due to network issues, proceeding with local cleanup."
|
|
519
|
+
)
|
|
520
|
+
elif error_category == "server":
|
|
521
|
+
if is_verification:
|
|
522
|
+
log_func(f"{context} failed: Matrix server error.")
|
|
523
|
+
log_func(
|
|
524
|
+
"Please try again later or contact your Matrix server administrator."
|
|
525
|
+
)
|
|
526
|
+
print(f"{emoji} {context} failed: Matrix server error.")
|
|
527
|
+
print("Please try again later or contact your Matrix server administrator.")
|
|
528
|
+
else:
|
|
529
|
+
log_func(
|
|
530
|
+
f"{context} failed due to server error, proceeding with local cleanup."
|
|
531
|
+
)
|
|
532
|
+
print(
|
|
533
|
+
f"{emoji} {context} failed due to server error, proceeding with local cleanup."
|
|
534
|
+
)
|
|
535
|
+
else: # error_category == "other"
|
|
536
|
+
if is_verification:
|
|
537
|
+
log_func(f"{context} failed: {error_detail or 'Unknown error'}")
|
|
538
|
+
logger.debug(f"Full error details: {exception}")
|
|
539
|
+
print(f"{emoji} {context} failed: {error_detail or 'Unknown error'}")
|
|
540
|
+
else:
|
|
541
|
+
log_func(
|
|
542
|
+
f"{context} failed ({error_detail or 'Unknown error'}), proceeding with local cleanup."
|
|
543
|
+
)
|
|
544
|
+
print(
|
|
545
|
+
f"{emoji} {context} failed ({error_detail or 'Unknown error'}), proceeding with local cleanup."
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
return True
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
async def logout_matrix_bot(password: str):
|
|
552
|
+
"""
|
|
553
|
+
Log out the configured Matrix account and remove local session data.
|
|
554
|
+
|
|
555
|
+
Verifies the provided Matrix password by performing a temporary login, then attempts to
|
|
556
|
+
log out the active session on the homeserver (invalidating the access token) and remove
|
|
557
|
+
local session artifacts (credentials.json and any E2EE store). If there is no active
|
|
558
|
+
session, the function reports that and returns True.
|
|
559
|
+
|
|
560
|
+
Parameters:
|
|
561
|
+
password (str): The Matrix account password used to verify the session before logout.
|
|
562
|
+
|
|
563
|
+
Returns:
|
|
564
|
+
bool: True when local cleanup (and server logout, if applicable) completed successfully;
|
|
565
|
+
False on failure. If matrix-nio is not installed, prints an error and returns False.
|
|
566
|
+
"""
|
|
567
|
+
|
|
568
|
+
# Import inside function to avoid circular imports
|
|
569
|
+
from mmrelay.matrix_utils import (
|
|
570
|
+
MATRIX_LOGIN_TIMEOUT,
|
|
571
|
+
load_credentials,
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
# Check if matrix-nio is available
|
|
575
|
+
if AsyncClient is None:
|
|
576
|
+
logger.error("Matrix-nio library not available. Cannot perform logout.")
|
|
577
|
+
print("❌ Matrix-nio library not available. Cannot perform logout.")
|
|
578
|
+
return False
|
|
579
|
+
|
|
580
|
+
# Load current credentials
|
|
581
|
+
credentials = load_credentials()
|
|
582
|
+
if not credentials:
|
|
583
|
+
logger.info("No active session found. Already logged out.")
|
|
584
|
+
print("ℹ️ No active session found. Already logged out.")
|
|
585
|
+
return True
|
|
586
|
+
|
|
587
|
+
homeserver = credentials.get("homeserver")
|
|
588
|
+
user_id = credentials.get("user_id")
|
|
589
|
+
access_token = credentials.get("access_token")
|
|
590
|
+
device_id = credentials.get("device_id")
|
|
591
|
+
|
|
592
|
+
if not all([homeserver, user_id, access_token, device_id]):
|
|
593
|
+
logger.error("Invalid credentials found. Cannot verify logout.")
|
|
594
|
+
logger.info("Proceeding with local cleanup only...")
|
|
595
|
+
print("⚠️ Invalid credentials found. Cannot verify logout.")
|
|
596
|
+
print("Proceeding with local cleanup only...")
|
|
597
|
+
|
|
598
|
+
# Still try to clean up local files
|
|
599
|
+
success = _cleanup_local_session_data()
|
|
600
|
+
if success:
|
|
601
|
+
print("✅ Local cleanup completed successfully!")
|
|
602
|
+
else:
|
|
603
|
+
print("❌ Local cleanup completed with some errors.")
|
|
604
|
+
return success
|
|
605
|
+
|
|
606
|
+
logger.info(f"Verifying password for {user_id}...")
|
|
607
|
+
print(f"🔐 Verifying password for {user_id}...")
|
|
608
|
+
|
|
609
|
+
try:
|
|
610
|
+
# Create SSL context using certifi's certificates
|
|
611
|
+
ssl_context = _create_ssl_context()
|
|
612
|
+
if ssl_context is None:
|
|
613
|
+
logger.warning(
|
|
614
|
+
"Failed to create SSL context for password verification; falling back to default system SSL"
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
# Create a temporary client to verify the password
|
|
618
|
+
# We'll try to login with the password to verify it's correct
|
|
619
|
+
temp_client = AsyncClient(homeserver, user_id, ssl=ssl_context)
|
|
620
|
+
|
|
621
|
+
try:
|
|
622
|
+
# Attempt login with the provided password
|
|
623
|
+
response = await asyncio.wait_for(
|
|
624
|
+
temp_client.login(password, device_name="mmrelay-logout-verify"),
|
|
625
|
+
timeout=MATRIX_LOGIN_TIMEOUT,
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
if hasattr(response, "access_token"):
|
|
629
|
+
logger.info("Password verified successfully.")
|
|
630
|
+
print("✅ Password verified successfully.")
|
|
631
|
+
|
|
632
|
+
# Immediately logout the temporary session
|
|
633
|
+
await temp_client.logout()
|
|
634
|
+
else:
|
|
635
|
+
logger.error("Password verification failed.")
|
|
636
|
+
print("❌ Password verification failed.")
|
|
637
|
+
return False
|
|
638
|
+
|
|
639
|
+
except asyncio.TimeoutError:
|
|
640
|
+
logger.error(
|
|
641
|
+
"Password verification timed out. Please check your network connection."
|
|
642
|
+
)
|
|
643
|
+
print(
|
|
644
|
+
"❌ Password verification timed out. Please check your network connection."
|
|
645
|
+
)
|
|
646
|
+
return False
|
|
647
|
+
except Exception as e:
|
|
648
|
+
_handle_matrix_error(e, "Password verification", "error")
|
|
649
|
+
return False
|
|
650
|
+
finally:
|
|
651
|
+
await temp_client.close()
|
|
652
|
+
|
|
653
|
+
# Now logout the main session
|
|
654
|
+
logger.info("Logging out from Matrix server...")
|
|
655
|
+
print("🚪 Logging out from Matrix server...")
|
|
656
|
+
main_client = AsyncClient(homeserver, user_id, ssl=ssl_context)
|
|
657
|
+
main_client.restore_login(
|
|
658
|
+
user_id=user_id,
|
|
659
|
+
device_id=device_id,
|
|
660
|
+
access_token=access_token,
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
try:
|
|
664
|
+
# Logout from the server (invalidates the access token)
|
|
665
|
+
logout_response = await main_client.logout()
|
|
666
|
+
if hasattr(logout_response, "transport_response"):
|
|
667
|
+
logger.info("Successfully logged out from Matrix server.")
|
|
668
|
+
print("✅ Successfully logged out from Matrix server.")
|
|
669
|
+
else:
|
|
670
|
+
logger.warning(
|
|
671
|
+
"Logout response unclear, proceeding with local cleanup."
|
|
672
|
+
)
|
|
673
|
+
print("⚠️ Logout response unclear, proceeding with local cleanup.")
|
|
674
|
+
except Exception as e:
|
|
675
|
+
_handle_matrix_error(e, "Server logout", "warning")
|
|
676
|
+
logger.debug(f"Logout error details: {e}")
|
|
677
|
+
finally:
|
|
678
|
+
await main_client.close()
|
|
679
|
+
|
|
680
|
+
# Clear local session data
|
|
681
|
+
success = _cleanup_local_session_data()
|
|
682
|
+
if success:
|
|
683
|
+
print()
|
|
684
|
+
print("✅ Logout completed successfully!")
|
|
685
|
+
print("All Matrix sessions and local data have been cleared.")
|
|
686
|
+
print("Run 'mmrelay auth login' to authenticate again.")
|
|
687
|
+
else:
|
|
688
|
+
print()
|
|
689
|
+
print("⚠️ Logout completed with some errors.")
|
|
690
|
+
print("Some files may not have been removed due to permission issues.")
|
|
691
|
+
return success
|
|
692
|
+
|
|
693
|
+
except Exception as e:
|
|
694
|
+
logger.error(f"Error during logout process: {e}")
|
|
695
|
+
print(f"❌ Error during logout process: {e}")
|
|
696
|
+
return False
|