From 66fa26115186ac87b17150a71fed8f89a9a376f1 Mon Sep 17 00:00:00 2001 From: MarekWo Date: Sat, 7 Mar 2026 08:09:24 +0100 Subject: [PATCH] fix(dm): prevent orphaned DMs on contact deletion, improve relinking - Stop deleting contacts from DB on device removal (preserves DM history) - Filter NULL contact_pubkey from DM conversations list - Match outgoing DMs by contact name in raw_json during relinking Co-Authored-By: Claude Opus 4.6 --- app/database.py | 28 +++++++++++++++++++--------- app/device_manager.py | 8 +++++--- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/app/database.py b/app/database.py index 8958070..3544f94 100644 --- a/app/database.py +++ b/app/database.py @@ -344,6 +344,7 @@ class Database: ORDER BY d3.timestamp DESC LIMIT 1) AS last_direction FROM direct_messages dm LEFT JOIN contacts c ON dm.contact_pubkey = c.public_key + WHERE dm.contact_pubkey IS NOT NULL GROUP BY dm.contact_pubkey ORDER BY last_message_timestamp DESC""" ).fetchall() @@ -382,22 +383,31 @@ class Database: ).fetchone() return dict(row) if row else None - def relink_orphaned_dms(self, public_key: str) -> int: + def relink_orphaned_dms(self, public_key: str, name: str = '') -> int: """Re-link DMs with NULL contact_pubkey back to this contact. When a contact is deleted, ON DELETE SET NULL nullifies contact_pubkey. When the contact is re-added, re-link those orphaned DMs. - Uses raw_json to match by pubkey_prefix. + Matches by pubkey prefix in raw_json (incoming) or contact name (outgoing). """ public_key = public_key.lower() - prefix = public_key[:12] # Short prefix used in pubkey_prefix field + prefix = public_key[:12] with self._connect() as conn: - cursor = conn.execute( - """UPDATE direct_messages SET contact_pubkey = ? - WHERE contact_pubkey IS NULL - AND (raw_json LIKE ? OR raw_json IS NULL)""", - (public_key, f'%{prefix}%') - ) + if name: + cursor = conn.execute( + """UPDATE direct_messages SET contact_pubkey = ? + WHERE contact_pubkey IS NULL + AND (raw_json LIKE ? OR raw_json LIKE ? + OR raw_json IS NULL)""", + (public_key, f'%{prefix}%', f'%"name": "{name}"%') + ) + else: + cursor = conn.execute( + """UPDATE direct_messages SET contact_pubkey = ? + WHERE contact_pubkey IS NULL + AND (raw_json LIKE ? OR raw_json IS NULL)""", + (public_key, f'%{prefix}%') + ) if cursor.rowcount > 0: logger.info(f"Re-linked {cursor.rowcount} orphaned DMs to {public_key[:12]}...") return cursor.rowcount diff --git a/app/device_manager.py b/app/device_manager.py index dda5a9f..4e5d4d2 100644 --- a/app/device_manager.py +++ b/app/device_manager.py @@ -1000,13 +1000,14 @@ class DeviceManager: return self.db.get_contacts() # return cached def delete_contact(self, pubkey: str) -> Dict: - """Delete a contact from device and database.""" + """Delete a contact from device. Keep DB record to preserve DM history.""" if not self.is_connected: return {'success': False, 'error': 'Device not connected'} try: self.execute(self.mc.commands.remove_contact(pubkey)) - self.db.delete_contact(pubkey) + # Don't delete from DB — ON DELETE SET NULL would orphan all DMs. + # Contact stays in DB for historical reference; upsert updates on re-add. # Also remove from in-memory contacts cache if self.mc.contacts and pubkey in self.mc.contacts: del self.mc.contacts[pubkey] @@ -1187,7 +1188,8 @@ class DeviceManager: source='device', ) # Re-link orphaned DMs (from previous ON DELETE SET NULL) - self.db.relink_orphaned_dms(pubkey) + contact_name = contact.get('adv_name', contact.get('name', '')) + self.db.relink_orphaned_dms(pubkey, name=contact_name) # Remove from pending list after successful approval self.mc.pending_contacts.pop(pubkey, None)