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 <noreply@anthropic.com>
This commit is contained in:
MarekWo
2026-03-07 08:09:24 +01:00
parent d1ce3ceb92
commit 66fa261151
2 changed files with 24 additions and 12 deletions
+19 -9
View File
@@ -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
+5 -3
View File
@@ -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)