mmrelay 1.1.1__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/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
- Return True if message storage is required for enabled message interactions.
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
- interactions (dict): Dictionary with 'reactions' and 'replies' boolean flags.
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
- bool: True if reactions or replies are enabled; otherwise, False.
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
- return interactions["reactions"] or interactions["replies"]
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
- short_display_name = full_display_name[:5]
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
- Sends a reply message from Matrix to Meshtastic and optionally stores the message mapping for future interactions.
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
- Args:
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
- try:
514
- sent_packet = sendTextReply(
515
- meshtastic_interface,
516
- text=reply_message,
517
- reply_id=reply_id,
518
- channelIndex=meshtastic_channel,
519
- )
520
- meshtastic_logger.info(
521
- f"Relaying Matrix reply from {full_display_name} to radio broadcast as structured reply to message {reply_id}"
522
- )
523
- meshtastic_logger.debug(
524
- f"sendTextReply returned packet: {sent_packet}"
525
- )
526
- except Exception as e:
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
- f"Error sending structured reply to Meshtastic: {e}"
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
- try:
534
- meshtastic_logger.debug(
535
- f"Attempting to send text to Meshtastic: '{reply_message}' on channel {meshtastic_channel}"
536
- )
537
- sent_packet = meshtastic_interface.sendText(
538
- text=reply_message, channelIndex=meshtastic_channel
539
- )
540
- meshtastic_logger.info(
541
- f"Relaying Matrix reply from {full_display_name} to radio broadcast as regular message"
542
- )
543
- meshtastic_logger.debug(f"sendText returned packet: {sent_packet}")
544
- except Exception as e:
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
- f"Error sending reply message to Meshtastic: {e}"
806
+ "Failed to relay reply message to Meshtastic"
547
807
  )
548
808
  return
549
809
 
550
- # Store the reply in message map if storage is enabled
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
- Handles incoming Matrix room messages, reactions, and replies, relaying them to Meshtastic as appropriate.
878
+ Handle incoming Matrix room messages, reactions, and replies, relaying them to Meshtastic as appropriate.
639
879
 
640
- Processes events from Matrix rooms, including text messages, reactions, and replies. Relays supported messages to Meshtastic if broadcasting is enabled, applying message mapping for cross-referencing when reactions or replies are enabled. Prevents relaying of reactions to reactions and avoids processing messages from the bot itself or messages sent before the bot started. Integrates with plugins for command and message handling, and ensures that only supported messages are forwarded to Meshtastic.
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
- try:
789
- sent_packet = meshtastic_interface.sendText(
790
- text=reaction_message, channelIndex=meshtastic_channel
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"Remote reaction sendText returned packet: {sent_packet}"
1039
+ f"Queued remote reaction to Meshtastic: {reaction_message}"
794
1040
  )
795
- except Exception as e:
796
- logger.error(f"Error sending remote reaction to Meshtastic: {e}")
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
- short_display_name = full_display_name[:5]
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
- try:
859
- sent_packet = meshtastic_interface.sendText(
860
- text=reaction_message, channelIndex=meshtastic_channel
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"Local reaction sendText returned packet: {sent_packet}"
1112
+ f"Queued local reaction to Meshtastic: {reaction_message}"
864
1113
  )
865
- except Exception as e:
866
- logger.error(f"Error sending local reaction to Meshtastic: {e}")
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 "[longname/meshnet]: " to avoid double-tagging
897
- text = re.sub(rf"^\[{full_display_name}\]: ", "", text)
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
- full_message = f"{shortname}/{short_meshnet_name}: {text}"
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
- short_display_name = full_display_name[:5]
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
- try:
975
- meshtastic_logger.debug(
976
- f"Attempting to send detection sensor data to Meshtastic: '{full_message}' on channel {meshtastic_channel}"
977
- )
978
- sent_packet = meshtastic_interface.sendData(
979
- data=full_message.encode("utf-8"),
980
- channelIndex=meshtastic_channel,
981
- portNum=meshtastic.protobuf.portnums_pb2.PortNum.DETECTION_SENSOR_APP,
982
- )
983
- meshtastic_logger.debug(
984
- f"sendData returned packet: {sent_packet}"
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
- except Exception as e:
1255
+ else:
989
1256
  meshtastic_logger.error(
990
- f"Error sending detection sensor data to Meshtastic: {e}"
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
- meshtastic_logger.info(
999
- f"Relaying message from {full_display_name} to radio broadcast"
1000
- )
1265
+ # Regular text message - logging will be handled by queue success handler
1266
+ pass
1001
1267
 
1002
- try:
1003
- sent_packet = meshtastic_interface.sendText(
1004
- text=full_message, channelIndex=meshtastic_channel
1005
- )
1006
- if not sent_packet:
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
- meshtastic_logger.error(f"Full traceback: {traceback.format_exc()}")
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
- cleaned_text,
1026
- meshtastic_meshnet=local_meshnet_name,
1277
+ text,
1278
+ local_meshnet_name,
1279
+ msgs_to_keep,
1027
1280
  )
1028
- # Check database config for message map settings (preferred format)
1029
- database_config = config.get("database", {})
1030
- msg_map_config = database_config.get("msg_map", {})
1031
-
1032
- # If not found in database config, check legacy db config
1033
- if not msg_map_config:
1034
- db_config = config.get("db", {})
1035
- legacy_msg_map_config = db_config.get("msg_map", {})
1036
-
1037
- if legacy_msg_map_config:
1038
- msg_map_config = legacy_msg_map_config
1039
- logger.warning(
1040
- "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."
1041
- )
1042
- msgs_to_keep = msg_map_config.get("msgs_to_keep", 500)
1043
- if msgs_to_keep > 0:
1044
- prune_message_map(msgs_to_keep)
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."