diff --git a/contact/message_handlers/rx_handler.py b/contact/message_handlers/rx_handler.py index 746c15a..936440f 100644 --- a/contact/message_handlers/rx_handler.py +++ b/contact/message_handlers/rx_handler.py @@ -135,6 +135,10 @@ def on_receive(packet: Dict[str, Any], interface: Any) -> None: maybe_store_nodeinfo_in_db(packet) elif packet["decoded"]["portnum"] == "TEXT_MESSAGE_APP": + # Queued packets retain the time the node received them. + timestamp = packet.get("rxTime") + if not isinstance(timestamp, int) or timestamp <= 0: + timestamp = int(time.time()) hop_start = packet.get('hopStart', 0) hop_limit = packet.get('hopLimit', 0) @@ -189,6 +193,7 @@ def on_receive(packet: Dict[str, Any], interface: Any) -> None: f"{config.message_prefix} [{hops}] {message_from_string} ", f"{reply_context}{message_string}", packet_id=packet.get("id"), + timestamp=timestamp, ) if refresh_channels: @@ -206,6 +211,7 @@ def on_receive(packet: Dict[str, Any], interface: Any) -> None: message_string, packet_id=packet.get("id"), reply_id=reply_id, + timestamp=timestamp, ) except KeyError as e: diff --git a/contact/utilities/db_handler.py b/contact/utilities/db_handler.py index 6ea88c9..1606905 100644 --- a/contact/utilities/db_handler.py +++ b/contact/utilities/db_handler.py @@ -35,6 +35,7 @@ def save_message_to_db( message_text: str, packet_id: Optional[int] = None, reply_id: Optional[int] = None, + timestamp: Optional[int] = None, ) -> Optional[int]: """Save messages to the database, ensuring the table exists.""" try: @@ -54,7 +55,8 @@ def save_message_to_db( db_connection.execute("PRAGMA busy_timeout=10000") db_cursor = db_connection.cursor() _ensure_message_columns(db_cursor, quoted_table_name) - timestamp = int(time.time()) + if timestamp is None: + timestamp = int(time.time()) # Insert the message insert_query = f""" diff --git a/contact/utilities/utils.py b/contact/utilities/utils.py index e311e51..14b0249 100644 --- a/contact/utilities/utils.py +++ b/contact/utilities/utils.py @@ -224,7 +224,7 @@ def get_reply_context(reply_id): return "" -def add_new_message(channel_id, prefix, message, packet_id=None): +def add_new_message(channel_id, prefix, message, packet_id=None, timestamp=None): if channel_id not in ui_state.all_messages: ui_state.all_messages[channel_id] = [] @@ -233,8 +233,10 @@ def add_new_message(channel_id, prefix, message, packet_id=None): packet_ids.append(None) # Timestamp handling - current_timestamp = time.time() - current_hour = datetime.datetime.fromtimestamp(current_timestamp).strftime("%Y-%m-%d %H:00") + if timestamp is None: + timestamp = time.time() + message_time = datetime.datetime.fromtimestamp(timestamp) + current_hour = message_time.strftime("%Y-%m-%d %H:00") # Retrieve the last timestamp if available channel_messages = ui_state.all_messages[channel_id] @@ -255,7 +257,7 @@ def add_new_message(channel_id, prefix, message, packet_id=None): packet_ids.append(None) # Add the message - ts_str = time.strftime("[%H:%M:%S] ") + ts_str = message_time.strftime("[%H:%M:%S] ") ui_state.all_messages[channel_id].append((f"{ts_str}{prefix}", message)) packet_ids.append(packet_id) diff --git a/tests/test_db_handler.py b/tests/test_db_handler.py index 0e378c6..b8ec334 100644 --- a/tests/test_db_handler.py +++ b/tests/test_db_handler.py @@ -1,3 +1,4 @@ +from datetime import datetime import os import sqlite3 import tempfile @@ -45,6 +46,18 @@ class DbHandlerTests(unittest.TestCase): self.assertEqual(row, ("123", "hello", "Ack")) + def test_receive_timestamp_survives_history_reload(self) -> None: + timestamp = 1700000000 + result = db_handler.save_message_to_db("Primary", "456", "queued", timestamp=timestamp) + self.assertEqual(result, timestamp) + with sqlite3.connect(config.db_file_path) as conn: + stored = conn.execute('SELECT timestamp FROM "123_Primary_messages"').fetchone()[0] + self.assertEqual(stored, timestamp) + db_handler.load_messages_from_db() + prefix, message = ui_state.all_messages["Primary"][-1] + self.assertEqual(message, "queued") + self.assertTrue(prefix.startswith(datetime.fromtimestamp(timestamp).strftime("[%H:%M:%S]"))) + def test_message_ids_are_migrated_and_reloaded_with_history(self) -> None: db_handler.ensure_table_exists( '"123_Primary_messages"', diff --git a/tests/test_rx_handler.py b/tests/test_rx_handler.py index 830d205..0242e99 100644 --- a/tests/test_rx_handler.py +++ b/tests/test_rx_handler.py @@ -1,3 +1,4 @@ +from datetime import datetime import unittest from unittest import mock @@ -52,7 +53,7 @@ class RxHandlerTests(unittest.TestCase): ], ) add_notification.assert_not_called() - save_message_to_db.assert_called_once_with("Primary", 222, "hello", packet_id=None, reply_id=None) + save_message_to_db.assert_called_once_with("Primary", 222, "hello", packet_id=None, reply_id=None, timestamp=mock.ANY) self.assertEqual(ui_state.all_messages["Primary"][-1][1], "hello") self.assertIn("SAT2:", ui_state.all_messages["Primary"][-1][0]) self.assertIn("[2]", ui_state.all_messages["Primary"][-1][0]) @@ -111,7 +112,7 @@ class RxHandlerTests(unittest.TestCase): request_ui_redraw.assert_called_once_with(channels=True) add_notification.assert_called_once_with(1) update_node_info_in_db.assert_called_once_with(222, chat_archived=False) - save_message_to_db.assert_called_once_with(222, 222, "dm", packet_id=None, reply_id=None) + save_message_to_db.assert_called_once_with(222, 222, "dm", packet_id=None, reply_id=None, timestamp=mock.ANY) def test_on_receive_displays_context_for_native_reply_id(self) -> None: interface_state.myNodeNum = 111 @@ -137,7 +138,32 @@ class RxHandlerTests(unittest.TestCase): self.assertEqual(ui_state.all_messages["Primary"][-1][1], " hi") self.assertEqual(ui_state.message_packet_ids["Primary"][-1], 901) - save_message_to_db.assert_called_once_with("Primary", 222, "hi", packet_id=901, reply_id=900) + save_message_to_db.assert_called_once_with("Primary", 222, "hi", packet_id=901, reply_id=900, timestamp=mock.ANY) + + def test_node_receive_time_is_used_for_display_and_storage(self) -> None: + interface_state.myNodeNum = 111 + ui_state.channel_list = ["Primary"] + ui_state.selected_channel = 0 + for rx_time in (1700000000, 1700007200, None, 0): + with self.subTest(rx_time=rx_time): + ui_state.all_messages = {"Primary": []} + packet = { + "from": 222, "to": 999, "rxTime": rx_time, + "decoded": {"portnum": "TEXT_MESSAGE_APP", "payload": b"queued"}, + } + expected = rx_time or 1800000000 + with mock.patch.object(rx_handler, "refresh_node_list", return_value=False), \ + mock.patch.object(rx_handler, "request_ui_redraw"), \ + mock.patch.object(rx_handler, "get_name_from_database", return_value="NODE"), \ + mock.patch.object(rx_handler, "save_message_to_db") as save, \ + mock.patch.object(rx_handler.time, "time", return_value=1800000000): + rx_handler.on_receive(packet, interface=None) + self.assertEqual(save.call_args.kwargs["timestamp"], expected) + message_time = datetime.fromtimestamp(expected) + self.assertEqual(ui_state.all_messages["Primary"][0][0], + message_time.strftime("-- %Y-%m-%d %H:00 --")) + self.assertTrue(ui_state.all_messages["Primary"][-1][0].startswith( + message_time.strftime("[%H:%M:%S] "))) def test_on_receive_trims_packet_buffer_even_when_packet_is_undecoded(self) -> None: ui_state.packet_buffer = list(range(25)) diff --git a/tests/test_utils.py b/tests/test_utils.py index e036652..4196faf 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -81,19 +81,18 @@ class UtilsTests(unittest.TestCase): def test_add_new_message_groups_messages_by_hour(self) -> None: ui_state.all_messages = {"MediumFast": []} - with mock.patch("contact.utilities.utils.time.time", side_effect=[1000, 1000]): - with mock.patch("contact.utilities.utils.time.strftime", return_value="[00:16:40] "): - with mock.patch("contact.utilities.utils.datetime.datetime") as mocked_datetime: - mocked_datetime.fromtimestamp.return_value.strftime.return_value = "2025-02-04 17:00" - add_new_message("MediumFast", ">> Test: ", "First") - add_new_message("MediumFast", ">> Test: ", "Second") + from datetime import datetime + timestamp = datetime(2025, 2, 4, 17, 16, 40).timestamp() + with mock.patch("contact.utilities.utils.time.time", return_value=timestamp): + add_new_message("MediumFast", ">> Test: ", "First") + add_new_message("MediumFast", ">> Test: ", "Second") self.assertEqual( ui_state.all_messages["MediumFast"], [ ("-- 2025-02-04 17:00 --", ""), - ("[00:16:40] >> Test: ", "First"), - ("[00:16:40] >> Test: ", "Second"), + ("[17:16:40] >> Test: ", "First"), + ("[17:16:40] >> Test: ", "Second"), ], )