From e76d9227522bd71b32edf3c6b958929f00c71228 Mon Sep 17 00:00:00 2001 From: Jack Kingsman Date: Thu, 30 Apr 2026 19:07:50 -0700 Subject: [PATCH] Add recieved time to packet display. Closes #238. --- app/models.py | 2 + app/packet_processor.py | 6 ++ app/routers/packets.py | 4 + frontend/src/test/rawPacketStats.test.ts | 2 + frontend/src/types.ts | 2 + frontend/src/utils/rawPacketInspector.ts | 95 +++++++++++++----------- tests/test_packets_router.py | 2 + 7 files changed, 68 insertions(+), 45 deletions(-) diff --git a/app/models.py b/app/models.py index 2e58eb6..232c113 100644 --- a/app/models.py +++ b/app/models.py @@ -448,6 +448,8 @@ class RawPacketDecryptedInfo(BaseModel): sender: str | None = None channel_key: str | None = None contact_key: str | None = None + sender_timestamp: int | None = None + message: str | None = None class RawPacketBroadcast(BaseModel): diff --git a/app/packet_processor.py b/app/packet_processor.py index 36ab6d0..8c84d16 100644 --- a/app/packet_processor.py +++ b/app/packet_processor.py @@ -366,6 +366,8 @@ async def process_raw_packet( sender=result["sender"], channel_key=result.get("channel_key"), contact_key=result.get("contact_key"), + sender_timestamp=result.get("sender_timestamp"), + message=result.get("message"), ) if result["decrypted"] else None, @@ -428,6 +430,8 @@ async def _process_group_text( "sender": decrypted.sender, "message_id": msg_id, # None if duplicate, msg_id if new "channel_key": channel.key, + "sender_timestamp": decrypted.timestamp, + "message": decrypted.message, } # Couldn't decrypt with any known key @@ -694,6 +698,8 @@ async def _process_direct_message( "sender": contact.name or contact.public_key[:12], "message_id": msg_id, "contact_key": contact.public_key, + "sender_timestamp": result.timestamp, + "message": result.message, } # Couldn't decrypt with any known contact diff --git a/app/routers/packets.py b/app/routers/packets.py index 7dab832..6422d94 100644 --- a/app/routers/packets.py +++ b/app/routers/packets.py @@ -128,11 +128,15 @@ async def get_raw_packet(packet_id: int) -> RawPacketDetail: sender=message.sender_name, channel_key=message.conversation_key, contact_key=message.sender_key, + sender_timestamp=message.sender_timestamp, + message=message.text, ) else: decrypted_info = RawPacketDecryptedInfo( sender=message.sender_name, contact_key=message.conversation_key, + sender_timestamp=message.sender_timestamp, + message=message.text, ) return RawPacketDetail( diff --git a/frontend/src/test/rawPacketStats.test.ts b/frontend/src/test/rawPacketStats.test.ts index d9d8b9d..27fc2ac 100644 --- a/frontend/src/test/rawPacketStats.test.ts +++ b/frontend/src/test/rawPacketStats.test.ts @@ -94,6 +94,8 @@ describe('buildRawPacketStatsSnapshot', () => { sender: 'Alpha', channel_key: null, contact_key: '0a'.repeat(32), + sender_timestamp: null, + message: null, }, }; diff --git a/frontend/src/types.ts b/frontend/src/types.ts index cd80136..1472b5b 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -343,6 +343,8 @@ export interface RawPacket { sender: string | null; channel_key: string | null; contact_key: string | null; + sender_timestamp: number | null; + message: string | null; } | null; } diff --git a/frontend/src/utils/rawPacketInspector.ts b/frontend/src/utils/rawPacketInspector.ts index 73409a4..190a2bb 100644 --- a/frontend/src/utils/rawPacketInspector.ts +++ b/frontend/src/utils/rawPacketInspector.ts @@ -324,51 +324,56 @@ export function inspectRawPacketWithOptions( createPacketField('payload', `payload-${index}`, segment, structure.payload.startByte) ); - const enrichedPayloadFields = - decoded?.isValid && decoded.payloadType === PayloadType.GroupText && decoded.payload.decoded - ? payloadFields.map((field) => { - if (field.name !== 'Ciphertext') { - return field; - } - const payload = decoded.payload.decoded as { - decrypted?: { timestamp?: number; flags?: number; sender?: string; message?: string }; - }; - if (!payload.decrypted?.message) { - return field; - } - const detailLines = [ - payload.decrypted.timestamp != null - ? `Timestamp: ${formatUnixTimestamp(payload.decrypted.timestamp)}` - : null, - payload.decrypted.flags != null - ? `Flags: 0x${payload.decrypted.flags.toString(16).padStart(2, '0')}` - : null, - payload.decrypted.sender ? `Sender: ${payload.decrypted.sender}` : null, - `Message: ${payload.decrypted.message}`, - ].filter((line): line is string => line !== null); - return { - ...field, - description: describeCiphertextStructure( - decoded.payloadType, - field.endByte - field.startByte + 1, - field.description - ), - decryptedMessage: detailLines.join('\n'), - }; - }) - : payloadFields.map((field) => { - if (!decoded?.isValid || field.name !== 'Ciphertext') { - return field; - } - return { - ...field, - description: describeCiphertextStructure( - decoded.payloadType, - field.endByte - field.startByte + 1, - field.description - ), - }; - }); + const enrichedPayloadFields = payloadFields.map((field) => { + if (!decoded?.isValid || field.name !== 'Ciphertext') { + return field; + } + + const withStructure = { + ...field, + description: describeCiphertextStructure( + decoded.payloadType, + field.endByte - field.startByte + 1, + field.description + ), + }; + + // GroupText: client-side decoder has the decrypted content + if (decoded.payloadType === PayloadType.GroupText && decoded.payload.decoded) { + const payload = decoded.payload.decoded as { + decrypted?: { timestamp?: number; flags?: number; sender?: string; message?: string }; + }; + if (!payload.decrypted?.message) { + return withStructure; + } + const detailLines = [ + payload.decrypted.timestamp != null + ? `Sent (packet): ${formatUnixTimestamp(payload.decrypted.timestamp)}` + : null, + payload.decrypted.flags != null + ? `Flags: 0x${payload.decrypted.flags.toString(16).padStart(2, '0')}` + : null, + payload.decrypted.sender ? `Sender: ${payload.decrypted.sender}` : null, + `Message: ${payload.decrypted.message}`, + ].filter((line): line is string => line !== null); + return { ...withStructure, decryptedMessage: detailLines.join('\n') }; + } + + // TextMessage (DM): server-side decryption via decrypted_info + if (decoded.payloadType === PayloadType.TextMessage && packet.decrypted_info?.message) { + const info = packet.decrypted_info; + const detailLines = [ + info.sender_timestamp != null + ? `Sent (packet): ${formatUnixTimestamp(info.sender_timestamp)}` + : null, + info.sender ? `Sender: ${info.sender}` : null, + `Message: ${info.message}`, + ].filter((line): line is string => line !== null); + return { ...withStructure, decryptedMessage: detailLines.join('\n') }; + } + + return withStructure; + }); return { decoded, diff --git a/tests/test_packets_router.py b/tests/test_packets_router.py index 4d14418..2b38b01 100644 --- a/tests/test_packets_router.py +++ b/tests/test_packets_router.py @@ -95,6 +95,8 @@ class TestGetRawPacket: "sender": "Alice", "channel_key": channel_key, "contact_key": None, + "sender_timestamp": 1700000000, + "message": "Alice: hello", }