Private
Public Access
1
0

Started working on contact resolution

This commit is contained in:
2025-06-26 16:23:53 -07:00
parent 3b30cb77c8
commit bb19db17cd
14 changed files with 405 additions and 27 deletions

View File

@@ -0,0 +1,229 @@
use super::ContactResolverBackend;
use dbus::blocking::Connection;
use dbus::arg::{RefArg, Variant};
use once_cell::sync::OnceCell;
use std::collections::HashMap;
use std::time::Duration;
pub struct EDSContactResolverBackend;
// Cache the UID of the default local address book so we do not have to scan
// all sources over and over again. Discovering the address book requires a
// D-Bus round-trip that we would rather avoid on every lookup.
static ADDRESS_BOOK_SOURCE_UID: OnceCell<String> = OnceCell::new();
/// Helper that returns a blocking D-Bus session connection. Creating the
/// connection is cheap (<1 ms) but we still keep it around because the
/// underlying socket is re-used by the dbus crate.
fn new_session_connection() -> Result<Connection, dbus::Error> {
Connection::new_session()
}
/// Scan Evolution-Data-Server sources to find a suitable address-book source
/// UID. The implementation mirrors what `gdbus introspect` reveals for the
/// EDS interfaces. We search all `org.gnome.evolution.dataserver.Source`
/// objects and pick the first one that advertises the `[Address Book]` section
/// with a `BackendName=` entry in its INI-style `Data` property.
fn ensure_address_book_uid(conn: &Connection) -> anyhow::Result<String> {
if let Some(uid) = ADDRESS_BOOK_SOURCE_UID.get() {
return Ok(uid.clone());
}
let source_manager_proxy = conn.with_proxy(
"org.gnome.evolution.dataserver.Sources5",
"/org/gnome/evolution/dataserver/SourceManager",
Duration::from_secs(5),
);
// The GetManagedObjects reply is the usual ObjectManager map.
let (managed_objects,): (
HashMap<
dbus::Path<'static>,
HashMap<String, HashMap<String, Variant<Box<dyn RefArg>>>>,
>,
) = source_manager_proxy.method_call(
"org.freedesktop.DBus.ObjectManager",
"GetManagedObjects",
(),
)?;
let uid = managed_objects
.values()
.filter_map(|ifaces| ifaces.get("org.gnome.evolution.dataserver.Source"))
.filter_map(|props| {
let uid = props.get("UID")?.as_str()?;
let data = props.get("Data")?.as_str()?;
if data_contains_address_book_backend(data) {
Some(uid.to_owned())
} else {
None
}
})
.next()
.ok_or_else(|| anyhow::anyhow!("No address book source found"))?;
// Remember for future look-ups.
log::debug!("EDS resolver: found address book source UID: {}", uid);
let _ = ADDRESS_BOOK_SOURCE_UID.set(uid.clone());
Ok(uid)
}
fn data_contains_address_book_backend(data: &str) -> bool {
let mut in_address_book_section = false;
for line in data.lines() {
let trimmed = line.trim();
if trimmed.starts_with('[') && trimmed.ends_with(']') {
in_address_book_section = trimmed == "[Address Book]";
continue;
}
if in_address_book_section && trimmed.starts_with("BackendName=") {
return true;
}
}
false
}
/// Open the Evolution address book referenced by `source_uid` and return the
/// pair `(object_path, bus_name)` that identifies the newly created D-Bus
/// proxy.
fn open_address_book(
conn: &Connection,
source_uid: &str,
) -> anyhow::Result<(String, String)> {
let factory_proxy = conn.with_proxy(
"org.gnome.evolution.dataserver.AddressBook10",
"/org/gnome/evolution/dataserver/AddressBookFactory",
Duration::from_secs(60),
);
let (object_path, bus_name): (String, String) = factory_proxy.method_call(
"org.gnome.evolution.dataserver.AddressBookFactory",
"OpenAddressBook",
(source_uid.to_owned(),),
)?;
Ok((object_path, bus_name))
}
impl ContactResolverBackend for EDSContactResolverBackend {
type ContactID = String;
fn resolve_contact_id(&self, address: &str) -> Option<Self::ContactID> {
// Only email addresses are supported for now. We fall back to NONE on
// any error to keep the resolver infallible for callers.
let conn = match new_session_connection() {
Ok(c) => c,
Err(e) => {
log::debug!("EDS resolver: failed to open session D-Bus: {}", e);
return None;
}
};
let source_uid = match ensure_address_book_uid(&conn) {
Ok(u) => u,
Err(e) => {
log::debug!("EDS resolver: could not determine address-book UID: {}", e);
return None;
}
};
let (object_path, bus_name) = match open_address_book(&conn, &source_uid) {
Ok(v) => v,
Err(e) => {
log::debug!("EDS resolver: failed to open address book: {}", e);
return None;
}
};
let address_book_proxy = conn.with_proxy(bus_name, object_path, Duration::from_secs(60));
let filter = if address.contains('@') {
format!("(is \"email\" \"{}\")", address)
} else {
// Remove country code, if present
let address = address.replace("+", "")
.chars()
.skip_while(|c| c.is_numeric() || *c == '(' || *c == ')')
.collect::<String>();
// Remove any remaining non-numeric characters
let address = address.chars()
.filter(|c| c.is_numeric())
.collect::<String>();
format!("(is \"phone\" \"{}\")", address)
};
let uids_result: Result<(Vec<String>,), _> = address_book_proxy.method_call(
"org.gnome.evolution.dataserver.AddressBook",
"GetContactListUids",
(filter,),
);
let (uids,) = match uids_result {
Ok(v) => v,
Err(e) => {
log::debug!("EDS resolver: GetContactListUids failed: {}", e);
return None;
}
};
uids.into_iter().next()
}
fn get_contact_display_name(&self, contact_id: &Self::ContactID) -> Option<String> {
let conn = match new_session_connection() {
Ok(c) => c,
Err(e) => {
log::debug!("EDS resolver: failed to open session D-Bus: {}", e);
return None;
}
};
let source_uid = match ensure_address_book_uid(&conn) {
Ok(u) => u,
Err(e) => {
log::debug!("EDS resolver: could not determine address-book UID: {}", e);
return None;
}
};
let (object_path, bus_name) = match open_address_book(&conn, &source_uid) {
Ok(v) => v,
Err(e) => {
log::debug!("EDS resolver: failed to open address book: {}", e);
return None;
}
};
let address_book_proxy = conn.with_proxy(bus_name, object_path, Duration::from_secs(60));
let vcard_result: Result<(String,), _> = address_book_proxy.method_call(
"org.gnome.evolution.dataserver.AddressBook",
"GetContact",
(contact_id.clone(),),
);
let (vcard,) = match vcard_result {
Ok(v) => v,
Err(e) => {
log::debug!("EDS resolver: GetContact failed: {}", e);
return None;
}
};
for line in vcard.lines() {
if let Some(rest) = line.strip_prefix("FN:") {
return Some(rest.to_string());
}
}
None
}
}
impl Default for EDSContactResolverBackend {
fn default() -> Self {
Self
}
}

