mmrelay 1.0.10__py3-none-any.whl → 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mmrelay might be problematic. Click here for more details.
- mmrelay/__init__.py +1 -1
- mmrelay/db_utils.py +61 -9
- mmrelay/log_utils.py +56 -0
- mmrelay/main.py +11 -12
- mmrelay/matrix_utils.py +385 -73
- mmrelay/meshtastic_utils.py +158 -30
- mmrelay/tools/sample-docker-compose.yaml +32 -0
- mmrelay/tools/sample.env +10 -0
- mmrelay/tools/sample_config.yaml +6 -3
- {mmrelay-1.0.10.dist-info → mmrelay-1.1.0.dist-info}/METADATA +49 -25
- {mmrelay-1.0.10.dist-info → mmrelay-1.1.0.dist-info}/RECORD +15 -13
- {mmrelay-1.0.10.dist-info → mmrelay-1.1.0.dist-info}/WHEEL +0 -0
- {mmrelay-1.0.10.dist-info → mmrelay-1.1.0.dist-info}/entry_points.txt +0 -0
- {mmrelay-1.0.10.dist-info → mmrelay-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {mmrelay-1.0.10.dist-info → mmrelay-1.1.0.dist-info}/top_level.txt +0 -0
mmrelay/matrix_utils.py
CHANGED
|
@@ -31,6 +31,56 @@ from mmrelay.log_utils import get_logger
|
|
|
31
31
|
# Do not import plugin_loader here to avoid circular imports
|
|
32
32
|
from mmrelay.meshtastic_utils import connect_meshtastic
|
|
33
33
|
|
|
34
|
+
|
|
35
|
+
def get_interaction_settings(config):
|
|
36
|
+
"""
|
|
37
|
+
Returns a dictionary indicating whether message reactions and replies are enabled based on the provided configuration.
|
|
38
|
+
|
|
39
|
+
Supports both the new `message_interactions` structure and the legacy `relay_reactions` flag for backward compatibility. Defaults to disabling both features if not specified.
|
|
40
|
+
"""
|
|
41
|
+
if config is None:
|
|
42
|
+
return {"reactions": False, "replies": False}
|
|
43
|
+
|
|
44
|
+
meshtastic_config = config.get("meshtastic", {})
|
|
45
|
+
|
|
46
|
+
# Check for new structured configuration first
|
|
47
|
+
if "message_interactions" in meshtastic_config:
|
|
48
|
+
interactions = meshtastic_config["message_interactions"]
|
|
49
|
+
return {
|
|
50
|
+
"reactions": interactions.get("reactions", False),
|
|
51
|
+
"replies": interactions.get("replies", False),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Fall back to legacy relay_reactions setting
|
|
55
|
+
if "relay_reactions" in meshtastic_config:
|
|
56
|
+
enabled = meshtastic_config["relay_reactions"]
|
|
57
|
+
logger.warning(
|
|
58
|
+
"Configuration setting 'relay_reactions' is deprecated. "
|
|
59
|
+
"Please use 'message_interactions: {reactions: bool, replies: bool}' instead. "
|
|
60
|
+
"Legacy mode: enabling reactions only."
|
|
61
|
+
)
|
|
62
|
+
return {
|
|
63
|
+
"reactions": enabled,
|
|
64
|
+
"replies": False,
|
|
65
|
+
} # Only reactions for legacy compatibility
|
|
66
|
+
|
|
67
|
+
# Default to privacy-first (both disabled)
|
|
68
|
+
return {"reactions": False, "replies": False}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def message_storage_enabled(interactions):
|
|
72
|
+
"""
|
|
73
|
+
Return True if message storage is required for enabled message interactions.
|
|
74
|
+
|
|
75
|
+
Parameters:
|
|
76
|
+
interactions (dict): Dictionary with 'reactions' and 'replies' boolean flags.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
bool: True if reactions or replies are enabled; otherwise, False.
|
|
80
|
+
"""
|
|
81
|
+
return interactions["reactions"] or interactions["replies"]
|
|
82
|
+
|
|
83
|
+
|
|
34
84
|
# Global config variable that will be set from config.py
|
|
35
85
|
config = None
|
|
36
86
|
|
|
@@ -157,9 +207,9 @@ async def join_matrix_room(matrix_client, room_id_or_alias: str) -> None:
|
|
|
157
207
|
if room_id_or_alias.startswith("#"):
|
|
158
208
|
# If it's a room alias, resolve it to a room ID
|
|
159
209
|
response = await matrix_client.room_resolve_alias(room_id_or_alias)
|
|
160
|
-
if not response.room_id:
|
|
210
|
+
if not hasattr(response, "room_id") or not response.room_id:
|
|
161
211
|
logger.error(
|
|
162
|
-
f"Failed to resolve room alias '{room_id_or_alias}': {response
|
|
212
|
+
f"Failed to resolve room alias '{room_id_or_alias}': {getattr(response, 'message', str(response))}"
|
|
163
213
|
)
|
|
164
214
|
return
|
|
165
215
|
room_id = response.room_id
|
|
@@ -198,16 +248,29 @@ async def matrix_relay(
|
|
|
198
248
|
meshtastic_text=None,
|
|
199
249
|
emote=False,
|
|
200
250
|
emoji=False,
|
|
251
|
+
reply_to_event_id=None,
|
|
201
252
|
):
|
|
202
253
|
"""
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
254
|
+
Relays a message from Meshtastic to a Matrix room, supporting replies and message mapping for interactions.
|
|
255
|
+
|
|
256
|
+
If `reply_to_event_id` is provided, sends the message as a Matrix reply, formatting the content to include quoted original text and appropriate Matrix reply relations. When message interactions (reactions or replies) are enabled in the configuration, stores a mapping between the Meshtastic message ID and the resulting Matrix event ID to enable cross-referencing for future interactions. Prunes old message mappings based on configuration to limit storage.
|
|
257
|
+
|
|
258
|
+
Parameters:
|
|
259
|
+
room_id (str): The Matrix room ID to send the message to.
|
|
260
|
+
message (str): The message content to relay.
|
|
261
|
+
longname (str): The sender's long display name from Meshtastic.
|
|
262
|
+
shortname (str): The sender's short display name from Meshtastic.
|
|
263
|
+
meshnet_name (str): The originating meshnet name.
|
|
264
|
+
portnum (int): The Meshtastic port number.
|
|
265
|
+
meshtastic_id (str, optional): The Meshtastic message ID for mapping.
|
|
266
|
+
meshtastic_replyId (str, optional): The Meshtastic message ID being replied to, if any.
|
|
267
|
+
meshtastic_text (str, optional): The original Meshtastic message text.
|
|
268
|
+
emote (bool, optional): Whether to send the message as an emote.
|
|
269
|
+
emoji (bool, optional): Whether the message is an emoji reaction.
|
|
270
|
+
reply_to_event_id (str, optional): The Matrix event ID being replied to, if sending a reply.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
None
|
|
211
274
|
"""
|
|
212
275
|
global config
|
|
213
276
|
|
|
@@ -221,8 +284,9 @@ async def matrix_relay(
|
|
|
221
284
|
logger.error("No configuration available. Cannot relay message to Matrix.")
|
|
222
285
|
return
|
|
223
286
|
|
|
224
|
-
#
|
|
225
|
-
|
|
287
|
+
# Get interaction settings
|
|
288
|
+
interactions = get_interaction_settings(config)
|
|
289
|
+
storage_enabled = message_storage_enabled(interactions)
|
|
226
290
|
|
|
227
291
|
# Retrieve db config for message_map pruning
|
|
228
292
|
# Check database config for message map settings (preferred format)
|
|
@@ -263,6 +327,40 @@ async def matrix_relay(
|
|
|
263
327
|
if emoji:
|
|
264
328
|
content["meshtastic_emoji"] = 1
|
|
265
329
|
|
|
330
|
+
# Add Matrix reply formatting if this is a reply
|
|
331
|
+
if reply_to_event_id:
|
|
332
|
+
content["m.relates_to"] = {"m.in_reply_to": {"event_id": reply_to_event_id}}
|
|
333
|
+
# For Matrix replies, we need to format the body with quoted content
|
|
334
|
+
# Get the original message details for proper quoting
|
|
335
|
+
try:
|
|
336
|
+
orig = get_message_map_by_matrix_event_id(reply_to_event_id)
|
|
337
|
+
if orig:
|
|
338
|
+
# orig = (meshtastic_id, matrix_room_id, meshtastic_text, meshtastic_meshnet)
|
|
339
|
+
_, _, original_text, original_meshnet = orig
|
|
340
|
+
|
|
341
|
+
# Use the relay bot's user ID for attribution (this is correct for relay messages)
|
|
342
|
+
bot_user_id = matrix_client.user_id
|
|
343
|
+
original_sender_display = f"{longname}/{original_meshnet}"
|
|
344
|
+
|
|
345
|
+
# Create the quoted reply format
|
|
346
|
+
quoted_text = f"> <@{bot_user_id}> [{original_sender_display}]: {original_text}"
|
|
347
|
+
content["body"] = f"{quoted_text}\n\n{message}"
|
|
348
|
+
content["format"] = "org.matrix.custom.html"
|
|
349
|
+
|
|
350
|
+
# Create formatted HTML version with better readability
|
|
351
|
+
reply_link = f"https://matrix.to/#/{room_id}/{reply_to_event_id}"
|
|
352
|
+
bot_link = f"https://matrix.to/#/@{bot_user_id}"
|
|
353
|
+
blockquote_content = f'<a href="{reply_link}">In reply to</a> <a href="{bot_link}">@{bot_user_id}</a><br>[{original_sender_display}]: {original_text}'
|
|
354
|
+
content["formatted_body"] = (
|
|
355
|
+
f"<mx-reply><blockquote>{blockquote_content}</blockquote></mx-reply>{message}"
|
|
356
|
+
)
|
|
357
|
+
else:
|
|
358
|
+
logger.warning(
|
|
359
|
+
f"Could not find original message for reply_to_event_id: {reply_to_event_id}"
|
|
360
|
+
)
|
|
361
|
+
except Exception as e:
|
|
362
|
+
logger.error(f"Error formatting Matrix reply: {e}")
|
|
363
|
+
|
|
266
364
|
try:
|
|
267
365
|
# Ensure matrix_client is not None
|
|
268
366
|
if not matrix_client:
|
|
@@ -292,10 +390,10 @@ async def matrix_relay(
|
|
|
292
390
|
logger.error(f"Error sending message to Matrix room {room_id}: {e}")
|
|
293
391
|
return
|
|
294
392
|
|
|
295
|
-
# Only store message map if
|
|
296
|
-
#
|
|
393
|
+
# Only store message map if any interactions are enabled and conditions are met
|
|
394
|
+
# This enables reactions and/or replies functionality based on configuration
|
|
297
395
|
if (
|
|
298
|
-
|
|
396
|
+
storage_enabled
|
|
299
397
|
and meshtastic_id is not None
|
|
300
398
|
and not emote
|
|
301
399
|
and hasattr(response, "event_id")
|
|
@@ -337,30 +435,209 @@ def truncate_message(text, max_bytes=227):
|
|
|
337
435
|
|
|
338
436
|
def strip_quoted_lines(text: str) -> str:
|
|
339
437
|
"""
|
|
340
|
-
|
|
341
|
-
|
|
438
|
+
Removes lines starting with '>' from the input text.
|
|
439
|
+
|
|
440
|
+
This is typically used to exclude quoted content from Matrix replies, such as when processing reaction text.
|
|
342
441
|
"""
|
|
343
442
|
lines = text.splitlines()
|
|
344
443
|
filtered = [line for line in lines if not line.strip().startswith(">")]
|
|
345
444
|
return " ".join(filtered).strip()
|
|
346
445
|
|
|
347
446
|
|
|
447
|
+
async def get_user_display_name(room, event):
|
|
448
|
+
"""
|
|
449
|
+
Retrieve the display name of a Matrix user, preferring the room-specific name if available.
|
|
450
|
+
|
|
451
|
+
Returns:
|
|
452
|
+
str: The user's display name, or their Matrix ID if no display name is set.
|
|
453
|
+
"""
|
|
454
|
+
room_display_name = room.user_name(event.sender)
|
|
455
|
+
if room_display_name:
|
|
456
|
+
return room_display_name
|
|
457
|
+
|
|
458
|
+
display_name_response = await matrix_client.get_displayname(event.sender)
|
|
459
|
+
return display_name_response.displayname or event.sender
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def format_reply_message(full_display_name, text):
|
|
463
|
+
"""
|
|
464
|
+
Format a reply message by prefixing a truncated display name and removing quoted lines.
|
|
465
|
+
|
|
466
|
+
The resulting message is prefixed with the first five characters of the user's display name followed by "[M]: ", has quoted lines removed, and is truncated to fit within the allowed message length.
|
|
467
|
+
|
|
468
|
+
Parameters:
|
|
469
|
+
full_display_name (str): The user's full display name to be truncated for the prefix.
|
|
470
|
+
text (str): The reply text, possibly containing quoted lines.
|
|
471
|
+
|
|
472
|
+
Returns:
|
|
473
|
+
str: The formatted and truncated reply message.
|
|
474
|
+
"""
|
|
475
|
+
short_display_name = full_display_name[:5]
|
|
476
|
+
prefix = f"{short_display_name}[M]: "
|
|
477
|
+
|
|
478
|
+
# Strip quoted content from the reply text
|
|
479
|
+
clean_text = strip_quoted_lines(text)
|
|
480
|
+
reply_message = f"{prefix}{clean_text}"
|
|
481
|
+
return truncate_message(reply_message)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
async def send_reply_to_meshtastic(
|
|
485
|
+
reply_message,
|
|
486
|
+
full_display_name,
|
|
487
|
+
room_config,
|
|
488
|
+
room,
|
|
489
|
+
event,
|
|
490
|
+
text,
|
|
491
|
+
storage_enabled,
|
|
492
|
+
local_meshnet_name,
|
|
493
|
+
reply_id=None,
|
|
494
|
+
):
|
|
495
|
+
"""
|
|
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.
|
|
499
|
+
|
|
500
|
+
Args:
|
|
501
|
+
reply_id: If provided, sends as a structured Meshtastic reply to this message ID
|
|
502
|
+
"""
|
|
503
|
+
meshtastic_interface = connect_meshtastic()
|
|
504
|
+
from mmrelay.meshtastic_utils import logger as meshtastic_logger
|
|
505
|
+
from mmrelay.meshtastic_utils import sendTextReply
|
|
506
|
+
|
|
507
|
+
meshtastic_channel = room_config["meshtastic_channel"]
|
|
508
|
+
|
|
509
|
+
if config["meshtastic"]["broadcast_enabled"]:
|
|
510
|
+
try:
|
|
511
|
+
if reply_id is not None:
|
|
512
|
+
# 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:
|
|
527
|
+
meshtastic_logger.error(
|
|
528
|
+
f"Error sending structured reply to Meshtastic: {e}"
|
|
529
|
+
)
|
|
530
|
+
return
|
|
531
|
+
else:
|
|
532
|
+
# 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:
|
|
545
|
+
meshtastic_logger.error(
|
|
546
|
+
f"Error sending reply message to Meshtastic: {e}"
|
|
547
|
+
)
|
|
548
|
+
return
|
|
549
|
+
|
|
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)
|
|
572
|
+
|
|
573
|
+
except Exception as e:
|
|
574
|
+
meshtastic_logger.error(f"Error sending Matrix reply to Meshtastic: {e}")
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
async def handle_matrix_reply(
|
|
578
|
+
room,
|
|
579
|
+
event,
|
|
580
|
+
reply_to_event_id,
|
|
581
|
+
text,
|
|
582
|
+
room_config,
|
|
583
|
+
storage_enabled,
|
|
584
|
+
local_meshnet_name,
|
|
585
|
+
):
|
|
586
|
+
"""
|
|
587
|
+
Relays a Matrix reply to the corresponding Meshtastic message if a mapping exists.
|
|
588
|
+
|
|
589
|
+
Looks up the original Meshtastic message using the Matrix event ID being replied to. If found, formats and sends the reply to Meshtastic, preserving conversational context. Returns True if the reply was successfully handled; otherwise, returns False to allow normal message processing.
|
|
590
|
+
|
|
591
|
+
Returns:
|
|
592
|
+
bool: True if the reply was relayed to Meshtastic, False otherwise.
|
|
593
|
+
"""
|
|
594
|
+
# Look up the original message in the message map
|
|
595
|
+
orig = get_message_map_by_matrix_event_id(reply_to_event_id)
|
|
596
|
+
if not orig:
|
|
597
|
+
logger.debug(
|
|
598
|
+
f"Original message for Matrix reply not found in DB: {reply_to_event_id}"
|
|
599
|
+
)
|
|
600
|
+
return False # Continue processing as normal message if original not found
|
|
601
|
+
|
|
602
|
+
# Extract the original meshtastic_id to use as reply_id
|
|
603
|
+
# orig = (meshtastic_id, matrix_room_id, meshtastic_text, meshtastic_meshnet)
|
|
604
|
+
original_meshtastic_id = orig[0]
|
|
605
|
+
|
|
606
|
+
# Get user display name
|
|
607
|
+
full_display_name = await get_user_display_name(room, event)
|
|
608
|
+
|
|
609
|
+
# Format the reply message
|
|
610
|
+
reply_message = format_reply_message(full_display_name, text)
|
|
611
|
+
|
|
612
|
+
logger.info(
|
|
613
|
+
f"Relaying Matrix reply from {full_display_name} to Meshtastic as reply to message {original_meshtastic_id}"
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
# Send the reply to Meshtastic with the original message ID as reply_id
|
|
617
|
+
await send_reply_to_meshtastic(
|
|
618
|
+
reply_message,
|
|
619
|
+
full_display_name,
|
|
620
|
+
room_config,
|
|
621
|
+
room,
|
|
622
|
+
event,
|
|
623
|
+
text,
|
|
624
|
+
storage_enabled,
|
|
625
|
+
local_meshnet_name,
|
|
626
|
+
reply_id=original_meshtastic_id,
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
return True # Reply was handled, stop further processing
|
|
630
|
+
|
|
631
|
+
|
|
348
632
|
# Callback for new messages in Matrix room
|
|
349
633
|
async def on_room_message(
|
|
350
634
|
room: MatrixRoom,
|
|
351
635
|
event: Union[RoomMessageText, RoomMessageNotice, ReactionEvent, RoomMessageEmote],
|
|
352
636
|
) -> None:
|
|
353
637
|
"""
|
|
354
|
-
|
|
355
|
-
to Meshtastic, we always apply our local meshnet_name to outgoing events.
|
|
356
|
-
|
|
357
|
-
We must be careful not to relay reactions to reactions (reaction-chains),
|
|
358
|
-
especially remote reactions that got relayed into the room as m.emote events,
|
|
359
|
-
as we do not store them in the database. If we can't find the original message in the DB,
|
|
360
|
-
it likely means it's a reaction to a reaction, and we stop there.
|
|
638
|
+
Handles incoming Matrix room messages, reactions, and replies, relaying them to Meshtastic as appropriate.
|
|
361
639
|
|
|
362
|
-
|
|
363
|
-
if relay_reactions is True. If it's False, none of these mappings are stored or used.
|
|
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.
|
|
364
641
|
"""
|
|
365
642
|
# Importing here to avoid circular imports and to keep logic consistent
|
|
366
643
|
# Note: We do not call store_message_map directly here for inbound matrix->mesh messages.
|
|
@@ -403,8 +680,9 @@ async def on_room_message(
|
|
|
403
680
|
logger.error("No configuration available. Cannot process Matrix message.")
|
|
404
681
|
return
|
|
405
682
|
|
|
406
|
-
#
|
|
407
|
-
|
|
683
|
+
# Get interaction settings
|
|
684
|
+
interactions = get_interaction_settings(config)
|
|
685
|
+
storage_enabled = message_storage_enabled(interactions)
|
|
408
686
|
|
|
409
687
|
# Check if this is a Matrix ReactionEvent (usually m.reaction)
|
|
410
688
|
if isinstance(event, ReactionEvent):
|
|
@@ -441,17 +719,26 @@ async def on_room_message(
|
|
|
441
719
|
if suppress:
|
|
442
720
|
return
|
|
443
721
|
|
|
444
|
-
# If this is a reaction and
|
|
445
|
-
if is_reaction and not
|
|
722
|
+
# If this is a reaction and reactions are disabled, do nothing
|
|
723
|
+
if is_reaction and not interactions["reactions"]:
|
|
446
724
|
logger.debug(
|
|
447
|
-
"Reaction event encountered but
|
|
725
|
+
"Reaction event encountered but reactions are disabled. Doing nothing."
|
|
448
726
|
)
|
|
449
727
|
return
|
|
450
728
|
|
|
451
729
|
local_meshnet_name = config["meshtastic"]["meshnet_name"]
|
|
452
730
|
|
|
453
|
-
#
|
|
454
|
-
|
|
731
|
+
# Check if this is a Matrix reply (not a reaction)
|
|
732
|
+
is_reply = False
|
|
733
|
+
reply_to_event_id = None
|
|
734
|
+
if not is_reaction and relates_to and "m.in_reply_to" in relates_to:
|
|
735
|
+
reply_to_event_id = relates_to["m.in_reply_to"].get("event_id")
|
|
736
|
+
if reply_to_event_id:
|
|
737
|
+
is_reply = True
|
|
738
|
+
logger.debug(f"Processing Matrix reply to event: {reply_to_event_id}")
|
|
739
|
+
|
|
740
|
+
# If this is a reaction and reactions are enabled, attempt to relay it
|
|
741
|
+
if is_reaction and interactions["reactions"]:
|
|
455
742
|
# Check if we need to relay a reaction from a remote meshnet to our local meshnet.
|
|
456
743
|
# If meshnet_name != local_meshnet_name and meshtastic_replyId is present and this is an emote,
|
|
457
744
|
# it's a remote reaction that needs to be forwarded as a text message describing the reaction.
|
|
@@ -498,9 +785,16 @@ async def on_room_message(
|
|
|
498
785
|
logger.debug(
|
|
499
786
|
f"Sending reaction to Meshtastic with meshnet={local_meshnet_name}: {reaction_message}"
|
|
500
787
|
)
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
788
|
+
try:
|
|
789
|
+
sent_packet = meshtastic_interface.sendText(
|
|
790
|
+
text=reaction_message, channelIndex=meshtastic_channel
|
|
791
|
+
)
|
|
792
|
+
logger.debug(
|
|
793
|
+
f"Remote reaction sendText returned packet: {sent_packet}"
|
|
794
|
+
)
|
|
795
|
+
except Exception as e:
|
|
796
|
+
logger.error(f"Error sending remote reaction to Meshtastic: {e}")
|
|
797
|
+
return
|
|
504
798
|
# We've relayed the remote reaction to our local mesh, so we're done.
|
|
505
799
|
return
|
|
506
800
|
|
|
@@ -561,9 +855,30 @@ async def on_room_message(
|
|
|
561
855
|
logger.debug(
|
|
562
856
|
f"Sending reaction to Meshtastic with meshnet={local_meshnet_name}: {reaction_message}"
|
|
563
857
|
)
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
858
|
+
try:
|
|
859
|
+
sent_packet = meshtastic_interface.sendText(
|
|
860
|
+
text=reaction_message, channelIndex=meshtastic_channel
|
|
861
|
+
)
|
|
862
|
+
logger.debug(
|
|
863
|
+
f"Local reaction sendText returned packet: {sent_packet}"
|
|
864
|
+
)
|
|
865
|
+
except Exception as e:
|
|
866
|
+
logger.error(f"Error sending local reaction to Meshtastic: {e}")
|
|
867
|
+
return
|
|
868
|
+
return
|
|
869
|
+
|
|
870
|
+
# Handle Matrix replies to Meshtastic messages (only if replies are enabled)
|
|
871
|
+
if is_reply and reply_to_event_id and interactions["replies"]:
|
|
872
|
+
reply_handled = await handle_matrix_reply(
|
|
873
|
+
room,
|
|
874
|
+
event,
|
|
875
|
+
reply_to_event_id,
|
|
876
|
+
text,
|
|
877
|
+
room_config,
|
|
878
|
+
storage_enabled,
|
|
879
|
+
local_meshnet_name,
|
|
880
|
+
)
|
|
881
|
+
if reply_handled:
|
|
567
882
|
return
|
|
568
883
|
|
|
569
884
|
# For Matrix->Mesh messages from a remote meshnet, rewrite the message format
|
|
@@ -656,38 +971,25 @@ async def on_room_message(
|
|
|
656
971
|
if portnum == "DETECTION_SENSOR_APP":
|
|
657
972
|
# If detection_sensor is enabled, forward this data as detection sensor data
|
|
658
973
|
if config["meshtastic"].get("detection_sensor", False):
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
room.room_id,
|
|
671
|
-
text,
|
|
672
|
-
meshtastic_meshnet=local_meshnet_name,
|
|
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}"
|
|
673
985
|
)
|
|
674
|
-
#
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
legacy_msg_map_config = db_config.get("msg_map", {})
|
|
682
|
-
|
|
683
|
-
if legacy_msg_map_config:
|
|
684
|
-
msg_map_config = legacy_msg_map_config
|
|
685
|
-
logger.warning(
|
|
686
|
-
"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."
|
|
687
|
-
)
|
|
688
|
-
msgs_to_keep = msg_map_config.get("msgs_to_keep", 500)
|
|
689
|
-
if msgs_to_keep > 0:
|
|
690
|
-
prune_message_map(msgs_to_keep)
|
|
986
|
+
# Note: Detection sensor messages are not stored in message_map because they are never replied to
|
|
987
|
+
# Only TEXT_MESSAGE_APP messages need to be stored for reaction handling
|
|
988
|
+
except Exception as e:
|
|
989
|
+
meshtastic_logger.error(
|
|
990
|
+
f"Error sending detection sensor data to Meshtastic: {e}"
|
|
991
|
+
)
|
|
992
|
+
return
|
|
691
993
|
else:
|
|
692
994
|
meshtastic_logger.debug(
|
|
693
995
|
f"Detection sensor packet received from {full_display_name}, but detection sensor processing is disabled."
|
|
@@ -701,16 +1003,26 @@ async def on_room_message(
|
|
|
701
1003
|
sent_packet = meshtastic_interface.sendText(
|
|
702
1004
|
text=full_message, channelIndex=meshtastic_channel
|
|
703
1005
|
)
|
|
1006
|
+
if not sent_packet:
|
|
1007
|
+
meshtastic_logger.warning(
|
|
1008
|
+
"sendText returned None - message may not have been sent"
|
|
1009
|
+
)
|
|
704
1010
|
except Exception as e:
|
|
705
1011
|
meshtastic_logger.error(f"Error sending message to Meshtastic: {e}")
|
|
1012
|
+
import traceback
|
|
1013
|
+
|
|
1014
|
+
meshtastic_logger.error(f"Full traceback: {traceback.format_exc()}")
|
|
706
1015
|
return
|
|
707
|
-
# Store message_map only if
|
|
708
|
-
|
|
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)
|
|
709
1021
|
store_message_map(
|
|
710
1022
|
sent_packet.id,
|
|
711
1023
|
event.event_id,
|
|
712
1024
|
room.room_id,
|
|
713
|
-
|
|
1025
|
+
cleaned_text,
|
|
714
1026
|
meshtastic_meshnet=local_meshnet_name,
|
|
715
1027
|
)
|
|
716
1028
|
# Check database config for message map settings (preferred format)
|