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 <noreply@anthropic.com>
This commit is contained in:
MarekWo
2026-03-07 07:41:15 +01:00
parent 1a3a1e937c
commit d1ce3ceb92
3 changed files with 49 additions and 3 deletions

View File

@@ -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]:

View File

@@ -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'}

View File

@@ -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