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.
- mmrelay/__init__.py +5 -0
- mmrelay/__main__.py +29 -0
- mmrelay/cli.py +2013 -0
- mmrelay/cli_utils.py +746 -0
- mmrelay/config.py +956 -0
- mmrelay/constants/__init__.py +65 -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 +45 -0
- mmrelay/constants/plugins.py +42 -0
- mmrelay/constants/queue.py +20 -0
- mmrelay/db_runtime.py +269 -0
- mmrelay/db_utils.py +1017 -0
- mmrelay/e2ee_utils.py +400 -0
- mmrelay/log_utils.py +274 -0
- mmrelay/main.py +439 -0
- mmrelay/matrix_utils.py +3091 -0
- mmrelay/meshtastic_utils.py +1245 -0
- mmrelay/message_queue.py +647 -0
- mmrelay/plugin_loader.py +1933 -0
- mmrelay/plugins/__init__.py +3 -0
- mmrelay/plugins/base_plugin.py +638 -0
- mmrelay/plugins/debug_plugin.py +30 -0
- mmrelay/plugins/drop_plugin.py +127 -0
- mmrelay/plugins/health_plugin.py +64 -0
- mmrelay/plugins/help_plugin.py +79 -0
- mmrelay/plugins/map_plugin.py +353 -0
- mmrelay/plugins/mesh_relay_plugin.py +222 -0
- mmrelay/plugins/nodes_plugin.py +92 -0
- mmrelay/plugins/ping_plugin.py +128 -0
- mmrelay/plugins/telemetry_plugin.py +179 -0
- mmrelay/plugins/weather_plugin.py +312 -0
- mmrelay/runtime_utils.py +35 -0
- mmrelay/setup_utils.py +828 -0
- mmrelay/tools/__init__.py +27 -0
- mmrelay/tools/mmrelay.service +19 -0
- mmrelay/tools/sample-docker-compose-prebuilt.yaml +30 -0
- mmrelay/tools/sample-docker-compose.yaml +30 -0
- mmrelay/tools/sample.env +10 -0
- mmrelay/tools/sample_config.yaml +120 -0
- mmrelay/windows_utils.py +346 -0
- mmrelay-1.2.6.dist-info/METADATA +145 -0
- mmrelay-1.2.6.dist-info/RECORD +50 -0
- mmrelay-1.2.6.dist-info/WHEEL +5 -0
- mmrelay-1.2.6.dist-info/entry_points.txt +2 -0
- mmrelay-1.2.6.dist-info/licenses/LICENSE +675 -0
- mmrelay-1.2.6.dist-info/top_level.txt +1 -0
mmrelay/db_utils.py
ADDED
|
@@ -0,0 +1,1017 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import sqlite3
|
|
5
|
+
import threading
|
|
6
|
+
from typing import Any, Dict, Tuple
|
|
7
|
+
|
|
8
|
+
from mmrelay.config import get_data_dir
|
|
9
|
+
from mmrelay.db_runtime import DatabaseManager
|
|
10
|
+
from mmrelay.log_utils import get_logger
|
|
11
|
+
|
|
12
|
+
# Global config variable that will be set from main.py
|
|
13
|
+
config = None
|
|
14
|
+
|
|
15
|
+
# Cache for database path to avoid repeated logging and path resolution
|
|
16
|
+
_cached_db_path = None
|
|
17
|
+
_db_path_logged = False
|
|
18
|
+
_cached_config_hash = None
|
|
19
|
+
|
|
20
|
+
# Database manager cache
|
|
21
|
+
_db_manager: DatabaseManager | None = None
|
|
22
|
+
_db_manager_signature: Tuple[str, bool, int, Tuple[Tuple[str, Any], ...]] | None = None
|
|
23
|
+
_db_manager_lock = threading.Lock()
|
|
24
|
+
|
|
25
|
+
DEFAULT_ENABLE_WAL = True
|
|
26
|
+
DEFAULT_BUSY_TIMEOUT_MS = 5000
|
|
27
|
+
DEFAULT_EXTRA_PRAGMAS: Dict[str, Any] = {
|
|
28
|
+
"synchronous": "NORMAL",
|
|
29
|
+
"temp_store": "MEMORY",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
logger = get_logger(name="db_utils")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def clear_db_path_cache():
|
|
36
|
+
"""Clear the cached database path to force re-resolution on next call.
|
|
37
|
+
|
|
38
|
+
This is useful for testing or if the application supports runtime
|
|
39
|
+
configuration changes.
|
|
40
|
+
"""
|
|
41
|
+
global _cached_db_path, _db_path_logged, _cached_config_hash
|
|
42
|
+
_cached_db_path = None
|
|
43
|
+
_db_path_logged = False
|
|
44
|
+
_cached_config_hash = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Get the database path
|
|
48
|
+
def get_db_path():
|
|
49
|
+
"""
|
|
50
|
+
Resolve and return the absolute filesystem path to the SQLite database.
|
|
51
|
+
|
|
52
|
+
Prefers a user-configured path (config["database"]["path"]), falls back to the legacy config["db"]["path"], and otherwise uses the default file meshtastic.sqlite in the application data directory returned by get_data_dir(). The result is cached and the cache is invalidated when relevant config sections change. Attempts to create parent directories for configured or default paths; directory creation failures are logged as warnings but are not raised here.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
str: The filesystem path to the SQLite database.
|
|
56
|
+
"""
|
|
57
|
+
global config, _cached_db_path, _db_path_logged, _cached_config_hash
|
|
58
|
+
|
|
59
|
+
# Create a deterministic JSON representation of relevant config sections to detect changes
|
|
60
|
+
current_config_hash = None
|
|
61
|
+
if config is not None:
|
|
62
|
+
# Use only the database-related config sections
|
|
63
|
+
db_config = {
|
|
64
|
+
"database": config.get("database", {}),
|
|
65
|
+
"db": config.get("db", {}), # Legacy format
|
|
66
|
+
}
|
|
67
|
+
current_config_hash = json.dumps(db_config, sort_keys=True)
|
|
68
|
+
|
|
69
|
+
# Check if cache is valid (path exists and config hasn't changed)
|
|
70
|
+
if _cached_db_path is not None and current_config_hash == _cached_config_hash:
|
|
71
|
+
return _cached_db_path
|
|
72
|
+
|
|
73
|
+
# Config changed or first call - clear cache and re-resolve
|
|
74
|
+
if current_config_hash != _cached_config_hash:
|
|
75
|
+
_cached_db_path = None
|
|
76
|
+
_db_path_logged = False
|
|
77
|
+
_cached_config_hash = current_config_hash
|
|
78
|
+
|
|
79
|
+
# Check if config is available
|
|
80
|
+
if config is not None:
|
|
81
|
+
# Check if database path is specified in config (preferred format)
|
|
82
|
+
if "database" in config and "path" in config["database"]:
|
|
83
|
+
custom_path = config["database"]["path"]
|
|
84
|
+
if custom_path:
|
|
85
|
+
# Ensure the directory exists
|
|
86
|
+
db_dir = os.path.dirname(custom_path)
|
|
87
|
+
if db_dir:
|
|
88
|
+
try:
|
|
89
|
+
os.makedirs(db_dir, exist_ok=True)
|
|
90
|
+
except (OSError, PermissionError) as e:
|
|
91
|
+
logger.warning(
|
|
92
|
+
"Could not create database directory %s: %s", db_dir, e
|
|
93
|
+
)
|
|
94
|
+
# Continue anyway - the database connection will fail later if needed
|
|
95
|
+
|
|
96
|
+
# Cache the path and log only once
|
|
97
|
+
_cached_db_path = custom_path
|
|
98
|
+
if not _db_path_logged:
|
|
99
|
+
logger.info("Using database path from config: %s", custom_path)
|
|
100
|
+
_db_path_logged = True
|
|
101
|
+
return custom_path
|
|
102
|
+
|
|
103
|
+
# Check legacy format (db section)
|
|
104
|
+
if "db" in config and "path" in config["db"]:
|
|
105
|
+
custom_path = config["db"]["path"]
|
|
106
|
+
if custom_path:
|
|
107
|
+
# Ensure the directory exists
|
|
108
|
+
db_dir = os.path.dirname(custom_path)
|
|
109
|
+
if db_dir:
|
|
110
|
+
try:
|
|
111
|
+
os.makedirs(db_dir, exist_ok=True)
|
|
112
|
+
except (OSError, PermissionError) as e:
|
|
113
|
+
logger.warning(
|
|
114
|
+
"Could not create database directory %s: %s", db_dir, e
|
|
115
|
+
)
|
|
116
|
+
# Continue anyway - the database connection will fail later if needed
|
|
117
|
+
|
|
118
|
+
# Cache the path and log only once
|
|
119
|
+
_cached_db_path = custom_path
|
|
120
|
+
if not _db_path_logged:
|
|
121
|
+
logger.warning(
|
|
122
|
+
"Using 'db.path' configuration (legacy). 'database.path' is now the preferred format and 'db.path' will be deprecated in a future version."
|
|
123
|
+
)
|
|
124
|
+
_db_path_logged = True
|
|
125
|
+
return custom_path
|
|
126
|
+
|
|
127
|
+
# Use the standard data directory
|
|
128
|
+
data_dir = get_data_dir()
|
|
129
|
+
# Ensure the data directory exists before using it
|
|
130
|
+
try:
|
|
131
|
+
os.makedirs(data_dir, exist_ok=True)
|
|
132
|
+
except (OSError, PermissionError) as e:
|
|
133
|
+
logger.warning("Could not create data directory %s: %s", data_dir, e)
|
|
134
|
+
# Continue anyway - the database connection will fail later if needed
|
|
135
|
+
default_path = os.path.join(data_dir, "meshtastic.sqlite")
|
|
136
|
+
_cached_db_path = default_path
|
|
137
|
+
return default_path
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _close_manager_safely(manager: DatabaseManager | None) -> None:
|
|
141
|
+
"""
|
|
142
|
+
Safely close a DatabaseManager instance.
|
|
143
|
+
|
|
144
|
+
Parameters:
|
|
145
|
+
manager: The manager to close, or None.
|
|
146
|
+
"""
|
|
147
|
+
if manager:
|
|
148
|
+
with contextlib.suppress(Exception):
|
|
149
|
+
manager.close()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _reset_db_manager():
|
|
153
|
+
"""
|
|
154
|
+
Reset the database manager instance.
|
|
155
|
+
Used for testing and configuration changes.
|
|
156
|
+
"""
|
|
157
|
+
global _db_manager, _db_manager_signature
|
|
158
|
+
manager_to_close = None
|
|
159
|
+
with _db_manager_lock:
|
|
160
|
+
if _db_manager is not None:
|
|
161
|
+
manager_to_close = _db_manager
|
|
162
|
+
_db_manager = None
|
|
163
|
+
_db_manager_signature = None
|
|
164
|
+
|
|
165
|
+
# Close old manager inside the lock to prevent race condition
|
|
166
|
+
# where another thread might be using connections from the old manager
|
|
167
|
+
_close_manager_safely(manager_to_close)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _parse_bool(value, default):
|
|
171
|
+
"""
|
|
172
|
+
Parse a value into a boolean using common representations.
|
|
173
|
+
|
|
174
|
+
Parameters:
|
|
175
|
+
value: The input to interpret; typically a bool or string. Common true strings: "1", "true", "yes", "on" (case-insensitive). Common false strings: "0", "false", "no", "off" (case-insensitive).
|
|
176
|
+
default (bool): Fallback value returned when `value` is not a boolean and does not match any recognized string representations.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
bool: `True` if `value` represents true, `False` if it represents false, otherwise `default`.
|
|
180
|
+
"""
|
|
181
|
+
if isinstance(value, bool):
|
|
182
|
+
return value
|
|
183
|
+
if isinstance(value, str):
|
|
184
|
+
lowered = value.strip().lower()
|
|
185
|
+
if lowered in {"1", "true", "yes", "on"}:
|
|
186
|
+
return True
|
|
187
|
+
if lowered in {"0", "false", "no", "off"}:
|
|
188
|
+
return False
|
|
189
|
+
return default
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _parse_int(value, default):
|
|
193
|
+
"""
|
|
194
|
+
Parse a value as an integer and return a fallback if parsing fails.
|
|
195
|
+
|
|
196
|
+
Parameters:
|
|
197
|
+
value: The value to convert to int (may be any type).
|
|
198
|
+
default (int): The value to return if `value` cannot be parsed as an integer.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
int: The parsed integer from `value`, or `default` if parsing raises TypeError or ValueError.
|
|
202
|
+
"""
|
|
203
|
+
try:
|
|
204
|
+
return int(value)
|
|
205
|
+
except (TypeError, ValueError):
|
|
206
|
+
return default
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _resolve_database_options() -> Tuple[bool, int, Dict[str, Any]]:
|
|
210
|
+
"""
|
|
211
|
+
Resolve database options (WAL, busy timeout, and SQLite pragmas) from the global config, supporting legacy keys and falling back to module defaults.
|
|
212
|
+
|
|
213
|
+
Reads values from config["database"] with fallback to legacy config["db"], parses boolean and integer settings, and merges any provided pragmas on top of DEFAULT_EXTRA_PRAGMAS.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
enable_wal (bool): `True` if write-ahead logging should be enabled, `False` otherwise.
|
|
217
|
+
busy_timeout_ms (int): Busy timeout in milliseconds to use for SQLite connections.
|
|
218
|
+
extra_pragmas (dict): Mapping of pragma names to values, starting from DEFAULT_EXTRA_PRAGMAS and overridden by config-provided pragmas.
|
|
219
|
+
"""
|
|
220
|
+
database_cfg = config.get("database", {}) if isinstance(config, dict) else {}
|
|
221
|
+
legacy_cfg = config.get("db", {}) if isinstance(config, dict) else {}
|
|
222
|
+
|
|
223
|
+
enable_wal = _parse_bool(
|
|
224
|
+
database_cfg.get(
|
|
225
|
+
"enable_wal", legacy_cfg.get("enable_wal", DEFAULT_ENABLE_WAL)
|
|
226
|
+
),
|
|
227
|
+
DEFAULT_ENABLE_WAL,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
busy_timeout_ms = _parse_int(
|
|
231
|
+
database_cfg.get(
|
|
232
|
+
"busy_timeout_ms",
|
|
233
|
+
legacy_cfg.get("busy_timeout_ms", DEFAULT_BUSY_TIMEOUT_MS),
|
|
234
|
+
),
|
|
235
|
+
DEFAULT_BUSY_TIMEOUT_MS,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
extra_pragmas = dict(DEFAULT_EXTRA_PRAGMAS)
|
|
239
|
+
pragmas_cfg = database_cfg.get("pragmas", legacy_cfg.get("pragmas"))
|
|
240
|
+
if isinstance(pragmas_cfg, dict):
|
|
241
|
+
for pragma, value in pragmas_cfg.items():
|
|
242
|
+
extra_pragmas[str(pragma)] = value
|
|
243
|
+
|
|
244
|
+
return enable_wal, busy_timeout_ms, extra_pragmas
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _get_db_manager() -> DatabaseManager:
|
|
248
|
+
"""
|
|
249
|
+
Obtain the global DatabaseManager, creating or replacing it when the resolved database path or options change.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
DatabaseManager: The cached DatabaseManager instance configured for the current database path and options.
|
|
253
|
+
|
|
254
|
+
Raises:
|
|
255
|
+
RuntimeError: If the DatabaseManager could not be initialized.
|
|
256
|
+
"""
|
|
257
|
+
global _db_manager, _db_manager_signature
|
|
258
|
+
path = get_db_path()
|
|
259
|
+
enable_wal, busy_timeout_ms, extra_pragmas = _resolve_database_options()
|
|
260
|
+
signature = (
|
|
261
|
+
path,
|
|
262
|
+
enable_wal,
|
|
263
|
+
busy_timeout_ms,
|
|
264
|
+
tuple(sorted(extra_pragmas.items())),
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
manager_to_close = None
|
|
268
|
+
with _db_manager_lock:
|
|
269
|
+
if _db_manager is None or _db_manager_signature != signature:
|
|
270
|
+
try:
|
|
271
|
+
new_manager = DatabaseManager(
|
|
272
|
+
path,
|
|
273
|
+
enable_wal=enable_wal,
|
|
274
|
+
busy_timeout_ms=busy_timeout_ms,
|
|
275
|
+
extra_pragmas=extra_pragmas,
|
|
276
|
+
)
|
|
277
|
+
# Successfully created a new manager, now swap it with the old one.
|
|
278
|
+
manager_to_close = _db_manager
|
|
279
|
+
_db_manager = new_manager
|
|
280
|
+
_db_manager_signature = signature
|
|
281
|
+
_close_manager_safely(manager_to_close)
|
|
282
|
+
except (KeyboardInterrupt, SystemExit):
|
|
283
|
+
raise
|
|
284
|
+
except Exception:
|
|
285
|
+
if _db_manager is None:
|
|
286
|
+
# First-time initialization failed, so we cannot proceed.
|
|
287
|
+
raise
|
|
288
|
+
|
|
289
|
+
# A configuration change failed. Log the error but continue with the old manager
|
|
290
|
+
# to keep the application alive.
|
|
291
|
+
logger.exception(
|
|
292
|
+
"Failed to create new DatabaseManager with updated configuration. "
|
|
293
|
+
"The application will continue using the previous database settings."
|
|
294
|
+
)
|
|
295
|
+
# Leave _db_manager_signature unchanged so a future call will retry once the issue is resolved.
|
|
296
|
+
|
|
297
|
+
# Critical: Final check and return must be inside the lock to prevent race condition.
|
|
298
|
+
# Without this, _reset_db_manager() could set _db_manager = None after we release
|
|
299
|
+
# the lock but before we return, causing an unexpected RuntimeError.
|
|
300
|
+
if _db_manager is None:
|
|
301
|
+
raise RuntimeError("Database manager initialization failed")
|
|
302
|
+
return _db_manager
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# Initialize SQLite database
|
|
306
|
+
def initialize_database():
|
|
307
|
+
"""
|
|
308
|
+
Initializes the SQLite database schema for the relay application.
|
|
309
|
+
|
|
310
|
+
Creates required tables (`longnames`, `shortnames`, `plugin_data`, and `message_map`) if they do not exist, and ensures the `meshtastic_meshnet` column is present in `message_map`. Raises an exception if database initialization fails.
|
|
311
|
+
"""
|
|
312
|
+
db_path = get_db_path()
|
|
313
|
+
# Check if database exists
|
|
314
|
+
if os.path.exists(db_path):
|
|
315
|
+
logger.info("Loading database from: %s", db_path)
|
|
316
|
+
else:
|
|
317
|
+
logger.info("Creating new database at: %s", db_path)
|
|
318
|
+
manager = _get_db_manager()
|
|
319
|
+
|
|
320
|
+
def _initialize(cursor: sqlite3.Cursor) -> None:
|
|
321
|
+
"""
|
|
322
|
+
Create required SQLite tables for the application's schema and apply minimal schema migrations.
|
|
323
|
+
|
|
324
|
+
Creates tables: `longnames`, `shortnames`, `plugin_data`, and `message_map`. Attempts to add the
|
|
325
|
+
`meshtastic_meshnet` column and to create an index on `message_map(meshtastic_id)`; failures
|
|
326
|
+
from those upgrade attempts are ignored (safe no-op if already applied).
|
|
327
|
+
|
|
328
|
+
Parameters:
|
|
329
|
+
cursor: An sqlite3.Cursor positioned on the target database; used to execute DDL statements.
|
|
330
|
+
"""
|
|
331
|
+
cursor.execute(
|
|
332
|
+
"CREATE TABLE IF NOT EXISTS longnames (meshtastic_id TEXT PRIMARY KEY, longname TEXT)"
|
|
333
|
+
)
|
|
334
|
+
cursor.execute(
|
|
335
|
+
"CREATE TABLE IF NOT EXISTS shortnames (meshtastic_id TEXT PRIMARY KEY, shortname TEXT)"
|
|
336
|
+
)
|
|
337
|
+
cursor.execute(
|
|
338
|
+
"CREATE TABLE IF NOT EXISTS plugin_data (plugin_name TEXT, meshtastic_id TEXT, data TEXT, PRIMARY KEY (plugin_name, meshtastic_id))"
|
|
339
|
+
)
|
|
340
|
+
cursor.execute(
|
|
341
|
+
"CREATE TABLE IF NOT EXISTS message_map (meshtastic_id INTEGER, matrix_event_id TEXT PRIMARY KEY, matrix_room_id TEXT, meshtastic_text TEXT, meshtastic_meshnet TEXT)"
|
|
342
|
+
)
|
|
343
|
+
# Attempt schema adjustments for upgrades
|
|
344
|
+
try:
|
|
345
|
+
cursor.execute("ALTER TABLE message_map ADD COLUMN meshtastic_meshnet TEXT")
|
|
346
|
+
except sqlite3.OperationalError:
|
|
347
|
+
pass
|
|
348
|
+
|
|
349
|
+
try:
|
|
350
|
+
cursor.execute(
|
|
351
|
+
"CREATE INDEX IF NOT EXISTS idx_message_map_meshtastic_id ON message_map (meshtastic_id)"
|
|
352
|
+
)
|
|
353
|
+
except sqlite3.OperationalError:
|
|
354
|
+
pass
|
|
355
|
+
|
|
356
|
+
try:
|
|
357
|
+
manager.run_sync(_initialize, write=True)
|
|
358
|
+
except sqlite3.Error:
|
|
359
|
+
logger.exception("Database initialization failed")
|
|
360
|
+
raise
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def store_plugin_data(plugin_name, meshtastic_id, data):
|
|
364
|
+
"""
|
|
365
|
+
Store or update JSON-serialized plugin data for a specific plugin and Meshtastic ID in the database.
|
|
366
|
+
|
|
367
|
+
Parameters:
|
|
368
|
+
plugin_name (str): The name of the plugin.
|
|
369
|
+
meshtastic_id (str): The Meshtastic node identifier.
|
|
370
|
+
data (Any): The plugin data to be serialized and stored.
|
|
371
|
+
"""
|
|
372
|
+
manager = _get_db_manager()
|
|
373
|
+
|
|
374
|
+
# Serialize payload up front to surface JSON errors before opening a write txn
|
|
375
|
+
try:
|
|
376
|
+
payload = json.dumps(data)
|
|
377
|
+
except (TypeError, ValueError):
|
|
378
|
+
logger.exception(
|
|
379
|
+
"Plugin data for %s/%s is not JSON-serializable", plugin_name, meshtastic_id
|
|
380
|
+
)
|
|
381
|
+
return
|
|
382
|
+
|
|
383
|
+
def _store(cursor: sqlite3.Cursor) -> None:
|
|
384
|
+
"""
|
|
385
|
+
Store JSON-serialized plugin data for a specific plugin and Meshtastic node using the provided DB cursor.
|
|
386
|
+
|
|
387
|
+
Executes an INSERT (with ON CONFLICT DO UPDATE) into `plugin_data` for captured `plugin_name` and `meshtastic_id`, storing `data` serialized as JSON.
|
|
388
|
+
|
|
389
|
+
Parameters:
|
|
390
|
+
cursor (sqlite3.Cursor): Open database cursor used to execute the insert/update. The function uses `plugin_name`, `meshtastic_id`, and `payload` from the enclosing scope.
|
|
391
|
+
"""
|
|
392
|
+
cursor.execute(
|
|
393
|
+
"INSERT INTO plugin_data (plugin_name, meshtastic_id, data) VALUES (?, ?, ?) "
|
|
394
|
+
"ON CONFLICT (plugin_name, meshtastic_id) DO UPDATE SET data = excluded.data",
|
|
395
|
+
(plugin_name, meshtastic_id, payload),
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
try:
|
|
399
|
+
manager.run_sync(_store, write=True)
|
|
400
|
+
except sqlite3.Error:
|
|
401
|
+
logger.exception(
|
|
402
|
+
"Database error storing plugin data for %s, %s",
|
|
403
|
+
plugin_name,
|
|
404
|
+
meshtastic_id,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def delete_plugin_data(plugin_name, meshtastic_id):
|
|
409
|
+
"""
|
|
410
|
+
Deletes the plugin data entry for the specified plugin and Meshtastic ID from the database.
|
|
411
|
+
|
|
412
|
+
Parameters:
|
|
413
|
+
plugin_name (str): The name of the plugin whose data should be deleted.
|
|
414
|
+
meshtastic_id (str): The Meshtastic node ID associated with the plugin data.
|
|
415
|
+
"""
|
|
416
|
+
manager = _get_db_manager()
|
|
417
|
+
|
|
418
|
+
def _delete(cursor: sqlite3.Cursor) -> None:
|
|
419
|
+
"""
|
|
420
|
+
Delete the plugin_data row for the current `plugin_name` and `meshtastic_id` using the provided DB cursor.
|
|
421
|
+
|
|
422
|
+
Parameters:
|
|
423
|
+
cursor (sqlite3.Cursor): Active database cursor; the deletion is executed on this cursor and should be part of the caller's transaction.
|
|
424
|
+
"""
|
|
425
|
+
cursor.execute(
|
|
426
|
+
"DELETE FROM plugin_data WHERE plugin_name=? AND meshtastic_id=?",
|
|
427
|
+
(plugin_name, meshtastic_id),
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
try:
|
|
431
|
+
manager.run_sync(_delete, write=True)
|
|
432
|
+
except sqlite3.Error:
|
|
433
|
+
logger.exception(
|
|
434
|
+
"Database error deleting plugin data for %s, %s",
|
|
435
|
+
plugin_name,
|
|
436
|
+
meshtastic_id,
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def get_plugin_data_for_node(plugin_name, meshtastic_id):
|
|
441
|
+
"""
|
|
442
|
+
Retrieve JSON-encoded plugin data for a specific Meshtastic node.
|
|
443
|
+
|
|
444
|
+
Parameters:
|
|
445
|
+
plugin_name (str): Name of the plugin whose data to fetch.
|
|
446
|
+
meshtastic_id (int | str): Node identifier used in the plugin_data table.
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
list: The deserialized plugin data as a Python list; returns an empty list if no data is found or if decoding or database errors occur.
|
|
450
|
+
"""
|
|
451
|
+
manager = _get_db_manager()
|
|
452
|
+
|
|
453
|
+
def _fetch(cursor: sqlite3.Cursor):
|
|
454
|
+
"""Retrieve the first `data` column for a plugin/node pair using the provided DB cursor.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
cursor: An open sqlite3.Cursor used to execute the query.
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
A single row (sequence) containing the `data` column for the
|
|
461
|
+
matching plugin and Meshtastic ID, or `None` if no matching row exists.
|
|
462
|
+
"""
|
|
463
|
+
cursor.execute(
|
|
464
|
+
"SELECT data FROM plugin_data WHERE plugin_name=? AND meshtastic_id=?",
|
|
465
|
+
(plugin_name, meshtastic_id),
|
|
466
|
+
)
|
|
467
|
+
return cursor.fetchone()
|
|
468
|
+
|
|
469
|
+
try:
|
|
470
|
+
result = manager.run_sync(_fetch)
|
|
471
|
+
except (MemoryError, sqlite3.Error):
|
|
472
|
+
logger.exception(
|
|
473
|
+
"Database error retrieving plugin data for %s, node %s",
|
|
474
|
+
plugin_name,
|
|
475
|
+
meshtastic_id,
|
|
476
|
+
)
|
|
477
|
+
return []
|
|
478
|
+
|
|
479
|
+
try:
|
|
480
|
+
return json.loads(result[0] if result else "[]")
|
|
481
|
+
except (json.JSONDecodeError, TypeError):
|
|
482
|
+
logger.exception(
|
|
483
|
+
"Failed to decode JSON data for plugin %s, node %s",
|
|
484
|
+
plugin_name,
|
|
485
|
+
meshtastic_id,
|
|
486
|
+
)
|
|
487
|
+
return []
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def get_plugin_data(plugin_name):
|
|
491
|
+
"""
|
|
492
|
+
Retrieve all stored plugin data rows for a given plugin.
|
|
493
|
+
|
|
494
|
+
Parameters:
|
|
495
|
+
plugin_name (str): Name of the plugin to query.
|
|
496
|
+
|
|
497
|
+
Returns:
|
|
498
|
+
list[tuple]: Rows matching the plugin; each row is a single-item tuple containing the stored JSON string from the `data` column.
|
|
499
|
+
"""
|
|
500
|
+
manager = _get_db_manager()
|
|
501
|
+
|
|
502
|
+
def _fetch_all(cursor: sqlite3.Cursor):
|
|
503
|
+
"""Fetch all data rows for a plugin using the provided DB cursor.
|
|
504
|
+
|
|
505
|
+
Args:
|
|
506
|
+
cursor: An open sqlite3.Cursor used to execute the query.
|
|
507
|
+
|
|
508
|
+
Returns:
|
|
509
|
+
List of rows, where each row is a tuple containing the JSON string
|
|
510
|
+
from the `data` column.
|
|
511
|
+
"""
|
|
512
|
+
cursor.execute(
|
|
513
|
+
"SELECT data FROM plugin_data WHERE plugin_name=?", (plugin_name,)
|
|
514
|
+
)
|
|
515
|
+
return cursor.fetchall()
|
|
516
|
+
|
|
517
|
+
try:
|
|
518
|
+
result = manager.run_sync(_fetch_all)
|
|
519
|
+
except (MemoryError, sqlite3.Error):
|
|
520
|
+
logger.exception(
|
|
521
|
+
"Database error retrieving all plugin data for %s", plugin_name
|
|
522
|
+
)
|
|
523
|
+
return []
|
|
524
|
+
|
|
525
|
+
return result
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def get_longname(meshtastic_id):
|
|
529
|
+
"""
|
|
530
|
+
Retrieve the long name associated with a given Meshtastic ID.
|
|
531
|
+
|
|
532
|
+
Parameters:
|
|
533
|
+
meshtastic_id (str): The Meshtastic node ID to look up.
|
|
534
|
+
|
|
535
|
+
Returns:
|
|
536
|
+
str or None: The long name if found, or None if not found or on database error.
|
|
537
|
+
"""
|
|
538
|
+
manager = _get_db_manager()
|
|
539
|
+
|
|
540
|
+
def _fetch(cursor: sqlite3.Cursor):
|
|
541
|
+
"""
|
|
542
|
+
Retrieve the longname row for the current Meshtastic ID using the provided DB cursor.
|
|
543
|
+
|
|
544
|
+
Parameters:
|
|
545
|
+
cursor (sqlite3.Cursor): Cursor used to execute the SELECT query.
|
|
546
|
+
|
|
547
|
+
Returns:
|
|
548
|
+
sqlite3.Row or tuple or None: The first row containing the `longname` if found, `None` otherwise.
|
|
549
|
+
"""
|
|
550
|
+
cursor.execute(
|
|
551
|
+
"SELECT longname FROM longnames WHERE meshtastic_id=?",
|
|
552
|
+
(meshtastic_id,),
|
|
553
|
+
)
|
|
554
|
+
return cursor.fetchone()
|
|
555
|
+
|
|
556
|
+
try:
|
|
557
|
+
result = manager.run_sync(_fetch)
|
|
558
|
+
return result[0] if result else None
|
|
559
|
+
except sqlite3.Error:
|
|
560
|
+
logger.exception("Database error retrieving longname for %s", meshtastic_id)
|
|
561
|
+
return None
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def save_longname(meshtastic_id, longname):
|
|
565
|
+
"""
|
|
566
|
+
Persist or update the long display name for a Meshtastic node.
|
|
567
|
+
|
|
568
|
+
Writes or replaces the row for the given meshtastic_id in the longnames table and commits the change.
|
|
569
|
+
If a database error occurs it is logged and swallowed (no exception is raised).
|
|
570
|
+
|
|
571
|
+
Parameters:
|
|
572
|
+
meshtastic_id: Unique identifier for the Meshtastic node (string-like).
|
|
573
|
+
longname: The full/display name to store for the node (string).
|
|
574
|
+
"""
|
|
575
|
+
manager = _get_db_manager()
|
|
576
|
+
|
|
577
|
+
def _store(cursor: sqlite3.Cursor) -> None:
|
|
578
|
+
"""
|
|
579
|
+
Store the longname using the provided cursor.
|
|
580
|
+
|
|
581
|
+
Parameters:
|
|
582
|
+
cursor (sqlite3.Cursor): Open database cursor used to execute the insert/update. The function uses `meshtastic_id` and `longname` from the enclosing scope.
|
|
583
|
+
"""
|
|
584
|
+
cursor.execute(
|
|
585
|
+
"INSERT INTO longnames (meshtastic_id, longname) VALUES (?, ?) "
|
|
586
|
+
"ON CONFLICT(meshtastic_id) DO UPDATE SET longname=excluded.longname",
|
|
587
|
+
(meshtastic_id, longname),
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
try:
|
|
591
|
+
manager.run_sync(_store, write=True)
|
|
592
|
+
except sqlite3.Error:
|
|
593
|
+
logger.exception("Database error saving longname for %s", meshtastic_id)
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def update_longnames(nodes):
|
|
597
|
+
"""
|
|
598
|
+
Update stored long names for nodes that contain user information.
|
|
599
|
+
|
|
600
|
+
For each node that has a "user" dictionary, persisting the user's `longName` (or "N/A" if missing) keyed by the user's `id` via save_longname.
|
|
601
|
+
|
|
602
|
+
Parameters:
|
|
603
|
+
nodes (Mapping): Mapping of node identifiers to node dictionaries; each node dictionary may contain a "user" dict with an "id" key and an optional "longName" key.
|
|
604
|
+
"""
|
|
605
|
+
if nodes:
|
|
606
|
+
for node in nodes.values():
|
|
607
|
+
user = node.get("user")
|
|
608
|
+
if user:
|
|
609
|
+
meshtastic_id = user["id"]
|
|
610
|
+
longname = user.get("longName", "N/A")
|
|
611
|
+
save_longname(meshtastic_id, longname)
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def get_shortname(meshtastic_id):
|
|
615
|
+
"""
|
|
616
|
+
Retrieve the short name associated with a given Meshtastic ID.
|
|
617
|
+
|
|
618
|
+
Parameters:
|
|
619
|
+
meshtastic_id (str): The Meshtastic node ID to look up.
|
|
620
|
+
|
|
621
|
+
Returns:
|
|
622
|
+
str or None: The short name if found, or None if not found or on database error.
|
|
623
|
+
"""
|
|
624
|
+
manager = _get_db_manager()
|
|
625
|
+
|
|
626
|
+
def _fetch(cursor: sqlite3.Cursor):
|
|
627
|
+
"""
|
|
628
|
+
Retrieve the shortname row for the current Meshtastic ID using the provided DB cursor.
|
|
629
|
+
|
|
630
|
+
Parameters:
|
|
631
|
+
cursor (sqlite3.Cursor): Cursor used to execute the SELECT query.
|
|
632
|
+
|
|
633
|
+
Returns:
|
|
634
|
+
sqlite3.Row or tuple or None: The first row containing the `shortname` if found, `None` otherwise.
|
|
635
|
+
"""
|
|
636
|
+
cursor.execute(
|
|
637
|
+
"SELECT shortname FROM shortnames WHERE meshtastic_id=?",
|
|
638
|
+
(meshtastic_id,),
|
|
639
|
+
)
|
|
640
|
+
return cursor.fetchone()
|
|
641
|
+
|
|
642
|
+
try:
|
|
643
|
+
result = manager.run_sync(_fetch)
|
|
644
|
+
return result[0] if result else None
|
|
645
|
+
except sqlite3.Error:
|
|
646
|
+
logger.exception("Database error retrieving shortname for %s", meshtastic_id)
|
|
647
|
+
return None
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def save_shortname(meshtastic_id, shortname):
|
|
651
|
+
"""
|
|
652
|
+
Insert or update the short name for a Meshtastic node.
|
|
653
|
+
|
|
654
|
+
Stores the provided shortname in the shortnames table keyed by meshtastic_id and commits the change. Database errors are logged (with stacktrace) and suppressed; the function does not raise on sqlite3 errors.
|
|
655
|
+
|
|
656
|
+
Parameters:
|
|
657
|
+
meshtastic_id (str): Node identifier used as the primary key in the shortnames table.
|
|
658
|
+
shortname (str): Display name to store for the node.
|
|
659
|
+
"""
|
|
660
|
+
manager = _get_db_manager()
|
|
661
|
+
|
|
662
|
+
def _store(cursor: sqlite3.Cursor) -> None:
|
|
663
|
+
"""
|
|
664
|
+
Insert or update a shortname row for the captured `meshtastic_id` and `shortname` into the `shortnames` table.
|
|
665
|
+
|
|
666
|
+
Parameters:
|
|
667
|
+
cursor (sqlite3.Cursor): Active database cursor used to execute the write operation.
|
|
668
|
+
"""
|
|
669
|
+
cursor.execute(
|
|
670
|
+
"INSERT INTO shortnames (meshtastic_id, shortname) VALUES (?, ?) "
|
|
671
|
+
"ON CONFLICT(meshtastic_id) DO UPDATE SET shortname=excluded.shortname",
|
|
672
|
+
(meshtastic_id, shortname),
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
try:
|
|
676
|
+
manager.run_sync(_store, write=True)
|
|
677
|
+
except sqlite3.Error:
|
|
678
|
+
logger.exception("Database error saving shortname for %s", meshtastic_id)
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def update_shortnames(nodes):
|
|
682
|
+
"""
|
|
683
|
+
Update persisted short names for nodes that include a user object.
|
|
684
|
+
|
|
685
|
+
For each node in the provided mapping, if the node contains a `user` dictionary, the function
|
|
686
|
+
uses `user["id"]` as the Meshtastic ID and `user.get("shortName", "N/A")` as the short name and
|
|
687
|
+
stores that value in the database.
|
|
688
|
+
|
|
689
|
+
Parameters:
|
|
690
|
+
nodes (Mapping): Mapping of node identifiers to node objects; nodes without a `user` entry are ignored.
|
|
691
|
+
"""
|
|
692
|
+
if nodes:
|
|
693
|
+
for node in nodes.values():
|
|
694
|
+
user = node.get("user")
|
|
695
|
+
if user:
|
|
696
|
+
meshtastic_id = user["id"]
|
|
697
|
+
shortname = user.get("shortName", "N/A")
|
|
698
|
+
save_shortname(meshtastic_id, shortname)
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def _store_message_map_core(
|
|
702
|
+
cursor: sqlite3.Cursor,
|
|
703
|
+
meshtastic_id,
|
|
704
|
+
matrix_event_id,
|
|
705
|
+
matrix_room_id,
|
|
706
|
+
meshtastic_text,
|
|
707
|
+
meshtastic_meshnet=None,
|
|
708
|
+
) -> None:
|
|
709
|
+
"""
|
|
710
|
+
Insert or replace a message mapping between a Meshtastic message and a Matrix event.
|
|
711
|
+
|
|
712
|
+
Parameters:
|
|
713
|
+
cursor (sqlite3.Cursor): Active database cursor to execute the statement.
|
|
714
|
+
meshtastic_id: Identifier of the Meshtastic message or node.
|
|
715
|
+
matrix_event_id: The Matrix event ID to map to.
|
|
716
|
+
matrix_room_id: The Matrix room ID where the Matrix event resides.
|
|
717
|
+
meshtastic_text: Text content of the Meshtastic message.
|
|
718
|
+
meshtastic_meshnet (optional): Meshnet flag or value associated with the Meshtastic message; may be None.
|
|
719
|
+
"""
|
|
720
|
+
cursor.execute(
|
|
721
|
+
"INSERT INTO message_map (meshtastic_id, matrix_event_id, matrix_room_id, meshtastic_text, meshtastic_meshnet) VALUES (?, ?, ?, ?, ?) "
|
|
722
|
+
"ON CONFLICT(matrix_event_id) DO UPDATE SET "
|
|
723
|
+
"meshtastic_id=excluded.meshtastic_id, "
|
|
724
|
+
"matrix_room_id=excluded.matrix_room_id, "
|
|
725
|
+
"meshtastic_text=excluded.meshtastic_text, "
|
|
726
|
+
"meshtastic_meshnet=excluded.meshtastic_meshnet",
|
|
727
|
+
(
|
|
728
|
+
meshtastic_id,
|
|
729
|
+
matrix_event_id,
|
|
730
|
+
matrix_room_id,
|
|
731
|
+
meshtastic_text,
|
|
732
|
+
meshtastic_meshnet,
|
|
733
|
+
),
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def store_message_map(
|
|
738
|
+
meshtastic_id,
|
|
739
|
+
matrix_event_id,
|
|
740
|
+
matrix_room_id,
|
|
741
|
+
meshtastic_text,
|
|
742
|
+
meshtastic_meshnet=None,
|
|
743
|
+
):
|
|
744
|
+
"""
|
|
745
|
+
Persist a mapping between a Meshtastic message and a Matrix event.
|
|
746
|
+
|
|
747
|
+
Parameters:
|
|
748
|
+
meshtastic_id (int|str): Identifier of the Meshtastic message.
|
|
749
|
+
matrix_event_id (str): Matrix event ID to associate with the Meshtastic message.
|
|
750
|
+
matrix_room_id (str): Matrix room ID where the event was posted.
|
|
751
|
+
meshtastic_text (str): Text content of the Meshtastic message.
|
|
752
|
+
meshtastic_meshnet (str|None): Optional meshnet identifier associated with the message; stored when provided.
|
|
753
|
+
"""
|
|
754
|
+
manager = _get_db_manager()
|
|
755
|
+
|
|
756
|
+
try:
|
|
757
|
+
logger.debug(
|
|
758
|
+
"Storing message map: meshtastic_id=%s, matrix_event_id=%s, matrix_room_id=%s, meshtastic_text=%s, meshtastic_meshnet=%s",
|
|
759
|
+
meshtastic_id,
|
|
760
|
+
matrix_event_id,
|
|
761
|
+
matrix_room_id,
|
|
762
|
+
meshtastic_text,
|
|
763
|
+
meshtastic_meshnet,
|
|
764
|
+
)
|
|
765
|
+
manager.run_sync(
|
|
766
|
+
lambda cursor: _store_message_map_core(
|
|
767
|
+
cursor,
|
|
768
|
+
meshtastic_id,
|
|
769
|
+
matrix_event_id,
|
|
770
|
+
matrix_room_id,
|
|
771
|
+
meshtastic_text,
|
|
772
|
+
meshtastic_meshnet,
|
|
773
|
+
),
|
|
774
|
+
write=True,
|
|
775
|
+
)
|
|
776
|
+
except sqlite3.Error:
|
|
777
|
+
logger.exception("Database error storing message map for %s", matrix_event_id)
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def get_message_map_by_meshtastic_id(meshtastic_id):
|
|
781
|
+
"""
|
|
782
|
+
Retrieve the mapping between a Meshtastic message ID and its corresponding Matrix event.
|
|
783
|
+
|
|
784
|
+
Returns:
|
|
785
|
+
tuple: (matrix_event_id, matrix_room_id, meshtastic_text, meshtastic_meshnet) if a valid mapping exists, `None` otherwise.
|
|
786
|
+
"""
|
|
787
|
+
manager = _get_db_manager()
|
|
788
|
+
|
|
789
|
+
def _fetch(cursor: sqlite3.Cursor):
|
|
790
|
+
"""
|
|
791
|
+
Fetches the message_map row for a Meshtastic message ID using the provided database cursor.
|
|
792
|
+
|
|
793
|
+
Parameters:
|
|
794
|
+
cursor (sqlite3.Cursor): Cursor on which the SELECT query will be executed.
|
|
795
|
+
|
|
796
|
+
Returns:
|
|
797
|
+
tuple: `(matrix_event_id, matrix_room_id, meshtastic_text, meshtastic_meshnet)` if a row is found, `None` otherwise.
|
|
798
|
+
"""
|
|
799
|
+
cursor.execute(
|
|
800
|
+
"SELECT matrix_event_id, matrix_room_id, meshtastic_text, meshtastic_meshnet FROM message_map WHERE meshtastic_id=?",
|
|
801
|
+
(meshtastic_id,),
|
|
802
|
+
)
|
|
803
|
+
return cursor.fetchone()
|
|
804
|
+
|
|
805
|
+
try:
|
|
806
|
+
result = manager.run_sync(_fetch)
|
|
807
|
+
logger.debug(
|
|
808
|
+
"Retrieved message map by meshtastic_id=%s: %s", meshtastic_id, result
|
|
809
|
+
)
|
|
810
|
+
if not result:
|
|
811
|
+
return None
|
|
812
|
+
try:
|
|
813
|
+
return result[0], result[1], result[2], result[3]
|
|
814
|
+
except (IndexError, TypeError):
|
|
815
|
+
logger.exception(
|
|
816
|
+
"Malformed data in message_map for meshtastic_id %s",
|
|
817
|
+
meshtastic_id,
|
|
818
|
+
)
|
|
819
|
+
return None
|
|
820
|
+
except sqlite3.Error:
|
|
821
|
+
logger.exception(
|
|
822
|
+
"Database error retrieving message map for meshtastic_id %s",
|
|
823
|
+
meshtastic_id,
|
|
824
|
+
)
|
|
825
|
+
return None
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
def get_message_map_by_matrix_event_id(matrix_event_id):
|
|
829
|
+
"""
|
|
830
|
+
Retrieve the message mapping entry for a given Matrix event ID.
|
|
831
|
+
|
|
832
|
+
Returns:
|
|
833
|
+
tuple or None: A tuple (meshtastic_id, matrix_room_id, meshtastic_text, meshtastic_meshnet) if found, or None if not found or on error.
|
|
834
|
+
"""
|
|
835
|
+
manager = _get_db_manager()
|
|
836
|
+
|
|
837
|
+
def _fetch(cursor: sqlite3.Cursor):
|
|
838
|
+
"""
|
|
839
|
+
Retrieve the message_map row for a Matrix event id using the provided SQLite cursor.
|
|
840
|
+
|
|
841
|
+
Parameters:
|
|
842
|
+
cursor (sqlite3.Cursor): Cursor used to execute the query; the function reads the value of `matrix_event_id` from the surrounding scope.
|
|
843
|
+
|
|
844
|
+
Returns:
|
|
845
|
+
tuple|None: A tuple `(meshtastic_id, matrix_room_id, meshtastic_text, meshtastic_meshnet)` if a matching row is found, `None` otherwise.
|
|
846
|
+
"""
|
|
847
|
+
cursor.execute(
|
|
848
|
+
"SELECT meshtastic_id, matrix_room_id, meshtastic_text, meshtastic_meshnet FROM message_map WHERE matrix_event_id=?",
|
|
849
|
+
(matrix_event_id,),
|
|
850
|
+
)
|
|
851
|
+
return cursor.fetchone()
|
|
852
|
+
|
|
853
|
+
try:
|
|
854
|
+
result = manager.run_sync(_fetch)
|
|
855
|
+
logger.debug(
|
|
856
|
+
"Retrieved message map by matrix_event_id=%s: %s", matrix_event_id, result
|
|
857
|
+
)
|
|
858
|
+
if not result:
|
|
859
|
+
return None
|
|
860
|
+
try:
|
|
861
|
+
return result[0], result[1], result[2], result[3]
|
|
862
|
+
except (IndexError, TypeError):
|
|
863
|
+
logger.exception(
|
|
864
|
+
"Malformed data in message_map for matrix_event_id %s",
|
|
865
|
+
matrix_event_id,
|
|
866
|
+
)
|
|
867
|
+
return None
|
|
868
|
+
except (UnicodeDecodeError, sqlite3.Error):
|
|
869
|
+
logger.exception(
|
|
870
|
+
"Database error retrieving message map for matrix_event_id %s",
|
|
871
|
+
matrix_event_id,
|
|
872
|
+
)
|
|
873
|
+
return None
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
def wipe_message_map():
|
|
877
|
+
"""
|
|
878
|
+
Wipes all entries from the message_map table.
|
|
879
|
+
Useful when database.msg_map.wipe_on_restart or db.msg_map.wipe_on_restart is True,
|
|
880
|
+
ensuring no stale data remains.
|
|
881
|
+
"""
|
|
882
|
+
manager = _get_db_manager()
|
|
883
|
+
|
|
884
|
+
def _wipe(cursor: sqlite3.Cursor) -> None:
|
|
885
|
+
"""
|
|
886
|
+
Delete all rows from the message_map table.
|
|
887
|
+
|
|
888
|
+
Parameters:
|
|
889
|
+
cursor (sqlite3.Cursor): Cursor used to execute the deletion.
|
|
890
|
+
"""
|
|
891
|
+
cursor.execute("DELETE FROM message_map")
|
|
892
|
+
|
|
893
|
+
try:
|
|
894
|
+
manager.run_sync(_wipe, write=True)
|
|
895
|
+
logger.info("message_map table wiped successfully.")
|
|
896
|
+
except sqlite3.Error:
|
|
897
|
+
logger.exception("Failed to wipe message_map")
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
def _prune_message_map_core(cursor: sqlite3.Cursor, msgs_to_keep: int) -> int:
|
|
901
|
+
"""
|
|
902
|
+
Prune the message_map table to keep only the most recent msgs_to_keep entries.
|
|
903
|
+
|
|
904
|
+
Parameters:
|
|
905
|
+
cursor (sqlite3.Cursor): Cursor used to execute the database statements.
|
|
906
|
+
msgs_to_keep (int): Number of most-recent rows to retain in message_map.
|
|
907
|
+
|
|
908
|
+
Returns:
|
|
909
|
+
int: Number of rows deleted.
|
|
910
|
+
"""
|
|
911
|
+
cursor.execute("SELECT COUNT(*) FROM message_map")
|
|
912
|
+
row = cursor.fetchone()
|
|
913
|
+
total = row[0] if row else 0
|
|
914
|
+
|
|
915
|
+
if total > msgs_to_keep:
|
|
916
|
+
to_delete = total - msgs_to_keep
|
|
917
|
+
cursor.execute(
|
|
918
|
+
"DELETE FROM message_map WHERE rowid IN (SELECT rowid FROM message_map ORDER BY rowid ASC LIMIT ?)",
|
|
919
|
+
(to_delete,),
|
|
920
|
+
)
|
|
921
|
+
return to_delete
|
|
922
|
+
return 0
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
def prune_message_map(msgs_to_keep):
|
|
926
|
+
"""
|
|
927
|
+
Prune the message_map table so only the most recent msgs_to_keep records remain.
|
|
928
|
+
|
|
929
|
+
Parameters:
|
|
930
|
+
msgs_to_keep (int): Maximum number of most-recent message_map rows to retain; older rows will be removed.
|
|
931
|
+
"""
|
|
932
|
+
manager = _get_db_manager()
|
|
933
|
+
|
|
934
|
+
try:
|
|
935
|
+
pruned = manager.run_sync(
|
|
936
|
+
lambda cursor: _prune_message_map_core(cursor, msgs_to_keep),
|
|
937
|
+
write=True,
|
|
938
|
+
)
|
|
939
|
+
if pruned > 0:
|
|
940
|
+
logger.info(
|
|
941
|
+
"Pruned %s old message_map entries, keeping last %s.",
|
|
942
|
+
pruned,
|
|
943
|
+
msgs_to_keep,
|
|
944
|
+
)
|
|
945
|
+
except sqlite3.Error:
|
|
946
|
+
logger.exception("Database error pruning message_map")
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
async def async_store_message_map(
|
|
950
|
+
meshtastic_id,
|
|
951
|
+
matrix_event_id,
|
|
952
|
+
matrix_room_id,
|
|
953
|
+
meshtastic_text,
|
|
954
|
+
meshtastic_meshnet=None,
|
|
955
|
+
):
|
|
956
|
+
"""
|
|
957
|
+
Store a mapping from a Meshtastic message to a Matrix event in the database asynchronously.
|
|
958
|
+
|
|
959
|
+
Inserts or updates the message_map row for the provided Meshtastic ID and Matrix event identifiers along with the message text and optional meshnet flag.
|
|
960
|
+
|
|
961
|
+
Parameters:
|
|
962
|
+
meshtastic_id (str): Meshtastic message identifier.
|
|
963
|
+
matrix_event_id (str): Matrix event ID to map to.
|
|
964
|
+
matrix_room_id (str): Matrix room ID where the Matrix event was posted.
|
|
965
|
+
meshtastic_text (str): Text content of the Meshtastic message.
|
|
966
|
+
meshtastic_meshnet (bool | None): Optional flag indicating whether the message originated from Meshnet; may be None.
|
|
967
|
+
"""
|
|
968
|
+
manager = _get_db_manager()
|
|
969
|
+
|
|
970
|
+
try:
|
|
971
|
+
logger.debug(
|
|
972
|
+
"Storing message map: meshtastic_id=%s, matrix_event_id=%s, matrix_room_id=%s, meshtastic_text=%s, meshtastic_meshnet=%s",
|
|
973
|
+
meshtastic_id,
|
|
974
|
+
matrix_event_id,
|
|
975
|
+
matrix_room_id,
|
|
976
|
+
meshtastic_text,
|
|
977
|
+
meshtastic_meshnet,
|
|
978
|
+
)
|
|
979
|
+
await manager.run_async(
|
|
980
|
+
lambda cursor: _store_message_map_core(
|
|
981
|
+
cursor,
|
|
982
|
+
meshtastic_id,
|
|
983
|
+
matrix_event_id,
|
|
984
|
+
matrix_room_id,
|
|
985
|
+
meshtastic_text,
|
|
986
|
+
meshtastic_meshnet,
|
|
987
|
+
),
|
|
988
|
+
write=True,
|
|
989
|
+
)
|
|
990
|
+
except sqlite3.Error:
|
|
991
|
+
logger.exception("Database error storing message map for %s", matrix_event_id)
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
async def async_prune_message_map(msgs_to_keep):
|
|
995
|
+
"""
|
|
996
|
+
Prune the message_map table to retain only the most recent `msgs_to_keep` entries asynchronously.
|
|
997
|
+
|
|
998
|
+
Executes the prune operation in a background database task and logs if rows were removed.
|
|
999
|
+
|
|
1000
|
+
Parameters:
|
|
1001
|
+
msgs_to_keep (int): Number of most recent message_map entries to keep; older entries will be deleted.
|
|
1002
|
+
"""
|
|
1003
|
+
manager = _get_db_manager()
|
|
1004
|
+
|
|
1005
|
+
try:
|
|
1006
|
+
pruned = await manager.run_async(
|
|
1007
|
+
lambda cursor: _prune_message_map_core(cursor, msgs_to_keep),
|
|
1008
|
+
write=True,
|
|
1009
|
+
)
|
|
1010
|
+
if pruned > 0:
|
|
1011
|
+
logger.info(
|
|
1012
|
+
"Pruned %s old message_map entries, keeping last %s.",
|
|
1013
|
+
pruned,
|
|
1014
|
+
msgs_to_keep,
|
|
1015
|
+
)
|
|
1016
|
+
except sqlite3.Error:
|
|
1017
|
+
logger.exception("Database error pruning message_map")
|