View File

@@ -0,0 +1,46 @@
pub mod eds;
pub use eds::EDSContactResolverBackend;
pub trait ContactResolverBackend {
type ContactID;
fn resolve_contact_id(&self, address: &str) -> Option<Self::ContactID>;
fn get_contact_display_name(&self, contact_id: &Self::ContactID) -> Option<String>;
}
pub type AnyContactID = String;
pub struct ContactResolver<T: ContactResolverBackend> {
backend: T,
}
impl<T: ContactResolverBackend> ContactResolver<T>
where
T::ContactID: From<AnyContactID>,
T::ContactID: Into<AnyContactID>,
T: Default,
{
pub fn new(backend: T) -> Self {
Self { backend }
}
pub fn resolve_contact_id(&self, address: &str) -> Option<AnyContactID> {
self.backend.resolve_contact_id(address).map(|id| id.into())
}
pub fn get_contact_display_name(&self, contact_id: &AnyContactID) -> Option<String> {
let backend_contact_id: T::ContactID = T::ContactID::from((*contact_id).clone());
self.backend.get_contact_display_name(&backend_contact_id)
}
}
impl<T: ContactResolverBackend> Default for ContactResolver<T>
where
T::ContactID: From<AnyContactID>,
T::ContactID: Into<AnyContactID>,
T: Default,
{
fn default() -> Self {
Self::new(T::default())
}
}

View File

