Document and patch historical prefix claiming, and handle reconnect interval better

This commit is contained in:
Jack Kingsman
2026-02-09 20:25:21 -08:00
parent 9294ffe138
commit cf215c4ba5
6 changed files with 39 additions and 2 deletions

View File

@@ -362,6 +362,16 @@ packet, `packet_processor.py` handles the complete flow:
export, unknown contact), `on_contact_message` handles DMs from the MeshCore library's
`CONTACT_MSG_RECV` event. DB deduplication prevents double-storage when both paths fire.
**Prefix-stored DMs (edge case)**: A rare scenario can occur when the radio can decrypt
a DM (contact is on the radio) but the server cannot (private key not exported or
contact not yet known server-side). The `CONTACT_MSG_RECV` payload may only include a
pubkey prefix. If the full key isn't known yet, the message is stored with the prefix
as `conversation_key`. When a full contact key becomes known (via advertisement or
radio sync), the server attempts to **claim** those prefix messages and upgrade them
to the full key. Claims only occur when the prefix matches exactly one contact to
avoid mis-attribution in large contact sets. Until claimed, these DMs will not show
in the UI because conversations are keyed by full public keys.
**Outgoing DMs**: Outgoing direct messages are only sent via the app's REST API
(`POST /api/messages/direct`), which stores the plaintext directly in the database.
No decryption is needed for outgoing DMs. The real-time packet processor may also see

View File

@@ -993,3 +993,4 @@ async def _migrate_015_fix_null_sender_timestamp(conn: aiosqlite.Connection) ->
)
await conn.commit()

View File

@@ -685,6 +685,13 @@ async def _process_advertisement(
}
await ContactRepository.upsert(contact_data)
claimed = await MessageRepository.claim_prefix_messages(advert.public_key.lower())
if claimed > 0:
logger.info(
"Claimed %d prefix DM message(s) for contact %s",
claimed,
advert.public_key[:12],
)
# Broadcast contact update to connected clients
broadcast_event(

View File

@@ -18,7 +18,7 @@ from meshcore import EventType
from app.models import Contact
from app.radio import radio_manager
from app.repository import AppSettingsRepository, ChannelRepository, ContactRepository
from app.repository import AppSettingsRepository, ChannelRepository, ContactRepository, MessageRepository
logger = logging.getLogger(__name__)
@@ -95,6 +95,13 @@ async def sync_and_offload_contacts() -> dict:
await ContactRepository.upsert(
Contact.from_radio_dict(public_key, contact_data, on_radio=False)
)
claimed = await MessageRepository.claim_prefix_messages(public_key.lower())
if claimed > 0:
logger.info(
"Claimed %d prefix DM message(s) for contact %s",
claimed,
public_key[:12],
)
synced += 1
# Remove from radio

View File

@@ -390,7 +390,11 @@ class MessageRepository:
cursor = await db.conn.execute(
"""UPDATE messages SET conversation_key = ?
WHERE type = 'PRIV' AND length(conversation_key) < 64
AND ? LIKE conversation_key || '%'""",
AND ? LIKE conversation_key || '%'
AND (
SELECT COUNT(*) FROM contacts
WHERE public_key LIKE messages.conversation_key || '%'
) = 1""",
(lower_key, lower_key),
)
await db.conn.commit()

View File

@@ -60,6 +60,10 @@ export function useWebSocket(options: UseWebSocketOptions) {
ws.onopen = () => {
console.log('WebSocket connected');
setConnected(true);
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
reconnectTimeoutRef.current = null;
}
};
ws.onclose = () => {
@@ -68,6 +72,9 @@ export function useWebSocket(options: UseWebSocketOptions) {
wsRef.current = null;
// Reconnect after 3 seconds
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
}
reconnectTimeoutRef.current = window.setTimeout(() => {
console.log('Attempting WebSocket reconnect...');
connect();
@@ -146,6 +153,7 @@ export function useWebSocket(options: UseWebSocketOptions) {
clearInterval(pingInterval);
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
reconnectTimeoutRef.current = null;
}
if (wsRef.current) {
wsRef.current.close();