diff --git a/contact/message_handlers/bot_handler.py b/contact/message_handlers/bot_handler.py index 374d2a3..4b88225 100644 --- a/contact/message_handlers/bot_handler.py +++ b/contact/message_handlers/bot_handler.py @@ -40,7 +40,8 @@ def bot_respond(packet: Dict[str, Any], message: str, send_channel: int) -> bool return False snr = packet.get('rxSnr', -128) rssi = packet.get('rxRssi', -128) - replyIDset = (packet.get('decoded') or {}).get('replyId', False) + incoming_reply_id = (packet.get('decoded') or {}).get('replyId') + packet_id = packet.get("id") hop_start = packet.get('hopStart', 0) hop_limit = packet.get('hopLimit', 0) transport_type = packet.get('transportMechanism', None) @@ -53,8 +54,8 @@ def bot_respond(packet: Dict[str, Any], message: str, send_channel: int) -> bool details.append(f"RSSI: {rssi}") if hops != 0: details.append(f"Hops: {hops}") - if replyIDset: - details.append(f"Relay: {replyIDset}") + if incoming_reply_id: + details.append(f"Relay: {incoming_reply_id}") transport_text = str(transport_type).upper() if transport_type is not None else "" for transport_name in ("UDP", "MQTT"): if transport_name in transport_text: @@ -72,7 +73,9 @@ def bot_respond(packet: Dict[str, Any], message: str, send_channel: int) -> bool if not ui_state.bot_mode_enabled: return - send_message(response_data_string,channel=send_channel) + # Use the triggering packet's ID so Meshtastic clients render this + # automatic response as a native reply when that ID is available. + send_message(response_data_string, channel=send_channel, reply_id=packet_id) # Import locally to avoid circular import at module import time. from contact.ui.contact_ui import request_ui_redraw diff --git a/tests/test_bot_handler.py b/tests/test_bot_handler.py index 250082c..ae73b91 100644 --- a/tests/test_bot_handler.py +++ b/tests/test_bot_handler.py @@ -5,6 +5,8 @@ import types from unittest import mock import contact.ui.default_config as config +from contact.utilities.singleton import interface_state, ui_state +from tests.test_support import reset_singletons class BotHandlerTests(unittest.TestCase): @@ -16,6 +18,13 @@ class BotHandlerTests(unittest.TestCase): ) cls.bot_handler = importlib.import_module("contact.message_handlers.bot_handler") + def setUp(self) -> None: + reset_singletons() + self.bot_handler.send_message.reset_mock() + + def tearDown(self) -> None: + reset_singletons() + def test_is_bot_message_uses_configured_catch_words(self) -> None: with mock.patch.object(config, "ping_bot_catch_words", "ping; test; pong"): self.assertTrue(self.bot_handler.is_bot_message("PING")) @@ -26,6 +35,25 @@ class BotHandlerTests(unittest.TestCase): with mock.patch.object(config, "ping_bot_catch_words", " ; ; "): self.assertTrue(self.bot_handler.is_bot_message("ping")) + def test_bot_response_replies_to_triggering_packet(self) -> None: + ui_state.bot_mode_enabled = True + interface_state.myNodeNum = 111 + packet = {"id": 900, "from": 222, "decoded": {}} + + with mock.patch.object(self.bot_handler.threading, "Thread") as thread: + self.assertTrue(self.bot_handler.bot_respond(packet, "ping", 0)) + send_response = thread.call_args.kwargs["target"] + with mock.patch.object(self.bot_handler.time, "sleep"): + with mock.patch.dict( + sys.modules, + {"contact.ui.contact_ui": types.SimpleNamespace(request_ui_redraw=mock.Mock())}, + ): + send_response() + + self.bot_handler.send_message.assert_called_once_with( + "Pong!", channel=0, reply_id=900 + ) + if __name__ == "__main__": unittest.main()