From a77c322f196ba68a98e30b6add78d640be3f1402 Mon Sep 17 00:00:00 2001 From: Edouard-m-333 Date: Tue, 9 Jun 2026 16:26:34 +0200 Subject: [PATCH] feat: populate contacts from message senders automatically MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously `contacts` was only filled from the address book (dialog peers and `contacts.*` lookups). The sender of a message from someone not in the address book — common in groups/supergroups — had no stored name, so output and analytics fell back to `user:`. The name cannot be recovered later from a bare numeric id, because resolving a user requires its access_hash. Every fetched message and every real-time update already carries its sender as a `Peer`, but only the bare id was kept. This upserts the sender into `contacts` wherever a message is processed, at no extra API cost: - daemon: each incoming message upserts its sender (real-time). - sync: the three message-sync paths upsert senders of fetched messages. - chats members: persists every listed participant (incl. silent members). All reuse the existing idempotent Store::upsert_contact (COALESCE merge), so a good value is never overwritten with an empty one. No new dependencies and no schema change. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app/sync.rs | 83 +++++++++++++++++++++++++++++++++++++++++++++-- src/cmd/chats.rs | 13 ++++++++ src/cmd/daemon.rs | 20 ++++++++++++ 3 files changed, 113 insertions(+), 3 deletions(-) diff --git a/src/app/sync.rs b/src/app/sync.rs index 3193422..fa9afe4 100644 --- a/src/app/sync.rs +++ b/src/app/sync.rs @@ -190,6 +190,34 @@ struct FetchedMessage { media_path: Option, reply_to_id: Option, topic_id: Option, + /// Sender identity captured with the message, so `contacts` can be + /// populated at store time without any extra API call. `None` when the + /// sender is not a user (e.g. a channel) or is unknown. + sender_contact: Option, +} + +/// A message sender's identity, lifted from the `Peer` that already rides +/// along with every fetched message. Used to upsert `contacts` for free. +struct SenderContact { + user_id: i64, + username: Option, + first_name: String, + last_name: String, + phone: String, +} + +/// Extract a [`SenderContact`] from a message's sender peer, if it is a user. +fn sender_contact_from_peer(sender: Option<&Peer>) -> Option { + match sender { + Some(Peer::User(u)) => Some(SenderContact { + user_id: u.bare_id(), + username: u.username().map(|s| s.to_string()), + first_name: u.first_name().unwrap_or("").to_string(), + last_name: u.last_name().unwrap_or("").to_string(), + phone: u.phone().unwrap_or("").to_string(), + }), + _ => None, + } } /// Output representation of a synced message (used for Text/Json/Stream modes) @@ -867,7 +895,10 @@ impl App { latest_ts = Some(msg_ts); } - let sender_id = msg.sender().map(|s| s.id().bare_id()).unwrap_or(0); + let sender = msg.sender(); + let sender_id = + sender.as_ref().map(|s| s.id().bare_id()).unwrap_or(0); + let sender_contact = sender_contact_from_peer(sender); let from_me = msg.outgoing(); let text = msg.text().to_string(); let reply_to_id = msg.reply_to_message_id().map(|id| id as i64); @@ -904,6 +935,7 @@ impl App { media_path, reply_to_id, topic_id, + sender_contact, }; // Stream output immediately (before collecting all results) @@ -1024,6 +1056,21 @@ impl App { topic_id: msg.topic_id, }) .await?; + // Populate contacts from the sender captured with the message + // (no extra API call — it rode along with the message history). + if let Some(c) = &msg.sender_contact { + let _ = self + .get_store() + .await? + .upsert_contact( + c.user_id, + c.username.as_deref(), + &c.first_name, + &c.last_name, + &c.phone, + ) + .await; + } messages_stored += 1; } @@ -1322,7 +1369,8 @@ impl App { latest_ts = Some(msg_ts); } - let sender_id = msg.sender().map(|s| s.id().bare_id()).unwrap_or(0); + let sender = msg.sender(); + let sender_id = sender.as_ref().map(|s| s.id().bare_id()).unwrap_or(0); let from_me = msg.outgoing(); let text = msg.text().to_string(); @@ -1364,6 +1412,20 @@ impl App { topic_id, }) .await?; + // Populate contacts from the message sender (free — no API call). + if let Some(c) = sender_contact_from_peer(sender) { + let _ = self + .get_store() + .await? + .upsert_contact( + c.user_id, + c.username.as_deref(), + &c.first_name, + &c.last_name, + &c.phone, + ) + .await; + } messages_stored += 1; // Show progress periodically @@ -1648,7 +1710,8 @@ impl App { latest_ts = Some(msg_ts); } - let sender_id = msg.sender().map(|s| s.id().bare_id()).unwrap_or(0); + let sender = msg.sender(); + let sender_id = sender.as_ref().map(|s| s.id().bare_id()).unwrap_or(0); let from_me = msg.outgoing(); let text = msg.text().to_string(); @@ -1687,6 +1750,20 @@ impl App { topic_id, }) .await?; + // Populate contacts from the message sender (free — no API call). + if let Some(c) = sender_contact_from_peer(sender) { + let _ = self + .get_store() + .await? + .upsert_contact( + c.user_id, + c.username.as_deref(), + &c.first_name, + &c.last_name, + &c.phone, + ) + .await; + } messages_stored += 1; // Show progress periodically diff --git a/src/cmd/chats.rs b/src/cmd/chats.rs index 154138e..6e133a2 100644 --- a/src/cmd/chats.rs +++ b/src/cmd/chats.rs @@ -413,6 +413,19 @@ pub async fn run(cli: &Cli, cmd: &ChatsCommand) -> Result<()> { role: format_role(&participant.role), }; members.push(member); + // Persist each participant into contacts so message senders in + // this chat resolve to names (covers silent members too). + let _ = app + .get_store() + .await? + .upsert_contact( + user.bare_id(), + user.username(), + user.first_name().unwrap_or(""), + user.last_name().unwrap_or(""), + user.phone().unwrap_or(""), + ) + .await; count += 1; // Check limit (0 = unlimited) diff --git a/src/cmd/daemon.rs b/src/cmd/daemon.rs index 343891e..8336f3b 100644 --- a/src/cmd/daemon.rs +++ b/src/cmd/daemon.rs @@ -334,6 +334,26 @@ pub async fn run(cli: &Cli, args: &DaemonArgs) -> Result<()> { messages_stored.fetch_add(1, Ordering::Relaxed); } + // Populate contacts from the sender in real time so + // names resolve. Costs nothing — the sender object + // already arrived with the update. + if let Some(Peer::User(user)) = msg.sender() { + if let Err(e) = app + .get_store() + .await? + .upsert_contact( + user.bare_id(), + user.username(), + user.first_name().unwrap_or(""), + user.last_name().unwrap_or(""), + user.phone().unwrap_or(""), + ) + .await + { + log::warn!("Failed to upsert sender contact: {}", e); + } + } + // Update chat metadata let chat_name = chat_name_from_peer(&peer); let username = username_from_peer(&peer);