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