mmrelay 1.1.2__py3-none-any.whl → 1.1.3__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/main.py +20 -2
- mmrelay/matrix_utils.py +405 -144
- mmrelay/meshtastic_utils.py +21 -8
- mmrelay/message_queue.py +475 -0
- mmrelay/plugin_loader.py +2 -2
- mmrelay/plugins/base_plugin.py +92 -39
- mmrelay/tools/sample_config.yaml +13 -3
- {mmrelay-1.1.2.dist-info → mmrelay-1.1.3.dist-info}/METADATA +10 -12
- {mmrelay-1.1.2.dist-info → mmrelay-1.1.3.dist-info}/RECORD +14 -13
- mmrelay-1.1.3.dist-info/licenses/LICENSE +675 -0
- mmrelay-1.1.2.dist-info/licenses/LICENSE +0 -21
- {mmrelay-1.1.2.dist-info → mmrelay-1.1.3.dist-info}/WHEEL +0 -0
- {mmrelay-1.1.2.dist-info → mmrelay-1.1.3.dist-info}/entry_points.txt +0 -0
- {mmrelay-1.1.2.dist-info → mmrelay-1.1.3.dist-info}/top_level.txt +0 -0
mmrelay/matrix_utils.py
CHANGED
|
@@ -29,7 +29,74 @@ from mmrelay.db_utils import (
|
|
|
29
29
|
from mmrelay.log_utils import get_logger
|
|
30
30
|
|
|
31
31
|
# Do not import plugin_loader here to avoid circular imports
|
|
32
|
-
from mmrelay.meshtastic_utils import connect_meshtastic
|
|
32
|
+
from mmrelay.meshtastic_utils import connect_meshtastic, sendTextReply
|
|
33
|
+
from mmrelay.message_queue import get_message_queue, queue_message
|
|
34
|
+
|
|
35
|
+
logger = get_logger(name="matrix_utils")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _get_msgs_to_keep_config():
|
|
39
|
+
"""
|
|
40
|
+
Retrieve the configured number of messages to retain for message mapping, supporting both current and legacy configuration formats.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
int: The number of messages to keep in the database for message mapping (default is 500).
|
|
44
|
+
"""
|
|
45
|
+
global config
|
|
46
|
+
if not config:
|
|
47
|
+
return 500
|
|
48
|
+
|
|
49
|
+
msg_map_config = config.get("database", {}).get("msg_map", {})
|
|
50
|
+
|
|
51
|
+
# If not found in database config, check legacy db config
|
|
52
|
+
if not msg_map_config:
|
|
53
|
+
legacy_msg_map_config = config.get("db", {}).get("msg_map", {})
|
|
54
|
+
|
|
55
|
+
if legacy_msg_map_config:
|
|
56
|
+
msg_map_config = legacy_msg_map_config
|
|
57
|
+
logger.warning(
|
|
58
|
+
"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."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return msg_map_config.get("msgs_to_keep", 500)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _create_mapping_info(
|
|
65
|
+
matrix_event_id, room_id, text, meshnet=None, msgs_to_keep=None
|
|
66
|
+
):
|
|
67
|
+
"""
|
|
68
|
+
Constructs a dictionary containing metadata for mapping a Matrix event to a Meshtastic message in the message queue.
|
|
69
|
+
|
|
70
|
+
Removes quoted lines from the message text and includes relevant identifiers and configuration for message retention. Returns `None` if required parameters are missing.
|
|
71
|
+
|
|
72
|
+
Parameters:
|
|
73
|
+
matrix_event_id: The Matrix event ID to map.
|
|
74
|
+
room_id: The Matrix room ID where the event occurred.
|
|
75
|
+
text: The message text to be mapped; quoted lines are removed.
|
|
76
|
+
meshnet: Optional name of the target mesh network.
|
|
77
|
+
msgs_to_keep: Optional number of messages to retain for mapping; uses configuration default if not provided.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
dict: A dictionary with mapping information for use by the message queue, or `None` if required fields are missing.
|
|
81
|
+
"""
|
|
82
|
+
if not matrix_event_id or not room_id or not text:
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
if msgs_to_keep is None:
|
|
86
|
+
msgs_to_keep = _get_msgs_to_keep_config()
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
"matrix_event_id": matrix_event_id,
|
|
90
|
+
"room_id": room_id,
|
|
91
|
+
"text": strip_quoted_lines(text),
|
|
92
|
+
"meshnet": meshnet,
|
|
93
|
+
"msgs_to_keep": msgs_to_keep,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# Default prefix format constants
|
|
98
|
+
DEFAULT_MESHTASTIC_PREFIX = "{display5}[M]: "
|
|
99
|
+
DEFAULT_MATRIX_PREFIX = "[{long}/{mesh}]: "
|
|
33
100
|
|
|
34
101
|
|
|
35
102
|
def get_interaction_settings(config):
|
|
@@ -70,15 +137,184 @@ 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
|
+
Generate a formatted prefix for Meshtastic messages relayed to Matrix, using configurable templates and variable-length truncation for sender and mesh network names.
|
|
74
248
|
|
|
75
249
|
Parameters:
|
|
76
|
-
|
|
250
|
+
config (dict): The application configuration dictionary.
|
|
251
|
+
longname (str): Full Meshtastic sender name.
|
|
252
|
+
shortname (str): Short Meshtastic sender name.
|
|
253
|
+
meshnet_name (str): Name of the mesh network.
|
|
77
254
|
|
|
78
255
|
Returns:
|
|
79
|
-
|
|
256
|
+
str: The formatted prefix string if prefixing is enabled; otherwise, an empty string.
|
|
257
|
+
|
|
258
|
+
Examples:
|
|
259
|
+
Basic usage:
|
|
260
|
+
get_matrix_prefix(config, "Alice", "Ali", "MyMesh")
|
|
261
|
+
# Returns: "[Alice/MyMesh]: " (with default format)
|
|
262
|
+
|
|
263
|
+
Custom format:
|
|
264
|
+
config = {"matrix": {"prefix_format": "({long4}): "}}
|
|
265
|
+
get_matrix_prefix(config, "Alice", "Ali", "MyMesh")
|
|
266
|
+
# Returns: "(Alic): "
|
|
80
267
|
"""
|
|
81
|
-
|
|
268
|
+
matrix_config = config.get("matrix", {})
|
|
269
|
+
|
|
270
|
+
# Enhanced debug logging for configuration troubleshooting
|
|
271
|
+
logger.debug(
|
|
272
|
+
f"get_matrix_prefix called with longname='{longname}', shortname='{shortname}', meshnet_name='{meshnet_name}'"
|
|
273
|
+
)
|
|
274
|
+
logger.debug(f"Matrix config section: {matrix_config}")
|
|
275
|
+
|
|
276
|
+
# Check if prefixes are enabled for Matrix direction
|
|
277
|
+
if not matrix_config.get("prefix_enabled", True):
|
|
278
|
+
logger.debug("Matrix prefixes are disabled, returning empty string")
|
|
279
|
+
return ""
|
|
280
|
+
|
|
281
|
+
# Get custom format or use default
|
|
282
|
+
matrix_prefix_format = matrix_config.get("prefix_format", DEFAULT_MATRIX_PREFIX)
|
|
283
|
+
logger.debug(
|
|
284
|
+
f"Using matrix prefix format: '{matrix_prefix_format}' (default: '{DEFAULT_MATRIX_PREFIX}')"
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
# Available variables for formatting with variable length support
|
|
288
|
+
format_vars = {
|
|
289
|
+
"long": longname,
|
|
290
|
+
"short": shortname,
|
|
291
|
+
"mesh": meshnet_name,
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
# Add variable length truncation for longname and mesh name
|
|
295
|
+
_add_truncated_vars(format_vars, "long", longname)
|
|
296
|
+
_add_truncated_vars(format_vars, "mesh", meshnet_name)
|
|
297
|
+
|
|
298
|
+
try:
|
|
299
|
+
result = matrix_prefix_format.format(**format_vars)
|
|
300
|
+
logger.debug(
|
|
301
|
+
f"Matrix prefix generated: '{result}' using format '{matrix_prefix_format}' with vars {format_vars}"
|
|
302
|
+
)
|
|
303
|
+
# Additional debug to help identify the issue
|
|
304
|
+
if result == f"[{longname}/{meshnet_name}]: ":
|
|
305
|
+
logger.debug(
|
|
306
|
+
"Generated prefix matches default format - check if custom configuration is being loaded correctly"
|
|
307
|
+
)
|
|
308
|
+
return result
|
|
309
|
+
except (KeyError, ValueError) as e:
|
|
310
|
+
# Fallback to default format if custom format is invalid
|
|
311
|
+
logger.warning(
|
|
312
|
+
f"Invalid matrix prefix_format '{matrix_prefix_format}': {e}. Using default format."
|
|
313
|
+
)
|
|
314
|
+
# The default format only uses 'long' and 'mesh', which are safe
|
|
315
|
+
return DEFAULT_MATRIX_PREFIX.format(
|
|
316
|
+
long=longname or "", mesh=meshnet_name or ""
|
|
317
|
+
)
|
|
82
318
|
|
|
83
319
|
|
|
84
320
|
# Global config variable that will be set from config.py
|
|
@@ -459,7 +695,7 @@ async def get_user_display_name(room, event):
|
|
|
459
695
|
return display_name_response.displayname or event.sender
|
|
460
696
|
|
|
461
697
|
|
|
462
|
-
def format_reply_message(full_display_name, text):
|
|
698
|
+
def format_reply_message(config, full_display_name, text):
|
|
463
699
|
"""
|
|
464
700
|
Format a reply message by prefixing a truncated display name and removing quoted lines.
|
|
465
701
|
|
|
@@ -472,8 +708,7 @@ def format_reply_message(full_display_name, text):
|
|
|
472
708
|
Returns:
|
|
473
709
|
str: The formatted and truncated reply message.
|
|
474
710
|
"""
|
|
475
|
-
|
|
476
|
-
prefix = f"{short_display_name}[M]: "
|
|
711
|
+
prefix = get_meshtastic_prefix(config, full_display_name)
|
|
477
712
|
|
|
478
713
|
# Strip quoted content from the reply text
|
|
479
714
|
clean_text = strip_quoted_lines(text)
|
|
@@ -493,82 +728,86 @@ async def send_reply_to_meshtastic(
|
|
|
493
728
|
reply_id=None,
|
|
494
729
|
):
|
|
495
730
|
"""
|
|
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.
|
|
731
|
+
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
732
|
|
|
500
|
-
|
|
501
|
-
reply_id: If provided, sends as a structured Meshtastic reply to this message ID
|
|
733
|
+
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
734
|
"""
|
|
503
735
|
meshtastic_interface = connect_meshtastic()
|
|
504
736
|
from mmrelay.meshtastic_utils import logger as meshtastic_logger
|
|
505
|
-
from mmrelay.meshtastic_utils import sendTextReply
|
|
506
737
|
|
|
507
738
|
meshtastic_channel = room_config["meshtastic_channel"]
|
|
508
739
|
|
|
509
740
|
if config["meshtastic"]["broadcast_enabled"]:
|
|
510
741
|
try:
|
|
742
|
+
# Create mapping info once if storage is enabled
|
|
743
|
+
mapping_info = None
|
|
744
|
+
if storage_enabled:
|
|
745
|
+
# Get message map configuration
|
|
746
|
+
msgs_to_keep = _get_msgs_to_keep_config()
|
|
747
|
+
|
|
748
|
+
mapping_info = _create_mapping_info(
|
|
749
|
+
event.event_id, room.room_id, text, local_meshnet_name, msgs_to_keep
|
|
750
|
+
)
|
|
751
|
+
|
|
511
752
|
if reply_id is not None:
|
|
512
753
|
# Send as a structured reply using our custom function
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
754
|
+
# Queue the reply message
|
|
755
|
+
success = queue_message(
|
|
756
|
+
sendTextReply,
|
|
757
|
+
meshtastic_interface,
|
|
758
|
+
text=reply_message,
|
|
759
|
+
reply_id=reply_id,
|
|
760
|
+
channelIndex=meshtastic_channel,
|
|
761
|
+
description=f"Reply from {full_display_name} to message {reply_id}",
|
|
762
|
+
mapping_info=mapping_info,
|
|
763
|
+
)
|
|
764
|
+
|
|
765
|
+
if success:
|
|
766
|
+
# Get queue size to determine logging approach
|
|
767
|
+
queue_size = get_message_queue().get_queue_size()
|
|
768
|
+
|
|
769
|
+
if queue_size > 1:
|
|
770
|
+
meshtastic_logger.info(
|
|
771
|
+
f"Relaying Matrix reply from {full_display_name} to radio broadcast as structured reply (queued: {queue_size} messages)"
|
|
772
|
+
)
|
|
773
|
+
else:
|
|
774
|
+
meshtastic_logger.info(
|
|
775
|
+
f"Relaying Matrix reply from {full_display_name} to radio broadcast as structured reply"
|
|
776
|
+
)
|
|
777
|
+
else:
|
|
527
778
|
meshtastic_logger.error(
|
|
528
|
-
|
|
779
|
+
"Failed to relay structured reply to Meshtastic"
|
|
529
780
|
)
|
|
530
781
|
return
|
|
531
782
|
else:
|
|
532
783
|
# Send as regular message (fallback for when no reply_id is available)
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
784
|
+
success = queue_message(
|
|
785
|
+
meshtastic_interface.sendText,
|
|
786
|
+
text=reply_message,
|
|
787
|
+
channelIndex=meshtastic_channel,
|
|
788
|
+
description=f"Reply from {full_display_name} (fallback to regular message)",
|
|
789
|
+
mapping_info=mapping_info,
|
|
790
|
+
)
|
|
791
|
+
|
|
792
|
+
if success:
|
|
793
|
+
# Get queue size to determine logging approach
|
|
794
|
+
queue_size = get_message_queue().get_queue_size()
|
|
795
|
+
|
|
796
|
+
if queue_size > 1:
|
|
797
|
+
meshtastic_logger.info(
|
|
798
|
+
f"Relaying Matrix reply from {full_display_name} to radio broadcast (queued: {queue_size} messages)"
|
|
799
|
+
)
|
|
800
|
+
else:
|
|
801
|
+
meshtastic_logger.info(
|
|
802
|
+
f"Relaying Matrix reply from {full_display_name} to radio broadcast"
|
|
803
|
+
)
|
|
804
|
+
else:
|
|
545
805
|
meshtastic_logger.error(
|
|
546
|
-
|
|
806
|
+
"Failed to relay reply message to Meshtastic"
|
|
547
807
|
)
|
|
548
808
|
return
|
|
549
809
|
|
|
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)
|
|
810
|
+
# Message mapping is now handled automatically by the queue system
|
|
572
811
|
|
|
573
812
|
except Exception as e:
|
|
574
813
|
meshtastic_logger.error(f"Error sending Matrix reply to Meshtastic: {e}")
|
|
@@ -582,6 +821,7 @@ async def handle_matrix_reply(
|
|
|
582
821
|
room_config,
|
|
583
822
|
storage_enabled,
|
|
584
823
|
local_meshnet_name,
|
|
824
|
+
config,
|
|
585
825
|
):
|
|
586
826
|
"""
|
|
587
827
|
Relays a Matrix reply to the corresponding Meshtastic message if a mapping exists.
|
|
@@ -607,7 +847,7 @@ async def handle_matrix_reply(
|
|
|
607
847
|
full_display_name = await get_user_display_name(room, event)
|
|
608
848
|
|
|
609
849
|
# Format the reply message
|
|
610
|
-
reply_message = format_reply_message(full_display_name, text)
|
|
850
|
+
reply_message = format_reply_message(config, full_display_name, text)
|
|
611
851
|
|
|
612
852
|
logger.info(
|
|
613
853
|
f"Relaying Matrix reply from {full_display_name} to Meshtastic as reply to message {original_meshtastic_id}"
|
|
@@ -635,12 +875,14 @@ async def on_room_message(
|
|
|
635
875
|
event: Union[RoomMessageText, RoomMessageNotice, ReactionEvent, RoomMessageEmote],
|
|
636
876
|
) -> None:
|
|
637
877
|
"""
|
|
638
|
-
|
|
878
|
+
Handle incoming Matrix room messages, reactions, and replies, relaying them to Meshtastic as appropriate.
|
|
639
879
|
|
|
640
|
-
|
|
880
|
+
This function processes Matrix events—including text messages, reactions, and replies—received in configured Matrix rooms. It relays supported messages to the Meshtastic mesh network if broadcasting is enabled, applying message mapping for cross-referencing when reactions or replies are enabled. The function prevents relaying of reactions to reactions, ignores messages from the bot itself or those sent before the bot started, and integrates with plugins for command and message handling. Only messages that are not commands or handled by plugins are forwarded to Meshtastic, with proper formatting and truncation as needed.
|
|
641
881
|
"""
|
|
642
882
|
# Importing here to avoid circular imports and to keep logic consistent
|
|
643
883
|
# Note: We do not call store_message_map directly here for inbound matrix->mesh messages.
|
|
884
|
+
from mmrelay.message_queue import get_message_queue
|
|
885
|
+
|
|
644
886
|
# That logic occurs inside matrix_relay if needed.
|
|
645
887
|
full_display_name = "Unknown user"
|
|
646
888
|
message_timestamp = event.server_timestamp
|
|
@@ -785,15 +1027,19 @@ async def on_room_message(
|
|
|
785
1027
|
logger.debug(
|
|
786
1028
|
f"Sending reaction to Meshtastic with meshnet={local_meshnet_name}: {reaction_message}"
|
|
787
1029
|
)
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
1030
|
+
success = queue_message(
|
|
1031
|
+
meshtastic_interface.sendText,
|
|
1032
|
+
text=reaction_message,
|
|
1033
|
+
channelIndex=meshtastic_channel,
|
|
1034
|
+
description=f"Remote reaction from {meshnet_name}",
|
|
1035
|
+
)
|
|
1036
|
+
|
|
1037
|
+
if success:
|
|
792
1038
|
logger.debug(
|
|
793
|
-
f"
|
|
1039
|
+
f"Queued remote reaction to Meshtastic: {reaction_message}"
|
|
794
1040
|
)
|
|
795
|
-
|
|
796
|
-
logger.error(
|
|
1041
|
+
else:
|
|
1042
|
+
logger.error("Failed to relay remote reaction to Meshtastic")
|
|
797
1043
|
return
|
|
798
1044
|
# We've relayed the remote reaction to our local mesh, so we're done.
|
|
799
1045
|
return
|
|
@@ -824,8 +1070,7 @@ async def on_room_message(
|
|
|
824
1070
|
full_display_name = display_name_response.displayname or event.sender
|
|
825
1071
|
|
|
826
1072
|
# If not from a remote meshnet, proceed as normal to relay back to the originating meshnet
|
|
827
|
-
|
|
828
|
-
prefix = f"{short_display_name}[M]: "
|
|
1073
|
+
prefix = get_meshtastic_prefix(config, full_display_name)
|
|
829
1074
|
|
|
830
1075
|
# Remove quoted lines so we don't bring in the original '>' lines from replies
|
|
831
1076
|
meshtastic_text_db = strip_quoted_lines(meshtastic_text_db)
|
|
@@ -855,15 +1100,19 @@ async def on_room_message(
|
|
|
855
1100
|
logger.debug(
|
|
856
1101
|
f"Sending reaction to Meshtastic with meshnet={local_meshnet_name}: {reaction_message}"
|
|
857
1102
|
)
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
1103
|
+
success = queue_message(
|
|
1104
|
+
meshtastic_interface.sendText,
|
|
1105
|
+
text=reaction_message,
|
|
1106
|
+
channelIndex=meshtastic_channel,
|
|
1107
|
+
description=f"Local reaction from {full_display_name}",
|
|
1108
|
+
)
|
|
1109
|
+
|
|
1110
|
+
if success:
|
|
862
1111
|
logger.debug(
|
|
863
|
-
f"
|
|
1112
|
+
f"Queued local reaction to Meshtastic: {reaction_message}"
|
|
864
1113
|
)
|
|
865
|
-
|
|
866
|
-
logger.error(
|
|
1114
|
+
else:
|
|
1115
|
+
logger.error("Failed to relay local reaction to Meshtastic")
|
|
867
1116
|
return
|
|
868
1117
|
return
|
|
869
1118
|
|
|
@@ -877,6 +1126,7 @@ async def on_room_message(
|
|
|
877
1126
|
room_config,
|
|
878
1127
|
storage_enabled,
|
|
879
1128
|
local_meshnet_name,
|
|
1129
|
+
config,
|
|
880
1130
|
)
|
|
881
1131
|
if reply_handled:
|
|
882
1132
|
return
|
|
@@ -893,10 +1143,20 @@ async def on_room_message(
|
|
|
893
1143
|
# If shortname is not available, derive it from the longname
|
|
894
1144
|
if shortname is None:
|
|
895
1145
|
shortname = longname[:3] if longname else "???"
|
|
896
|
-
# Remove the original prefix
|
|
897
|
-
|
|
1146
|
+
# Remove the original prefix to avoid double-tagging
|
|
1147
|
+
# Get the prefix that would have been used for this message
|
|
1148
|
+
original_prefix = get_matrix_prefix(
|
|
1149
|
+
config, longname, shortname, meshnet_name
|
|
1150
|
+
)
|
|
1151
|
+
if original_prefix and text.startswith(original_prefix):
|
|
1152
|
+
text = text[len(original_prefix) :]
|
|
1153
|
+
logger.debug(
|
|
1154
|
+
f"Removed original prefix '{original_prefix}' from remote meshnet message"
|
|
1155
|
+
)
|
|
898
1156
|
text = truncate_message(text)
|
|
899
|
-
|
|
1157
|
+
# Use the configured prefix format for remote meshnet messages
|
|
1158
|
+
prefix = get_matrix_prefix(config, longname, shortname, short_meshnet_name)
|
|
1159
|
+
full_message = f"{prefix}{text}"
|
|
900
1160
|
else:
|
|
901
1161
|
# If this message is from our local meshnet (loopback), we ignore it
|
|
902
1162
|
return
|
|
@@ -910,8 +1170,7 @@ async def on_room_message(
|
|
|
910
1170
|
# Fallback to global display name if room-specific name is not available
|
|
911
1171
|
display_name_response = await matrix_client.get_displayname(event.sender)
|
|
912
1172
|
full_display_name = display_name_response.displayname or event.sender
|
|
913
|
-
|
|
914
|
-
prefix = f"{short_display_name}[M]: "
|
|
1173
|
+
prefix = get_meshtastic_prefix(config, full_display_name, event.sender)
|
|
915
1174
|
logger.debug(f"Processing matrix message from [{full_display_name}]: {text}")
|
|
916
1175
|
full_message = f"{prefix}{text}"
|
|
917
1176
|
text = truncate_message(text)
|
|
@@ -971,23 +1230,31 @@ async def on_room_message(
|
|
|
971
1230
|
if portnum == "DETECTION_SENSOR_APP":
|
|
972
1231
|
# If detection_sensor is enabled, forward this data as detection sensor data
|
|
973
1232
|
if config["meshtastic"].get("detection_sensor", False):
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1233
|
+
success = queue_message(
|
|
1234
|
+
meshtastic_interface.sendData,
|
|
1235
|
+
data=full_message.encode("utf-8"),
|
|
1236
|
+
channelIndex=meshtastic_channel,
|
|
1237
|
+
portNum=meshtastic.protobuf.portnums_pb2.PortNum.DETECTION_SENSOR_APP,
|
|
1238
|
+
description=f"Detection sensor data from {full_display_name}",
|
|
1239
|
+
)
|
|
1240
|
+
|
|
1241
|
+
if success:
|
|
1242
|
+
# Get queue size to determine logging approach
|
|
1243
|
+
queue_size = get_message_queue().get_queue_size()
|
|
1244
|
+
|
|
1245
|
+
if queue_size > 1:
|
|
1246
|
+
meshtastic_logger.info(
|
|
1247
|
+
f"Relaying detection sensor data from {full_display_name} to radio broadcast (queued: {queue_size} messages)"
|
|
1248
|
+
)
|
|
1249
|
+
else:
|
|
1250
|
+
meshtastic_logger.info(
|
|
1251
|
+
f"Relaying detection sensor data from {full_display_name} to radio broadcast"
|
|
1252
|
+
)
|
|
986
1253
|
# Note: Detection sensor messages are not stored in message_map because they are never replied to
|
|
987
1254
|
# Only TEXT_MESSAGE_APP messages need to be stored for reaction handling
|
|
988
|
-
|
|
1255
|
+
else:
|
|
989
1256
|
meshtastic_logger.error(
|
|
990
|
-
|
|
1257
|
+
"Failed to relay detection sensor data to Meshtastic"
|
|
991
1258
|
)
|
|
992
1259
|
return
|
|
993
1260
|
else:
|
|
@@ -995,53 +1262,47 @@ async def on_room_message(
|
|
|
995
1262
|
f"Detection sensor packet received from {full_display_name}, but detection sensor processing is disabled."
|
|
996
1263
|
)
|
|
997
1264
|
else:
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
)
|
|
1265
|
+
# Regular text message - logging will be handled by queue success handler
|
|
1266
|
+
pass
|
|
1001
1267
|
|
|
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
|
|
1268
|
+
# Create mapping info if storage is enabled
|
|
1269
|
+
mapping_info = None
|
|
1270
|
+
if storage_enabled:
|
|
1271
|
+
# Check database config for message map settings (preferred format)
|
|
1272
|
+
msgs_to_keep = _get_msgs_to_keep_config()
|
|
1013
1273
|
|
|
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,
|
|
1274
|
+
mapping_info = _create_mapping_info(
|
|
1023
1275
|
event.event_id,
|
|
1024
1276
|
room.room_id,
|
|
1025
|
-
|
|
1026
|
-
|
|
1277
|
+
text,
|
|
1278
|
+
local_meshnet_name,
|
|
1279
|
+
msgs_to_keep,
|
|
1027
1280
|
)
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1281
|
+
|
|
1282
|
+
success = queue_message(
|
|
1283
|
+
meshtastic_interface.sendText,
|
|
1284
|
+
text=full_message,
|
|
1285
|
+
channelIndex=meshtastic_channel,
|
|
1286
|
+
description=f"Message from {full_display_name}",
|
|
1287
|
+
mapping_info=mapping_info,
|
|
1288
|
+
)
|
|
1289
|
+
|
|
1290
|
+
if success:
|
|
1291
|
+
# Get queue size to determine logging approach
|
|
1292
|
+
queue_size = get_message_queue().get_queue_size()
|
|
1293
|
+
|
|
1294
|
+
if queue_size > 1:
|
|
1295
|
+
meshtastic_logger.info(
|
|
1296
|
+
f"Relaying message from {full_display_name} to radio broadcast (queued: {queue_size} messages)"
|
|
1297
|
+
)
|
|
1298
|
+
else:
|
|
1299
|
+
meshtastic_logger.info(
|
|
1300
|
+
f"Relaying message from {full_display_name} to radio broadcast"
|
|
1301
|
+
)
|
|
1302
|
+
else:
|
|
1303
|
+
meshtastic_logger.error("Failed to relay message to Meshtastic")
|
|
1304
|
+
return
|
|
1305
|
+
# Message mapping is now handled automatically by the queue system
|
|
1045
1306
|
else:
|
|
1046
1307
|
logger.debug(
|
|
1047
1308
|
f"Broadcast not supported: Message from {full_display_name} dropped."
|