From d1ce3ceb925efb3690f2f1c8991e618c98516afb Mon Sep 17 00:00:00 2001 From: MarekWo Date: Sat, 7 Mar 2026 07:41:15 +0100 Subject: [PATCH] fix(dm): refresh mc.contacts on approve, DB name fallback, relink orphans Three fixes for DM sending after contact delete/re-add: 1. approve_contact() now calls ensure_contacts() to refresh mc.contacts so send_dm can find newly added contacts immediately 2. cli.send_dm() falls back to DB name lookup when mc.contacts misses, preventing the contact name from being passed as a pubkey string 3. approve_contact() re-links orphaned DMs (NULL contact_pubkey from ON DELETE SET NULL) back to the re-added contact New DB methods: get_contact_by_name(), relink_orphaned_dms() Co-Authored-By: Claude Opus 4.6 --- app/database.py | 29 +++++++++++++++++++++++++++++ app/device_manager.py | 7 +++++++ app/meshcore/cli.py | 16 +++++++++++++--- 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/app/database.py b/app/database.py index d01e676..8958070 100644 --- a/app/database.py +++ b/app/database.py @@ -137,6 +137,15 @@ class Database: ).fetchone() return dict(row) if row else None + def get_contact_by_name(self, name: str) -> Optional[Dict]: + """Find a contact by exact name match.""" + with self._connect() as conn: + row = conn.execute( + "SELECT * FROM contacts WHERE name = ? AND length(public_key) = 64 LIMIT 1", + (name,) + ).fetchone() + return dict(row) if row else None + def delete_contact(self, public_key: str) -> bool: with self._connect() as conn: cursor = conn.execute( @@ -373,6 +382,26 @@ class Database: ).fetchone() return dict(row) if row else None + def relink_orphaned_dms(self, public_key: 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. + """ + public_key = public_key.lower() + prefix = public_key[:12] # Short prefix used in pubkey_prefix field + 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 cursor.rowcount > 0: + logger.info(f"Re-linked {cursor.rowcount} orphaned DMs to {public_key[:12]}...") + return cursor.rowcount + def find_dm_duplicate(self, contact_pubkey: str, content: str, sender_timestamp: int = None, window_seconds: int = 300) -> Optional[Dict]: diff --git a/app/device_manager.py b/app/device_manager.py index adb78d1..dda5a9f 100644 --- a/app/device_manager.py +++ b/app/device_manager.py @@ -1167,6 +1167,10 @@ class DeviceManager: return {'success': False, 'error': 'Contact not in pending list'} self.execute(self.mc.commands.add_contact(contact)) + + # Refresh mc.contacts so send_dm can find the new contact + self.execute(self.mc.ensure_contacts(follow=True)) + last_adv = contact.get('last_advert') last_advert_val = ( str(int(last_adv)) @@ -1182,6 +1186,9 @@ class DeviceManager: last_advert=last_advert_val, source='device', ) + # Re-link orphaned DMs (from previous ON DELETE SET NULL) + self.db.relink_orphaned_dms(pubkey) + # Remove from pending list after successful approval self.mc.pending_contacts.pop(pubkey, None) return {'success': True, 'message': 'Contact approved'} diff --git a/app/meshcore/cli.py b/app/meshcore/cli.py index 3d8ddea..26728d5 100644 --- a/app/meshcore/cli.py +++ b/app/meshcore/cli.py @@ -376,15 +376,25 @@ def send_dm(recipient: str, text: str) -> Tuple[bool, Dict]: try: dm = _get_dm() - # Try to find contact by name first + recipient = recipient.strip() + + # Try to find contact by name in mc.contacts (in-memory) contact = None if dm.mc: - contact = dm.mc.get_contact_by_name(recipient.strip()) + contact = dm.mc.get_contact_by_name(recipient) if contact: pubkey = contact.get('public_key', recipient) + elif len(recipient) >= 12 and all(c in '0123456789abcdef' for c in recipient.lower()): + # Looks like a pubkey/prefix already + pubkey = recipient else: - pubkey = recipient.strip() + # Name not in mc.contacts — try DB lookup + db_contact = dm.db.get_contact_by_name(recipient) + if db_contact: + pubkey = db_contact['public_key'] + else: + pubkey = recipient result = dm.send_dm(pubkey, text.strip()) return result['success'], result