Compare commits

..
6 Commits
5 changed files with 73 additions and 9 deletions

No files matched your search

+7 -4
View File
@@ -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
+19 -4
View File
@@ -160,13 +160,28 @@ def get_time_ago(timestamp):
REPLY_EXCERPT_LENGTH = 5
def _local_short_name() -> str:
"""Return this node's short name without depending on the message database."""
nodes = getattr(interface_state.interface, "nodes", {}) or {}
for node in nodes.values():
if node.get("num") == interface_state.myNodeNum:
short_name = node.get("user", {}).get("shortName")
if short_name:
return str(short_name)
return ""
def build_reply_prefix(prefix: str, message: str) -> str:
"""Create Contact's local display marker for a native Meshtastic reply."""
sender_text = re.sub(r"^\[[^]]+\]\s*", "", prefix).strip()
sender_text = re.sub(r"^(?:>>|<<)\s*(?:\[[^]]+\]\s*)?", "", sender_text)
sender_match = re.search(r"(.+?)\s*:\s*$", sender_text)
sender = sender_match.group(1).strip() if sender_match else "me"
excerpt = " ".join(message.replace("\x00", "").split())[:REPLY_EXCERPT_LENGTH]
if sender_text.startswith(config.sent_message_prefix.strip()):
sender = _local_short_name()
else:
sender_text = re.sub(r"^(?:>>|<<)\s*(?:\[[^]]+\]\s*)?", "", sender_text)
sender_match = re.search(r"(.+?)\s*:\s*$", sender_text)
sender = sender_match.group(1).strip() if sender_match else ""
message_without_reply_marker = re.sub(r"^<Re:\s*[^>]*>\s*", "", message.replace("\x00", ""))
excerpt = " ".join(message_without_reply_marker.split())[:REPLY_EXCERPT_LENGTH]
return f"<Re: {sender}: {excerpt}> "
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "contact"
version = "1.5.9"
version = "1.5.11"
description = "This Python curses client for Meshtastic is a terminal-based client designed to manage device settings, enable mesh chat communication, and handle configuration backups and restores."
authors = [
{name = "Ben Lipsey",email = "ben@pdxlocations.com"}
+28
View File
@@ -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()
+18
View File
@@ -1,4 +1,5 @@
import unittest
from types import SimpleNamespace
from unittest import mock
import contact.ui.default_config as config
@@ -110,6 +111,23 @@ class ContactUiTests(unittest.TestCase):
self.assertEqual(reply, "<Re: B1G1: This > ")
def test_build_reply_prefix_uses_me_for_an_outgoing_message(self) -> None:
contact_ui.interface_state.myNodeNum = 123
contact_ui.interface_state.interface = SimpleNamespace(
nodes={"!0000007b": {"num": 123, "user": {"shortName": "LRY"}}}
)
reply = contact_ui.build_reply_prefix("[06:27:25] >> Sent[☼]: ", "Thanks Lairy")
self.assertEqual(reply, "<Re: LRY: Thank> ")
def test_build_reply_prefix_does_not_nest_an_existing_reply_marker(self) -> None:
reply = contact_ui.build_reply_prefix(
"[06:27:25] >> xT1e: ",
"<Re: CATS: Woot!> Woot indeed",
)
self.assertEqual(reply, "<Re: xT1e: Woot > ")
def test_handle_ctrl_r_prefills_reply_for_message_at_cursor(self) -> None:
ui_state.current_window = 1
ui_state.channel_list = ["Primary"]