@@ -49,6 +49,12 @@ mod attachment_store;
pub use attachment_store::AttachmentStore;
pub use attachment_store::AttachmentStoreEvent;
pub mod contact_resolver;
use contact_resolver::ContactResolver;
use contact_resolver::EDSContactResolverBackend;
use kordophone_db::models::participant::Participant as DbParticipant;
#[derive(Debug, Error)]
pub enum DaemonError {
#[error("Client Not Configured")]
@@ -501,10 +507,34 @@ impl Daemon {
// Insert each conversation
let num_conversations = db_conversations.len();
let contact_resolver = ContactResolver::new(EDSContactResolverBackend::default());
for conversation in db_conversations {
// Insert or update conversation and its participants
database
.with_repository(|r| r.insert_conversation(conversation))
.with_repository(|r| r.insert_conversation(conversation.clone()))
.await?;
// Resolve any new participants via the contact resolver and store their contact_id
log::trace!(target: target::SYNC, "Resolving participants for conversation: {}", conversation.guid);
let guid = conversation.guid.clone();
if let Some(saved) = database
.with_repository(|r| r.get_conversation_by_guid(&guid))
.await?
{
for p in &saved.participants {
if let DbParticipant::Remote { id: Some(pid), display_name, contact_id: None } = p {
log::trace!(target: target::SYNC, "Resolving contact id for participant: {}", display_name);
if let Some(contact) = contact_resolver.resolve_contact_id(display_name) {
log::trace!(target: target::SYNC, "Resolved contact id for participant: {}", contact);
let _ = database
.with_repository(|r| r.update_participant_contact(*pid, &contact))
.await;
} else {
log::trace!(target: target::SYNC, "No contact id found for participant: {}", display_name);
}
}
}
}
}
// Send conversations updated signal

View File

@@ -13,6 +13,7 @@ pub enum Participant {
Remote {
id: Option<i32>,
display_name: String,
contact_id: Option<String>,
},
}
@@ -21,6 +22,7 @@ impl From<String> for Participant {
Participant::Remote {
id: None,
display_name,
contact_id: None,
}
}
}
@@ -30,6 +32,7 @@ impl From<&str> for Participant {
Participant::Remote {
id: None,
display_name: display_name.to_string(),
contact_id: None,
}
}
}
@@ -38,8 +41,8 @@ impl From<kordophone_db::models::Participant> for Participant {
fn from(participant: kordophone_db::models::Participant) -> Self {
match participant {
kordophone_db::models::Participant::Me => Participant::Me,
kordophone_db::models::Participant::Remote { id, display_name } => {
Participant::Remote { id, display_name }
kordophone_db::models::Participant::Remote { id, display_name, contact_id } => {
Participant::Remote { id, display_name, contact_id }
}
}
}
@@ -107,8 +110,8 @@ impl From<Message> for kordophone_db::models::Message {
id: message.id,
sender: match message.sender {
Participant::Me => kordophone_db::models::Participant::Me,
Participant::Remote { id, display_name } => {
kordophone_db::models::Participant::Remote { id, display_name }
Participant::Remote { id, display_name, contact_id } => {
kordophone_db::models::Participant::Remote { id, display_name, contact_id }
}
},
text: message.text,
@@ -145,6 +148,7 @@ impl From<kordophone::model::Message> for Message {
Some(sender) => Participant::Remote {
id: None,
display_name: sender,
contact_id: None,
},
None => Participant::Me,
},

View File

@@ -9,8 +9,11 @@ use crate::daemon::{
settings::Settings,
signals::Signal,
DaemonResult,
contact_resolver::{ContactResolver, EDSContactResolverBackend},
};
use kordophone_db::models::participant::Participant;
use crate::dbus::endpoint::DbusRegistry;
use crate::dbus::interface;
use crate::dbus::interface::signals as DbusSignals;
@@ -167,6 +170,24 @@ impl DBusAgent {
.unwrap()
.map_err(|e| MethodErr::failed(&format!("Daemon error: {}", e)))
}
fn resolve_participant_display_name(&self, participant: &Participant) -> String {
let resolver = ContactResolver::new(EDSContactResolverBackend::default());
match participant {
// Me (we should use a special string here...)
Participant::Me => "(Me)".to_string(),
// Remote participant with a resolved contact_id
Participant::Remote { display_name, contact_id: Some(contact_id), .. } => {
resolver.get_contact_display_name(contact_id).unwrap_or_else(|| display_name.clone())
}
// Remote participant without a resolved contact_id
Participant::Remote { display_name, .. } => {
display_name.clone()
}
}
}
}
//
@@ -207,7 +228,7 @@ impl DbusRepository for DBusAgent {
arg::Variant(Box::new(
conv.participants
.into_iter()
.map(|p| p.display_name())
.map(|p| self.resolve_participant_display_name(&p))
.collect::<Vec<String>>(),
)),
);
@@ -221,6 +242,7 @@ impl DbusRepository for DBusAgent {
})
}
fn sync_conversation_list(&mut self) -> Result<(), MethodErr> {
self.send_event_sync(Event::SyncConversationList)
}