mmrelay 1.1.2__py3-none-any.whl → 1.1.4__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 -13
- mmrelay/cli.py +124 -64
- mmrelay/config.py +63 -36
- mmrelay/config_checker.py +41 -12
- mmrelay/constants/__init__.py +54 -0
- mmrelay/constants/app.py +17 -0
- mmrelay/constants/config.py +73 -0
- mmrelay/constants/database.py +22 -0
- mmrelay/constants/formats.py +20 -0
- mmrelay/constants/messages.py +36 -0
- mmrelay/constants/network.py +35 -0
- mmrelay/constants/queue.py +17 -0
- mmrelay/db_utils.py +281 -132
- mmrelay/log_utils.py +38 -14
- mmrelay/main.py +23 -4
- mmrelay/matrix_utils.py +413 -162
- mmrelay/meshtastic_utils.py +223 -106
- mmrelay/message_queue.py +475 -0
- mmrelay/plugin_loader.py +56 -53
- mmrelay/plugins/base_plugin.py +139 -39
- mmrelay/plugins/drop_plugin.py +13 -5
- mmrelay/plugins/mesh_relay_plugin.py +7 -10
- mmrelay/plugins/weather_plugin.py +10 -1
- mmrelay/setup_utils.py +67 -30
- mmrelay/tools/sample_config.yaml +13 -3
- {mmrelay-1.1.2.dist-info → mmrelay-1.1.4.dist-info}/METADATA +12 -14
- mmrelay-1.1.4.dist-info/RECORD +43 -0
- mmrelay-1.1.4.dist-info/licenses/LICENSE +675 -0
- mmrelay-1.1.2.dist-info/RECORD +0 -34
- mmrelay-1.1.2.dist-info/licenses/LICENSE +0 -21
- {mmrelay-1.1.2.dist-info → mmrelay-1.1.4.dist-info}/WHEEL +0 -0
- {mmrelay-1.1.2.dist-info → mmrelay-1.1.4.dist-info}/entry_points.txt +0 -0
- {mmrelay-1.1.2.dist-info → mmrelay-1.1.4.dist-info}/top_level.txt +0 -0
mmrelay/matrix_utils.py
CHANGED
|
@@ -21,6 +21,18 @@ from nio import (
|
|
|
21
21
|
from nio.events.room_events import RoomMemberEvent
|
|
22
22
|
from PIL import Image
|
|
23
23
|
|
|
24
|
+
from mmrelay.constants.config import (
|
|
25
|
+
CONFIG_KEY_ACCESS_TOKEN,
|
|
26
|
+
CONFIG_KEY_HOMESERVER,
|
|
27
|
+
CONFIG_SECTION_MATRIX,
|
|
28
|
+
)
|
|
29
|
+
from mmrelay.constants.database import DEFAULT_MSGS_TO_KEEP
|
|
30
|
+
from mmrelay.constants.formats import (
|
|
31
|
+
DEFAULT_MATRIX_PREFIX,
|
|
32
|
+
DEFAULT_MESHTASTIC_PREFIX,
|
|
33
|
+
DETECTION_SENSOR_APP,
|
|
34
|
+
)
|
|
35
|
+
from mmrelay.constants.network import MILLISECONDS_PER_SECOND
|
|
24
36
|
from mmrelay.db_utils import (
|
|
25
37
|
get_message_map_by_matrix_event_id,
|
|
26
38
|
prune_message_map,
|
|
@@ -29,14 +41,69 @@ from mmrelay.db_utils import (
|
|
|
29
41
|
from mmrelay.log_utils import get_logger
|
|
30
42
|
|
|
31
43
|
# Do not import plugin_loader here to avoid circular imports
|
|
32
|
-
from mmrelay.meshtastic_utils import connect_meshtastic
|
|
44
|
+
from mmrelay.meshtastic_utils import connect_meshtastic, sendTextReply
|
|
45
|
+
from mmrelay.message_queue import get_message_queue, queue_message
|
|
46
|
+
|
|
47
|
+
logger = get_logger(name="matrix_utils")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _get_msgs_to_keep_config():
|
|
51
|
+
"""
|
|
52
|
+
Returns the configured number of messages to retain for message mapping, supporting both current and legacy configuration sections.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
int: Number of messages to keep for message mapping; defaults to the predefined constant if not set.
|
|
56
|
+
"""
|
|
57
|
+
global config
|
|
58
|
+
if not config:
|
|
59
|
+
return DEFAULT_MSGS_TO_KEEP
|
|
60
|
+
|
|
61
|
+
msg_map_config = config.get("database", {}).get("msg_map", {})
|
|
62
|
+
|
|
63
|
+
# If not found in database config, check legacy db config
|
|
64
|
+
if not msg_map_config:
|
|
65
|
+
legacy_msg_map_config = config.get("db", {}).get("msg_map", {})
|
|
66
|
+
|
|
67
|
+
if legacy_msg_map_config:
|
|
68
|
+
msg_map_config = legacy_msg_map_config
|
|
69
|
+
logger.warning(
|
|
70
|
+
"Using 'db.msg_map' configuration (legacy). 'database.msg_map' is now the preferred format and 'db.msg_map' will be deprecated in a future version."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return msg_map_config.get("msgs_to_keep", DEFAULT_MSGS_TO_KEEP)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _create_mapping_info(
|
|
77
|
+
matrix_event_id, room_id, text, meshnet=None, msgs_to_keep=None
|
|
78
|
+
):
|
|
79
|
+
"""
|
|
80
|
+
Create a metadata dictionary linking a Matrix event to a Meshtastic message for message mapping.
|
|
81
|
+
|
|
82
|
+
Removes quoted lines from the message text and includes identifiers and message retention settings. Returns `None` if any required parameter is missing.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
dict: Mapping information for the message queue, or `None` if required fields are missing.
|
|
86
|
+
"""
|
|
87
|
+
if not matrix_event_id or not room_id or not text:
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
if msgs_to_keep is None:
|
|
91
|
+
msgs_to_keep = _get_msgs_to_keep_config()
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
"matrix_event_id": matrix_event_id,
|
|
95
|
+
"room_id": room_id,
|
|
96
|
+
"text": strip_quoted_lines(text),
|
|
97
|
+
"meshnet": meshnet,
|
|
98
|
+
"msgs_to_keep": msgs_to_keep,
|
|
99
|
+
}
|
|
33
100
|
|
|
34
101
|
|
|
35
102
|
def get_interaction_settings(config):
|
|
36
103
|
"""
|
|
37
|
-
|
|
104
|
+
Determine if message reactions and replies are enabled in the configuration.
|
|
38
105
|
|
|
39
|
-
|
|
106
|
+
Checks for both the new `message_interactions` structure and the legacy `relay_reactions` flag for backward compatibility. Returns a dictionary with boolean values for `reactions` and `replies`, defaulting to both disabled if not specified.
|
|
40
107
|
"""
|
|
41
108
|
if config is None:
|
|
42
109
|
return {"reactions": False, "replies": False}
|
|
@@ -70,15 +137,175 @@ def get_interaction_settings(config):
|
|
|
70
137
|
|
|
71
138
|
def message_storage_enabled(interactions):
|
|
72
139
|
"""
|
|
73
|
-
|
|
140
|
+
Determine if message storage is needed based on enabled message interactions.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
True if either reactions or replies are enabled in the interactions dictionary; otherwise, False.
|
|
144
|
+
"""
|
|
145
|
+
return interactions["reactions"] or interactions["replies"]
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _add_truncated_vars(format_vars, prefix, text):
|
|
149
|
+
"""Helper function to add variable length truncation variables to format_vars dict."""
|
|
150
|
+
# Always add truncated variables, even for empty text (to prevent KeyError)
|
|
151
|
+
text = text or "" # Convert None to empty string
|
|
152
|
+
logger.debug(f"Adding truncated vars for prefix='{prefix}', text='{text}'")
|
|
153
|
+
for i in range(1, 21): # Support up to 20 chars, always add all variants
|
|
154
|
+
truncated_value = text[:i]
|
|
155
|
+
format_vars[f"{prefix}{i}"] = truncated_value
|
|
156
|
+
if i <= 6: # Only log first few to avoid spam
|
|
157
|
+
logger.debug(f" {prefix}{i} = '{truncated_value}'")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def validate_prefix_format(format_string, available_vars):
|
|
161
|
+
"""Validate prefix format string against available variables.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
format_string (str): The format string to validate.
|
|
165
|
+
available_vars (dict): Dictionary of available variables with test values.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
tuple: (is_valid: bool, error_message: str or None)
|
|
169
|
+
"""
|
|
170
|
+
try:
|
|
171
|
+
# Test format with dummy data
|
|
172
|
+
format_string.format(**available_vars)
|
|
173
|
+
return True, None
|
|
174
|
+
except (KeyError, ValueError) as e:
|
|
175
|
+
return False, str(e)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def get_meshtastic_prefix(config, display_name, user_id=None):
|
|
179
|
+
"""
|
|
180
|
+
Generate a Meshtastic message prefix using the configured format, supporting variable-length truncation and user-specific variables.
|
|
181
|
+
|
|
182
|
+
If prefix formatting is enabled in the configuration, returns a formatted prefix string for Meshtastic messages using the user's display name and optional Matrix user ID. Supports custom format strings with placeholders for the display name, truncated display name segments (e.g., `{display5}`), and user ID components. Falls back to a default format if the custom format is invalid or missing. Returns an empty string if prefixing is disabled.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
config (dict): The application configuration dictionary.
|
|
186
|
+
display_name (str): The user's display name (room-specific or global).
|
|
187
|
+
user_id (str, optional): The user's Matrix ID (@user:server.com).
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
str: The formatted prefix string if enabled, empty string otherwise.
|
|
191
|
+
|
|
192
|
+
Examples:
|
|
193
|
+
Basic usage:
|
|
194
|
+
get_meshtastic_prefix(config, "Alice Smith")
|
|
195
|
+
# Returns: "Alice[M]: " (with default format)
|
|
196
|
+
|
|
197
|
+
Custom format:
|
|
198
|
+
config = {"meshtastic": {"prefix_format": "{display8}> "}}
|
|
199
|
+
get_meshtastic_prefix(config, "Alice Smith")
|
|
200
|
+
# Returns: "Alice Sm> "
|
|
201
|
+
"""
|
|
202
|
+
meshtastic_config = config.get("meshtastic", {})
|
|
203
|
+
|
|
204
|
+
# Check if prefixes are enabled
|
|
205
|
+
if not meshtastic_config.get("prefix_enabled", True):
|
|
206
|
+
return ""
|
|
207
|
+
|
|
208
|
+
# Get custom format or use default
|
|
209
|
+
prefix_format = meshtastic_config.get("prefix_format", DEFAULT_MESHTASTIC_PREFIX)
|
|
210
|
+
|
|
211
|
+
# Parse username and server from user_id if available
|
|
212
|
+
username = ""
|
|
213
|
+
server = ""
|
|
214
|
+
if user_id:
|
|
215
|
+
# Extract username and server from @username:server.com format
|
|
216
|
+
if user_id.startswith("@") and ":" in user_id:
|
|
217
|
+
parts = user_id[1:].split(":", 1) # Remove @ and split on first :
|
|
218
|
+
username = parts[0]
|
|
219
|
+
server = parts[1] if len(parts) > 1 else ""
|
|
220
|
+
|
|
221
|
+
# Available variables for formatting with variable length support
|
|
222
|
+
format_vars = {
|
|
223
|
+
"display": display_name or "",
|
|
224
|
+
"user": user_id or "",
|
|
225
|
+
"username": username,
|
|
226
|
+
"server": server,
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
# Add variable length display name truncation (display1, display2, display3, etc.)
|
|
230
|
+
_add_truncated_vars(format_vars, "display", display_name)
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
return prefix_format.format(**format_vars)
|
|
234
|
+
except (KeyError, ValueError) as e:
|
|
235
|
+
# Fallback to default format if custom format is invalid
|
|
236
|
+
logger.warning(
|
|
237
|
+
f"Invalid prefix_format '{prefix_format}': {e}. Using default format."
|
|
238
|
+
)
|
|
239
|
+
# The default format only uses 'display5', which is safe to format
|
|
240
|
+
return DEFAULT_MESHTASTIC_PREFIX.format(
|
|
241
|
+
display5=display_name[:5] if display_name else ""
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def get_matrix_prefix(config, longname, shortname, meshnet_name):
|
|
246
|
+
"""
|
|
247
|
+
Generates a formatted prefix string for Meshtastic messages relayed to Matrix, based on configuration settings and sender/mesh network names.
|
|
248
|
+
|
|
249
|
+
The prefix format supports variable-length truncation for the sender and mesh network names using template variables (e.g., `{long4}` for the first 4 characters of the sender name). Returns an empty string if prefixing is disabled in the configuration.
|
|
74
250
|
|
|
75
251
|
Parameters:
|
|
76
|
-
|
|
252
|
+
longname (str): Full Meshtastic sender name.
|
|
253
|
+
shortname (str): Short Meshtastic sender name.
|
|
254
|
+
meshnet_name (str): Name of the mesh network.
|
|
77
255
|
|
|
78
256
|
Returns:
|
|
79
|
-
|
|
257
|
+
str: The formatted prefix string, or an empty string if prefixing is disabled.
|
|
80
258
|
"""
|
|
81
|
-
|
|
259
|
+
matrix_config = config.get(CONFIG_SECTION_MATRIX, {})
|
|
260
|
+
|
|
261
|
+
# Enhanced debug logging for configuration troubleshooting
|
|
262
|
+
logger.debug(
|
|
263
|
+
f"get_matrix_prefix called with longname='{longname}', shortname='{shortname}', meshnet_name='{meshnet_name}'"
|
|
264
|
+
)
|
|
265
|
+
logger.debug(f"Matrix config section: {matrix_config}")
|
|
266
|
+
|
|
267
|
+
# Check if prefixes are enabled for Matrix direction
|
|
268
|
+
if not matrix_config.get("prefix_enabled", True):
|
|
269
|
+
logger.debug("Matrix prefixes are disabled, returning empty string")
|
|
270
|
+
return ""
|
|
271
|
+
|
|
272
|
+
# Get custom format or use default
|
|
273
|
+
matrix_prefix_format = matrix_config.get("prefix_format", DEFAULT_MATRIX_PREFIX)
|
|
274
|
+
logger.debug(
|
|
275
|
+
f"Using matrix prefix format: '{matrix_prefix_format}' (default: '{DEFAULT_MATRIX_PREFIX}')"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# Available variables for formatting with variable length support
|
|
279
|
+
format_vars = {
|
|
280
|
+
"long": longname,
|
|
281
|
+
"short": shortname,
|
|
282
|
+
"mesh": meshnet_name,
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
# Add variable length truncation for longname and mesh name
|
|
286
|
+
_add_truncated_vars(format_vars, "long", longname)
|
|
287
|
+
_add_truncated_vars(format_vars, "mesh", meshnet_name)
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
result = matrix_prefix_format.format(**format_vars)
|
|
291
|
+
logger.debug(
|
|
292
|
+
f"Matrix prefix generated: '{result}' using format '{matrix_prefix_format}' with vars {format_vars}"
|
|
293
|
+
)
|
|
294
|
+
# Additional debug to help identify the issue
|
|
295
|
+
if result == f"[{longname}/{meshnet_name}]: ":
|
|
296
|
+
logger.debug(
|
|
297
|
+
"Generated prefix matches default format - check if custom configuration is being loaded correctly"
|
|
298
|
+
)
|
|
299
|
+
return result
|
|
300
|
+
except (KeyError, ValueError) as e:
|
|
301
|
+
# Fallback to default format if custom format is invalid
|
|
302
|
+
logger.warning(
|
|
303
|
+
f"Invalid matrix prefix_format '{matrix_prefix_format}': {e}. Using default format."
|
|
304
|
+
)
|
|
305
|
+
# The default format only uses 'long' and 'mesh', which are safe
|
|
306
|
+
return DEFAULT_MATRIX_PREFIX.format(
|
|
307
|
+
long=longname or "", mesh=meshnet_name or ""
|
|
308
|
+
)
|
|
82
309
|
|
|
83
310
|
|
|
84
311
|
# Global config variable that will be set from config.py
|
|
@@ -91,7 +318,7 @@ matrix_access_token = None
|
|
|
91
318
|
bot_user_id = None
|
|
92
319
|
bot_user_name = None # Detected upon logon
|
|
93
320
|
bot_start_time = int(
|
|
94
|
-
time.time() *
|
|
321
|
+
time.time() * MILLISECONDS_PER_SECOND
|
|
95
322
|
) # Timestamp when the bot starts, used to filter out old messages
|
|
96
323
|
|
|
97
324
|
logger = get_logger(name="Matrix")
|
|
@@ -136,11 +363,9 @@ def bot_command(command, event):
|
|
|
136
363
|
|
|
137
364
|
async def connect_matrix(passed_config=None):
|
|
138
365
|
"""
|
|
139
|
-
|
|
140
|
-
Sets global matrix_client and detects the bot's display name.
|
|
366
|
+
Asynchronously connects to the Matrix homeserver, initializes the Matrix client, and retrieves the bot's device ID and display name.
|
|
141
367
|
|
|
142
|
-
|
|
143
|
-
passed_config: The configuration dictionary to use (will update global config)
|
|
368
|
+
If a configuration dictionary is provided, it updates the global configuration before connecting. Returns the initialized Matrix AsyncClient instance, or `None` if configuration is missing. Raises `ConnectionError` if SSL context creation fails.
|
|
144
369
|
"""
|
|
145
370
|
global matrix_client, bot_user_name, matrix_homeserver, matrix_rooms, matrix_access_token, bot_user_id, config
|
|
146
371
|
|
|
@@ -154,9 +379,9 @@ async def connect_matrix(passed_config=None):
|
|
|
154
379
|
return None
|
|
155
380
|
|
|
156
381
|
# Extract Matrix configuration
|
|
157
|
-
matrix_homeserver = config[
|
|
382
|
+
matrix_homeserver = config[CONFIG_SECTION_MATRIX][CONFIG_KEY_HOMESERVER]
|
|
158
383
|
matrix_rooms = config["matrix_rooms"]
|
|
159
|
-
matrix_access_token = config[
|
|
384
|
+
matrix_access_token = config[CONFIG_SECTION_MATRIX][CONFIG_KEY_ACCESS_TOKEN]
|
|
160
385
|
bot_user_id = config["matrix"]["bot_user_id"]
|
|
161
386
|
|
|
162
387
|
# Check if client already exists
|
|
@@ -164,7 +389,11 @@ async def connect_matrix(passed_config=None):
|
|
|
164
389
|
return matrix_client
|
|
165
390
|
|
|
166
391
|
# Create SSL context using certifi's certificates
|
|
167
|
-
|
|
392
|
+
try:
|
|
393
|
+
ssl_context = ssl.create_default_context(cafile=certifi.where())
|
|
394
|
+
except Exception as e:
|
|
395
|
+
logger.error(f"Failed to create SSL context: {e}")
|
|
396
|
+
raise ConnectionError(f"SSL context creation failed: {e}") from e
|
|
168
397
|
|
|
169
398
|
# Initialize the Matrix client with custom SSL context
|
|
170
399
|
client_config = AsyncClientConfig(encryption_enabled=False)
|
|
@@ -251,9 +480,9 @@ async def matrix_relay(
|
|
|
251
480
|
reply_to_event_id=None,
|
|
252
481
|
):
|
|
253
482
|
"""
|
|
254
|
-
|
|
483
|
+
Relay a message from the Meshtastic network to a Matrix room, supporting replies, emotes, emoji reactions, and message mapping for future interactions.
|
|
255
484
|
|
|
256
|
-
If
|
|
485
|
+
If a reply target is specified, formats the message as a Matrix reply with quoted original content. When message interactions (reactions or replies) are enabled, stores a mapping between the Meshtastic message ID and the resulting Matrix event ID to support cross-network interactions. Prunes old message mappings according to configuration to limit storage.
|
|
257
486
|
|
|
258
487
|
Parameters:
|
|
259
488
|
room_id (str): The Matrix room ID to send the message to.
|
|
@@ -268,9 +497,6 @@ async def matrix_relay(
|
|
|
268
497
|
emote (bool, optional): Whether to send the message as an emote.
|
|
269
498
|
emoji (bool, optional): Whether the message is an emoji reaction.
|
|
270
499
|
reply_to_event_id (str, optional): The Matrix event ID being replied to, if sending a reply.
|
|
271
|
-
|
|
272
|
-
Returns:
|
|
273
|
-
None
|
|
274
500
|
"""
|
|
275
501
|
global config
|
|
276
502
|
|
|
@@ -304,8 +530,8 @@ async def matrix_relay(
|
|
|
304
530
|
"Using 'db.msg_map' configuration (legacy). 'database.msg_map' is now the preferred format and 'db.msg_map' will be deprecated in a future version."
|
|
305
531
|
)
|
|
306
532
|
msgs_to_keep = msg_map_config.get(
|
|
307
|
-
"msgs_to_keep",
|
|
308
|
-
) # Default
|
|
533
|
+
"msgs_to_keep", DEFAULT_MSGS_TO_KEEP
|
|
534
|
+
) # Default from constants
|
|
309
535
|
|
|
310
536
|
try:
|
|
311
537
|
# Always use our own local meshnet_name for outgoing events
|
|
@@ -459,7 +685,7 @@ async def get_user_display_name(room, event):
|
|
|
459
685
|
return display_name_response.displayname or event.sender
|
|
460
686
|
|
|
461
687
|
|
|
462
|
-
def format_reply_message(full_display_name, text):
|
|
688
|
+
def format_reply_message(config, full_display_name, text):
|
|
463
689
|
"""
|
|
464
690
|
Format a reply message by prefixing a truncated display name and removing quoted lines.
|
|
465
691
|
|
|
@@ -472,8 +698,7 @@ def format_reply_message(full_display_name, text):
|
|
|
472
698
|
Returns:
|
|
473
699
|
str: The formatted and truncated reply message.
|
|
474
700
|
"""
|
|
475
|
-
|
|
476
|
-
prefix = f"{short_display_name}[M]: "
|
|
701
|
+
prefix = get_meshtastic_prefix(config, full_display_name)
|
|
477
702
|
|
|
478
703
|
# Strip quoted content from the reply text
|
|
479
704
|
clean_text = strip_quoted_lines(text)
|
|
@@ -493,82 +718,86 @@ async def send_reply_to_meshtastic(
|
|
|
493
718
|
reply_id=None,
|
|
494
719
|
):
|
|
495
720
|
"""
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
If message storage is enabled and the message is successfully sent, stores a mapping between the Meshtastic packet ID and the Matrix event ID, after removing quoted lines from the reply text. Prunes old message mappings based on configuration to limit storage size. Logs errors if sending fails.
|
|
721
|
+
Queues a reply message from Matrix to be sent to Meshtastic, optionally as a structured reply, and includes message mapping metadata if storage is enabled.
|
|
499
722
|
|
|
500
|
-
|
|
501
|
-
reply_id: If provided, sends as a structured Meshtastic reply to this message ID
|
|
723
|
+
If a `reply_id` is provided, the message is sent as a structured reply to the referenced Meshtastic message; otherwise, it is sent as a regular message. When message storage is enabled, mapping information is attached for future interaction tracking. The function logs the outcome of the queuing operation.
|
|
502
724
|
"""
|
|
503
725
|
meshtastic_interface = connect_meshtastic()
|
|
504
726
|
from mmrelay.meshtastic_utils import logger as meshtastic_logger
|
|
505
|
-
from mmrelay.meshtastic_utils import sendTextReply
|
|
506
727
|
|
|
507
728
|
meshtastic_channel = room_config["meshtastic_channel"]
|
|
508
729
|
|
|
509
730
|
if config["meshtastic"]["broadcast_enabled"]:
|
|
510
731
|
try:
|
|
732
|
+
# Create mapping info once if storage is enabled
|
|
733
|
+
mapping_info = None
|
|
734
|
+
if storage_enabled:
|
|
735
|
+
# Get message map configuration
|
|
736
|
+
msgs_to_keep = _get_msgs_to_keep_config()
|
|
737
|
+
|
|
738
|
+
mapping_info = _create_mapping_info(
|
|
739
|
+
event.event_id, room.room_id, text, local_meshnet_name, msgs_to_keep
|
|
740
|
+
)
|
|
741
|
+
|
|
511
742
|
if reply_id is not None:
|
|
512
743
|
# Send as a structured reply using our custom function
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
744
|
+
# Queue the reply message
|
|
745
|
+
success = queue_message(
|
|
746
|
+
sendTextReply,
|
|
747
|
+
meshtastic_interface,
|
|
748
|
+
text=reply_message,
|
|
749
|
+
reply_id=reply_id,
|
|
750
|
+
channelIndex=meshtastic_channel,
|
|
751
|
+
description=f"Reply from {full_display_name} to message {reply_id}",
|
|
752
|
+
mapping_info=mapping_info,
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
if success:
|
|
756
|
+
# Get queue size to determine logging approach
|
|
757
|
+
queue_size = get_message_queue().get_queue_size()
|
|
758
|
+
|
|
759
|
+
if queue_size > 1:
|
|
760
|
+
meshtastic_logger.info(
|
|
761
|
+
f"Relaying Matrix reply from {full_display_name} to radio broadcast as structured reply (queued: {queue_size} messages)"
|
|
762
|
+
)
|
|
763
|
+
else:
|
|
764
|
+
meshtastic_logger.info(
|
|
765
|
+
f"Relaying Matrix reply from {full_display_name} to radio broadcast as structured reply"
|
|
766
|
+
)
|
|
767
|
+
else:
|
|
527
768
|
meshtastic_logger.error(
|
|
528
|
-
|
|
769
|
+
"Failed to relay structured reply to Meshtastic"
|
|
529
770
|
)
|
|
530
771
|
return
|
|
531
772
|
else:
|
|
532
773
|
# Send as regular message (fallback for when no reply_id is available)
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
774
|
+
success = queue_message(
|
|
775
|
+
meshtastic_interface.sendText,
|
|
776
|
+
text=reply_message,
|
|
777
|
+
channelIndex=meshtastic_channel,
|
|
778
|
+
description=f"Reply from {full_display_name} (fallback to regular message)",
|
|
779
|
+
mapping_info=mapping_info,
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
if success:
|
|
783
|
+
# Get queue size to determine logging approach
|
|
784
|
+
queue_size = get_message_queue().get_queue_size()
|
|
785
|
+
|
|
786
|
+
if queue_size > 1:
|
|
787
|
+
meshtastic_logger.info(
|
|
788
|
+
f"Relaying Matrix reply from {full_display_name} to radio broadcast (queued: {queue_size} messages)"
|
|
789
|
+
)
|
|
790
|
+
else:
|
|
791
|
+
meshtastic_logger.info(
|
|
792
|
+
f"Relaying Matrix reply from {full_display_name} to radio broadcast"
|
|
793
|
+
)
|
|
794
|
+
else:
|
|
545
795
|
meshtastic_logger.error(
|
|
546
|
-
|
|
796
|
+
"Failed to relay reply message to Meshtastic"
|
|
547
797
|
)
|
|
548
798
|
return
|
|
549
799
|
|
|
550
|
-
#
|
|
551
|
-
if storage_enabled and sent_packet and hasattr(sent_packet, "id"):
|
|
552
|
-
# Strip quoted lines from text before storing to prevent issues with reactions to replies
|
|
553
|
-
cleaned_text = strip_quoted_lines(text)
|
|
554
|
-
store_message_map(
|
|
555
|
-
sent_packet.id,
|
|
556
|
-
event.event_id,
|
|
557
|
-
room.room_id,
|
|
558
|
-
cleaned_text,
|
|
559
|
-
meshtastic_meshnet=local_meshnet_name,
|
|
560
|
-
)
|
|
561
|
-
# Prune old messages if configured
|
|
562
|
-
database_config = config.get("database", {})
|
|
563
|
-
msg_map_config = database_config.get("msg_map", {})
|
|
564
|
-
if not msg_map_config:
|
|
565
|
-
db_config = config.get("db", {})
|
|
566
|
-
legacy_msg_map_config = db_config.get("msg_map", {})
|
|
567
|
-
if legacy_msg_map_config:
|
|
568
|
-
msg_map_config = legacy_msg_map_config
|
|
569
|
-
msgs_to_keep = msg_map_config.get("msgs_to_keep", 500)
|
|
570
|
-
if msgs_to_keep > 0:
|
|
571
|
-
prune_message_map(msgs_to_keep)
|
|
800
|
+
# Message mapping is now handled automatically by the queue system
|
|
572
801
|
|
|
573
802
|
except Exception as e:
|
|
574
803
|
meshtastic_logger.error(f"Error sending Matrix reply to Meshtastic: {e}")
|
|
@@ -582,6 +811,7 @@ async def handle_matrix_reply(
|
|
|
582
811
|
room_config,
|
|
583
812
|
storage_enabled,
|
|
584
813
|
local_meshnet_name,
|
|
814
|
+
config,
|
|
585
815
|
):
|
|
586
816
|
"""
|
|
587
817
|
Relays a Matrix reply to the corresponding Meshtastic message if a mapping exists.
|
|
@@ -607,7 +837,7 @@ async def handle_matrix_reply(
|
|
|
607
837
|
full_display_name = await get_user_display_name(room, event)
|
|
608
838
|
|
|
609
839
|
# Format the reply message
|
|
610
|
-
reply_message = format_reply_message(full_display_name, text)
|
|
840
|
+
reply_message = format_reply_message(config, full_display_name, text)
|
|
611
841
|
|
|
612
842
|
logger.info(
|
|
613
843
|
f"Relaying Matrix reply from {full_display_name} to Meshtastic as reply to message {original_meshtastic_id}"
|
|
@@ -635,12 +865,14 @@ async def on_room_message(
|
|
|
635
865
|
event: Union[RoomMessageText, RoomMessageNotice, ReactionEvent, RoomMessageEmote],
|
|
636
866
|
) -> None:
|
|
637
867
|
"""
|
|
638
|
-
|
|
868
|
+
Asynchronously handles incoming Matrix room messages, reactions, and replies, relaying them to Meshtastic as appropriate.
|
|
639
869
|
|
|
640
|
-
Processes
|
|
870
|
+
Processes Matrix events—including text messages, reactions, and replies—in configured rooms. Relays supported messages to the Meshtastic mesh network if broadcasting is enabled, applying message mapping for cross-referencing when reactions or replies are enabled. Ignores messages from the bot itself, messages sent before the bot started, and reactions to reactions. Integrates with plugins for command and message handling; only messages not handled by plugins or identified as commands are forwarded to Meshtastic, with appropriate formatting and truncation. Handles special cases for relaying messages and reactions from remote mesh networks and detection sensor data.
|
|
641
871
|
"""
|
|
642
872
|
# Importing here to avoid circular imports and to keep logic consistent
|
|
643
873
|
# Note: We do not call store_message_map directly here for inbound matrix->mesh messages.
|
|
874
|
+
from mmrelay.message_queue import get_message_queue
|
|
875
|
+
|
|
644
876
|
# That logic occurs inside matrix_relay if needed.
|
|
645
877
|
full_display_name = "Unknown user"
|
|
646
878
|
message_timestamp = event.server_timestamp
|
|
@@ -785,15 +1017,19 @@ async def on_room_message(
|
|
|
785
1017
|
logger.debug(
|
|
786
1018
|
f"Sending reaction to Meshtastic with meshnet={local_meshnet_name}: {reaction_message}"
|
|
787
1019
|
)
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
1020
|
+
success = queue_message(
|
|
1021
|
+
meshtastic_interface.sendText,
|
|
1022
|
+
text=reaction_message,
|
|
1023
|
+
channelIndex=meshtastic_channel,
|
|
1024
|
+
description=f"Remote reaction from {meshnet_name}",
|
|
1025
|
+
)
|
|
1026
|
+
|
|
1027
|
+
if success:
|
|
792
1028
|
logger.debug(
|
|
793
|
-
f"
|
|
1029
|
+
f"Queued remote reaction to Meshtastic: {reaction_message}"
|
|
794
1030
|
)
|
|
795
|
-
|
|
796
|
-
logger.error(
|
|
1031
|
+
else:
|
|
1032
|
+
logger.error("Failed to relay remote reaction to Meshtastic")
|
|
797
1033
|
return
|
|
798
1034
|
# We've relayed the remote reaction to our local mesh, so we're done.
|
|
799
1035
|
return
|
|
@@ -824,8 +1060,7 @@ async def on_room_message(
|
|
|
824
1060
|
full_display_name = display_name_response.displayname or event.sender
|
|
825
1061
|
|
|
826
1062
|
# If not from a remote meshnet, proceed as normal to relay back to the originating meshnet
|
|
827
|
-
|
|
828
|
-
prefix = f"{short_display_name}[M]: "
|
|
1063
|
+
prefix = get_meshtastic_prefix(config, full_display_name)
|
|
829
1064
|
|
|
830
1065
|
# Remove quoted lines so we don't bring in the original '>' lines from replies
|
|
831
1066
|
meshtastic_text_db = strip_quoted_lines(meshtastic_text_db)
|
|
@@ -855,15 +1090,19 @@ async def on_room_message(
|
|
|
855
1090
|
logger.debug(
|
|
856
1091
|
f"Sending reaction to Meshtastic with meshnet={local_meshnet_name}: {reaction_message}"
|
|
857
1092
|
)
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
1093
|
+
success = queue_message(
|
|
1094
|
+
meshtastic_interface.sendText,
|
|
1095
|
+
text=reaction_message,
|
|
1096
|
+
channelIndex=meshtastic_channel,
|
|
1097
|
+
description=f"Local reaction from {full_display_name}",
|
|
1098
|
+
)
|
|
1099
|
+
|
|
1100
|
+
if success:
|
|
862
1101
|
logger.debug(
|
|
863
|
-
f"
|
|
1102
|
+
f"Queued local reaction to Meshtastic: {reaction_message}"
|
|
864
1103
|
)
|
|
865
|
-
|
|
866
|
-
logger.error(
|
|
1104
|
+
else:
|
|
1105
|
+
logger.error("Failed to relay local reaction to Meshtastic")
|
|
867
1106
|
return
|
|
868
1107
|
return
|
|
869
1108
|
|
|
@@ -877,6 +1116,7 @@ async def on_room_message(
|
|
|
877
1116
|
room_config,
|
|
878
1117
|
storage_enabled,
|
|
879
1118
|
local_meshnet_name,
|
|
1119
|
+
config,
|
|
880
1120
|
)
|
|
881
1121
|
if reply_handled:
|
|
882
1122
|
return
|
|
@@ -893,10 +1133,20 @@ async def on_room_message(
|
|
|
893
1133
|
# If shortname is not available, derive it from the longname
|
|
894
1134
|
if shortname is None:
|
|
895
1135
|
shortname = longname[:3] if longname else "???"
|
|
896
|
-
# Remove the original prefix
|
|
897
|
-
|
|
1136
|
+
# Remove the original prefix to avoid double-tagging
|
|
1137
|
+
# Get the prefix that would have been used for this message
|
|
1138
|
+
original_prefix = get_matrix_prefix(
|
|
1139
|
+
config, longname, shortname, meshnet_name
|
|
1140
|
+
)
|
|
1141
|
+
if original_prefix and text.startswith(original_prefix):
|
|
1142
|
+
text = text[len(original_prefix) :]
|
|
1143
|
+
logger.debug(
|
|
1144
|
+
f"Removed original prefix '{original_prefix}' from remote meshnet message"
|
|
1145
|
+
)
|
|
898
1146
|
text = truncate_message(text)
|
|
899
|
-
|
|
1147
|
+
# Use the configured prefix format for remote meshnet messages
|
|
1148
|
+
prefix = get_matrix_prefix(config, longname, shortname, short_meshnet_name)
|
|
1149
|
+
full_message = f"{prefix}{text}"
|
|
900
1150
|
else:
|
|
901
1151
|
# If this message is from our local meshnet (loopback), we ignore it
|
|
902
1152
|
return
|
|
@@ -910,8 +1160,7 @@ async def on_room_message(
|
|
|
910
1160
|
# Fallback to global display name if room-specific name is not available
|
|
911
1161
|
display_name_response = await matrix_client.get_displayname(event.sender)
|
|
912
1162
|
full_display_name = display_name_response.displayname or event.sender
|
|
913
|
-
|
|
914
|
-
prefix = f"{short_display_name}[M]: "
|
|
1163
|
+
prefix = get_meshtastic_prefix(config, full_display_name, event.sender)
|
|
915
1164
|
logger.debug(f"Processing matrix message from [{full_display_name}]: {text}")
|
|
916
1165
|
full_message = f"{prefix}{text}"
|
|
917
1166
|
text = truncate_message(text)
|
|
@@ -968,26 +1217,34 @@ async def on_room_message(
|
|
|
968
1217
|
if not found_matching_plugin:
|
|
969
1218
|
if config["meshtastic"]["broadcast_enabled"]:
|
|
970
1219
|
portnum = event.source["content"].get("meshtastic_portnum")
|
|
971
|
-
if portnum ==
|
|
1220
|
+
if portnum == DETECTION_SENSOR_APP:
|
|
972
1221
|
# If detection_sensor is enabled, forward this data as detection sensor data
|
|
973
1222
|
if config["meshtastic"].get("detection_sensor", False):
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1223
|
+
success = queue_message(
|
|
1224
|
+
meshtastic_interface.sendData,
|
|
1225
|
+
data=full_message.encode("utf-8"),
|
|
1226
|
+
channelIndex=meshtastic_channel,
|
|
1227
|
+
portNum=meshtastic.protobuf.portnums_pb2.PortNum.DETECTION_SENSOR_APP,
|
|
1228
|
+
description=f"Detection sensor data from {full_display_name}",
|
|
1229
|
+
)
|
|
1230
|
+
|
|
1231
|
+
if success:
|
|
1232
|
+
# Get queue size to determine logging approach
|
|
1233
|
+
queue_size = get_message_queue().get_queue_size()
|
|
1234
|
+
|
|
1235
|
+
if queue_size > 1:
|
|
1236
|
+
meshtastic_logger.info(
|
|
1237
|
+
f"Relaying detection sensor data from {full_display_name} to radio broadcast (queued: {queue_size} messages)"
|
|
1238
|
+
)
|
|
1239
|
+
else:
|
|
1240
|
+
meshtastic_logger.info(
|
|
1241
|
+
f"Relaying detection sensor data from {full_display_name} to radio broadcast"
|
|
1242
|
+
)
|
|
986
1243
|
# Note: Detection sensor messages are not stored in message_map because they are never replied to
|
|
987
1244
|
# Only TEXT_MESSAGE_APP messages need to be stored for reaction handling
|
|
988
|
-
|
|
1245
|
+
else:
|
|
989
1246
|
meshtastic_logger.error(
|
|
990
|
-
|
|
1247
|
+
"Failed to relay detection sensor data to Meshtastic"
|
|
991
1248
|
)
|
|
992
1249
|
return
|
|
993
1250
|
else:
|
|
@@ -995,53 +1252,47 @@ async def on_room_message(
|
|
|
995
1252
|
f"Detection sensor packet received from {full_display_name}, but detection sensor processing is disabled."
|
|
996
1253
|
)
|
|
997
1254
|
else:
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
)
|
|
1255
|
+
# Regular text message - logging will be handled by queue success handler
|
|
1256
|
+
pass
|
|
1001
1257
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
)
|
|
1006
|
-
|
|
1007
|
-
meshtastic_logger.warning(
|
|
1008
|
-
"sendText returned None - message may not have been sent"
|
|
1009
|
-
)
|
|
1010
|
-
except Exception as e:
|
|
1011
|
-
meshtastic_logger.error(f"Error sending message to Meshtastic: {e}")
|
|
1012
|
-
import traceback
|
|
1258
|
+
# Create mapping info if storage is enabled
|
|
1259
|
+
mapping_info = None
|
|
1260
|
+
if storage_enabled:
|
|
1261
|
+
# Check database config for message map settings (preferred format)
|
|
1262
|
+
msgs_to_keep = _get_msgs_to_keep_config()
|
|
1013
1263
|
|
|
1014
|
-
|
|
1015
|
-
return
|
|
1016
|
-
# Store message_map only if storage is enabled and only for TEXT_MESSAGE_APP
|
|
1017
|
-
# (these are the only messages that can be replied to and thus need reaction handling)
|
|
1018
|
-
if storage_enabled and sent_packet and hasattr(sent_packet, "id"):
|
|
1019
|
-
# Strip quoted lines from text before storing to prevent issues with reactions to replies
|
|
1020
|
-
cleaned_text = strip_quoted_lines(text)
|
|
1021
|
-
store_message_map(
|
|
1022
|
-
sent_packet.id,
|
|
1264
|
+
mapping_info = _create_mapping_info(
|
|
1023
1265
|
event.event_id,
|
|
1024
1266
|
room.room_id,
|
|
1025
|
-
|
|
1026
|
-
|
|
1267
|
+
text,
|
|
1268
|
+
local_meshnet_name,
|
|
1269
|
+
msgs_to_keep,
|
|
1027
1270
|
)
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1271
|
+
|
|
1272
|
+
success = queue_message(
|
|
1273
|
+
meshtastic_interface.sendText,
|
|
1274
|
+
text=full_message,
|
|
1275
|
+
channelIndex=meshtastic_channel,
|
|
1276
|
+
description=f"Message from {full_display_name}",
|
|
1277
|
+
mapping_info=mapping_info,
|
|
1278
|
+
)
|
|
1279
|
+
|
|
1280
|
+
if success:
|
|
1281
|
+
# Get queue size to determine logging approach
|
|
1282
|
+
queue_size = get_message_queue().get_queue_size()
|
|
1283
|
+
|
|
1284
|
+
if queue_size > 1:
|
|
1285
|
+
meshtastic_logger.info(
|
|
1286
|
+
f"Relaying message from {full_display_name} to radio broadcast (queued: {queue_size} messages)"
|
|
1287
|
+
)
|
|
1288
|
+
else:
|
|
1289
|
+
meshtastic_logger.info(
|
|
1290
|
+
f"Relaying message from {full_display_name} to radio broadcast"
|
|
1291
|
+
)
|
|
1292
|
+
else:
|
|
1293
|
+
meshtastic_logger.error("Failed to relay message to Meshtastic")
|
|
1294
|
+
return
|
|
1295
|
+
# Message mapping is now handled automatically by the queue system
|
|
1045
1296
|
else:
|
|
1046
1297
|
logger.debug(
|
|
1047
1298
|
f"Broadcast not supported: Message from {full_display_name} dropped."
|