Private
Public Access
1
0

Don't overwrite already resolved participants, better naming of keys

This commit is contained in:
2025-06-26 18:23:15 -07:00
parent bb19db17cd
commit f6bb1a9b57
25 changed files with 263 additions and 306 deletions

View File

@@ -1,34 +0,0 @@
-- Your SQL goes here
CREATE TABLE `conversation_participants`(
`conversation_id` TEXT NOT NULL,
`participant_id` INTEGER NOT NULL,
PRIMARY KEY(`conversation_id`, `participant_id`)
);
CREATE TABLE `messages`(
`id` TEXT NOT NULL PRIMARY KEY,
`text` TEXT NOT NULL,
`sender_participant_id` INTEGER,
`date` TIMESTAMP NOT NULL
);
CREATE TABLE `conversation_messages`(
`conversation_id` TEXT NOT NULL,
`message_id` TEXT NOT NULL,
PRIMARY KEY(`conversation_id`, `message_id`)
);
CREATE TABLE `participants`(
`id` INTEGER NOT NULL PRIMARY KEY,
`display_name` TEXT,
`is_me` BOOL NOT NULL
);
CREATE TABLE `conversations`(
`id` TEXT NOT NULL PRIMARY KEY,
`unread_count` BIGINT NOT NULL,
`display_name` TEXT,
`last_message_preview` TEXT,
`date` TIMESTAMP NOT NULL
);

View File

@@ -1,7 +0,0 @@
-- This file should undo anything in `up.sql`
DROP TABLE IF EXISTS `settings`;

View File

@@ -1,11 +0,0 @@
-- Your SQL goes here
CREATE TABLE `settings`(
`key` TEXT NOT NULL PRIMARY KEY,
`value` BINARY NOT NULL
);

View File

@@ -1,2 +0,0 @@
-- Remove attachment_metadata column from messages table
ALTER TABLE messages DROP COLUMN attachment_metadata;

View File

@@ -1,2 +0,0 @@
-- Add attachment_metadata column to messages table
ALTER TABLE messages ADD COLUMN attachment_metadata TEXT;

View File

@@ -1,2 +0,0 @@
-- Remove file_transfer_guids column from messages table
ALTER TABLE messages DROP COLUMN file_transfer_guids;

View File

@@ -1,2 +0,0 @@
-- Add file_transfer_guids column to messages table
ALTER TABLE messages ADD COLUMN file_transfer_guids TEXT;

View File

@@ -1,14 +0,0 @@
-- Revert participants table to remove contact_id column
-- SQLite does not support DROP COLUMN directly, so we recreate the table without contact_id
PRAGMA foreign_keys=off;
CREATE TABLE participants_backup (
id INTEGER NOT NULL PRIMARY KEY,
display_name TEXT,
is_me BOOL NOT NULL
);
INSERT INTO participants_backup (id, display_name, is_me)
SELECT id, display_name, is_me
FROM participants;
DROP TABLE participants;
ALTER TABLE participants_backup RENAME TO participants;
PRAGMA foreign_keys=on;

View File

@@ -1,2 +0,0 @@
-- Add contact_id column to participants to store an external contact identifier (e.g., Folks ID)
ALTER TABLE participants ADD COLUMN contact_id TEXT;

View File

@@ -1,6 +1,7 @@
-- This file should undo anything in `up.sql` -- This file should undo anything in `up.sql`
DROP TABLE IF EXISTS `conversation_participants`;
DROP TABLE IF EXISTS `messages`; DROP TABLE IF EXISTS `messages`;
DROP TABLE IF EXISTS `conversation_messages`; DROP TABLE IF EXISTS `conversation_messages`;
DROP TABLE IF EXISTS `participants`; DROP TABLE IF EXISTS `settings`;
DROP TABLE IF EXISTS `conversations`; DROP TABLE IF EXISTS `conversations`;
DROP TABLE IF EXISTS `participants`;
DROP TABLE IF EXISTS `conversation_participants`;

View File

@@ -0,0 +1,46 @@
-- Your SQL goes here
CREATE TABLE `messages`(
`id` TEXT NOT NULL PRIMARY KEY,
`text` TEXT NOT NULL,
`sender_participant_handle` TEXT,
`date` TIMESTAMP NOT NULL,
`file_transfer_guids` TEXT,
`attachment_metadata` TEXT,
FOREIGN KEY (`sender_participant_handle`) REFERENCES `participants`(`handle`)
);
CREATE TABLE `conversation_messages`(
`conversation_id` TEXT NOT NULL,
`message_id` TEXT NOT NULL,
PRIMARY KEY(`conversation_id`, `message_id`),
FOREIGN KEY (`conversation_id`) REFERENCES `conversations`(`id`),
FOREIGN KEY (`message_id`) REFERENCES `messages`(`id`)
);
CREATE TABLE `settings`(
`key` TEXT NOT NULL PRIMARY KEY,
`value` BINARY NOT NULL
);
CREATE TABLE `conversations`(
`id` TEXT NOT NULL PRIMARY KEY,
`unread_count` BIGINT NOT NULL,
`display_name` TEXT,
`last_message_preview` TEXT,
`date` TIMESTAMP NOT NULL
);
CREATE TABLE `participants`(
`handle` TEXT NOT NULL PRIMARY KEY,
`is_me` BOOL NOT NULL,
`contact_id` TEXT
);
CREATE TABLE `conversation_participants`(
`conversation_id` TEXT NOT NULL,
`participant_handle` TEXT NOT NULL,
PRIMARY KEY(`conversation_id`, `participant_handle`),
FOREIGN KEY (`conversation_id`) REFERENCES `conversations`(`id`),
FOREIGN KEY (`participant_handle`) REFERENCES `participants`(`handle`)
);

View File

@@ -75,7 +75,7 @@ impl From<kordophone::model::Conversation> for Conversation {
participants: value participants: value
.participant_display_names .participant_display_names
.into_iter() .into_iter()
.map(|p| p.into()) .map(|p| Participant::Remote { handle: p, contact_id: None }) // todo: this is wrong
.collect(), .collect(),
} }
} }

View File

@@ -7,7 +7,7 @@ use diesel::prelude::*;
#[diesel(check_for_backend(diesel::sqlite::Sqlite))] #[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct Record { pub struct Record {
pub id: String, pub id: String,
pub sender_participant_id: Option<i32>, pub sender_participant_handle: Option<String>,
pub text: String, pub text: String,
pub date: NaiveDateTime, pub date: NaiveDateTime,
pub file_transfer_guids: Option<String>, // JSON array pub file_transfer_guids: Option<String>, // JSON array
@@ -28,9 +28,9 @@ impl From<Message> for Record {
Self { Self {
id: message.id, id: message.id,
sender_participant_id: match message.sender { sender_participant_handle: match message.sender {
Participant::Me => None, Participant::Me => None,
Participant::Remote { id, .. } => id, Participant::Remote { handle, .. } => Some(handle),
}, },
text: message.text, text: message.text,
date: message.date, date: message.date,
@@ -51,10 +51,13 @@ impl From<Record> for Message {
.attachment_metadata .attachment_metadata
.and_then(|json| serde_json::from_str(&json).ok()); .and_then(|json| serde_json::from_str(&json).ok());
let message_sender = match record.sender_participant_handle {
Some(handle) => Participant::Remote { handle, contact_id: None },
None => Participant::Me,
};
Self { Self {
id: record.id, id: record.id,
// We'll set the proper sender later when loading participant info sender: message_sender,
sender: Participant::Me,
text: record.text, text: record.text,
date: record.date, date: record.date,
file_transfer_guids, file_transfer_guids,

View File

@@ -4,9 +4,9 @@ use diesel::prelude::*;
#[derive(Queryable, Selectable, AsChangeset, Identifiable)] #[derive(Queryable, Selectable, AsChangeset, Identifiable)]
#[diesel(table_name = crate::schema::participants)] #[diesel(table_name = crate::schema::participants)]
#[diesel(primary_key(handle))]
pub struct Record { pub struct Record {
pub id: i32, pub handle: String,
pub display_name: Option<String>,
pub is_me: bool, pub is_me: bool,
pub contact_id: Option<String>, pub contact_id: Option<String>,
} }
@@ -14,7 +14,7 @@ pub struct Record {
#[derive(Insertable)] #[derive(Insertable)]
#[diesel(table_name = crate::schema::participants)] #[diesel(table_name = crate::schema::participants)]
pub struct InsertableRecord { pub struct InsertableRecord {
pub display_name: Option<String>, pub handle: String,
pub is_me: bool, pub is_me: bool,
pub contact_id: Option<String>, pub contact_id: Option<String>,
} }
@@ -23,12 +23,12 @@ impl From<Participant> for InsertableRecord {
fn from(participant: Participant) -> Self { fn from(participant: Participant) -> Self {
match participant { match participant {
Participant::Me => InsertableRecord { Participant::Me => InsertableRecord {
display_name: None, handle: "me".to_string(),
is_me: true, is_me: true,
contact_id: None, contact_id: None,
}, },
Participant::Remote { display_name, contact_id, .. } => InsertableRecord { Participant::Remote { handle, contact_id, .. } => InsertableRecord {
display_name: Some(display_name), handle,
is_me: false, is_me: false,
contact_id, contact_id,
}, },
@@ -38,12 +38,12 @@ impl From<Participant> for InsertableRecord {
#[derive(Identifiable, Selectable, Queryable, Associations, Debug)] #[derive(Identifiable, Selectable, Queryable, Associations, Debug)]
#[diesel(belongs_to(super::conversation::Record, foreign_key = conversation_id))] #[diesel(belongs_to(super::conversation::Record, foreign_key = conversation_id))]
#[diesel(belongs_to(Record, foreign_key = participant_id))] #[diesel(belongs_to(Record, foreign_key = participant_handle))]
#[diesel(table_name = conversation_participants)] #[diesel(table_name = conversation_participants)]
#[diesel(primary_key(conversation_id, participant_id))] #[diesel(primary_key(conversation_id, participant_handle))]
pub struct ConversationParticipant { pub struct ConversationParticipant {
pub conversation_id: String, pub conversation_id: String,
pub participant_id: i32, pub participant_handle: String,
} }
impl From<Record> for Participant { impl From<Record> for Participant {
@@ -52,8 +52,7 @@ impl From<Record> for Participant {
Participant::Me Participant::Me
} else { } else {
Participant::Remote { Participant::Remote {
id: Some(record.id), handle: record.handle.clone(),
display_name: record.display_name.unwrap_or_default(),
contact_id: record.contact_id, contact_id: record.contact_id,
} }
} }
@@ -64,14 +63,12 @@ impl From<Participant> for Record {
fn from(participant: Participant) -> Self { fn from(participant: Participant) -> Self {
match participant { match participant {
Participant::Me => Record { Participant::Me => Record {
id: 0, // This will be set by the database handle: "me".to_string(),
display_name: None,
is_me: true, is_me: true,
contact_id: None, contact_id: None,
}, },
Participant::Remote { display_name, contact_id, .. } => Record { Participant::Remote { handle, contact_id, .. } => Record {
id: 0, // This will be set by the database handle,
display_name: Some(display_name),
is_me: false, is_me: false,
contact_id, contact_id,
}, },

View File

@@ -27,8 +27,7 @@ impl From<kordophone::model::Message> for Message {
id: value.guid, id: value.guid,
sender: match value.sender { sender: match value.sender {
Some(sender) => Participant::Remote { Some(sender) => Participant::Remote {
id: None, handle: sender,
display_name: sender,
contact_id: None, contact_id: None,
}, },
None => Participant::Me, None => Participant::Me,

View File

@@ -5,4 +5,4 @@ pub mod participant;
pub use conversation::Conversation; pub use conversation::Conversation;
pub use message::Message; pub use message::Message;
pub use participant::Participant; pub use participant::Participant;

View File

@@ -2,37 +2,21 @@
pub enum Participant { pub enum Participant {
Me, Me,
Remote { Remote {
id: Option<i32>, handle: String,
display_name: String,
contact_id: Option<String>, contact_id: Option<String>,
}, },
} }
impl From<String> for Participant {
fn from(display_name: String) -> Self {
Participant::Remote {
id: None,
display_name,
contact_id: None,
}
}
}
impl From<&str> for Participant {
fn from(display_name: &str) -> Self {
Participant::Remote {
id: None,
display_name: display_name.to_string(),
contact_id: None,
}
}
}
impl Participant { impl Participant {
pub fn display_name(&self) -> String { pub fn handle(&self) -> String {
match self { match self {
Participant::Me => "(Me)".to_string(), Participant::Me => "(Me)".to_string(),
Participant::Remote { display_name, .. } => display_name.clone(), Participant::Remote { handle, .. } => handle.clone(),
} }
} }
// Temporary alias for backward compatibility
pub fn display_name(&self) -> String {
self.handle()
}
} }

View File

@@ -36,21 +36,19 @@ impl<'a> Repository<'a> {
.values(&db_conversation) .values(&db_conversation)
.execute(self.connection)?; .execute(self.connection)?;
diesel::replace_into(participants) for participant in &db_participants {
.values(&db_participants) diesel::insert_into(participants)
.execute(self.connection)?; .values(participant)
.on_conflict_do_nothing()
.execute(self.connection)?;
}
// Sqlite backend doesn't support batch insert, so we have to do this manually // Sqlite backend doesn't support batch insert, so we have to do this manually
for participant in db_participants { for participant in &db_participants {
let pid = participants
.select(schema::participants::id)
.filter(schema::participants::display_name.eq(&participant.display_name))
.first::<i32>(self.connection)?;
diesel::replace_into(conversation_participants) diesel::replace_into(conversation_participants)
.values(( .values((
conversation_id.eq(&db_conversation.id), conversation_id.eq(&db_conversation.id),
participant_id.eq(pid), participant_handle.eq(&participant.handle),
)) ))
.execute(self.connection)?; .execute(self.connection)?;
} }
@@ -117,7 +115,7 @@ impl<'a> Repository<'a> {
// Handle participant if message has a remote sender // Handle participant if message has a remote sender
let sender = message.sender.clone(); let sender = message.sender.clone();
let mut db_message: MessageRecord = message.into(); let mut db_message: MessageRecord = message.into();
db_message.sender_participant_id = self.get_or_create_participant(&sender); db_message.sender_participant_handle = self.get_or_create_participant(&sender);
diesel::replace_into(messages) diesel::replace_into(messages)
.values(&db_message) .values(&db_message)
@@ -161,11 +159,11 @@ impl<'a> Repository<'a> {
// individual queries. // individual queries.
self.connection self.connection
.transaction::<_, diesel::result::Error, _>(|conn| { .transaction::<_, diesel::result::Error, _>(|conn| {
// Cache participant ids we have already looked up / created in a // Cache participant handles we have already looked up / created in a
// typical conversation we only have a handful of participants, but we // typical conversation we only have a handful of participants, but we
// might be processing hundreds of messages. Avoiding an extra SELECT per // might be processing hundreds of messages. Avoiding an extra SELECT per
// message saves a lot of round-trips to SQLite. // message saves a lot of round-trips to SQLite.
let mut participant_cache: HashMap<String, i32> = HashMap::new(); let mut participant_cache: HashMap<String, String> = HashMap::new();
// Prepare collections for the batch inserts. // Prepare collections for the batch inserts.
let mut db_messages: Vec<MessageRecord> = Vec::with_capacity(in_messages.len()); let mut db_messages: Vec<MessageRecord> = Vec::with_capacity(in_messages.len());
@@ -174,50 +172,40 @@ impl<'a> Repository<'a> {
for message in in_messages { for message in in_messages {
// Resolve/insert the sender participant only once per display name. // Resolve/insert the sender participant only once per display name.
let sender_id = match &message.sender { let sender_handle_opt = match &message.sender {
Participant::Me => None, Participant::Me => None,
Participant::Remote { display_name, .. } => { Participant::Remote { handle, contact_id } => {
if let Some(cached_participant_id) = participant_cache.get(display_name) if participant_cache.contains_key(handle) {
{ Some(handle.clone())
Some(*cached_participant_id)
} else { } else {
// Try to load from DB first // Ensure participant exists in DB
let existing: Option<i32> = participants_dsl::participants let exists: Option<String> = participants_dsl::participants
.filter(participants_dsl::display_name.eq(display_name)) .filter(participants_dsl::handle.eq(handle))
.select(participants_dsl::id) .select(participants_dsl::handle)
.first::<i32>(conn) .first::<String>(conn)
.optional()?; .optional()?;
let participant_id = if let Some(pid) = existing { if exists.is_none() {
pid
} else {
let new_participant = InsertableParticipantRecord { let new_participant = InsertableParticipantRecord {
display_name: Some(display_name.clone()), handle: handle.clone(),
is_me: false, is_me: false,
contact_id: None, contact_id: contact_id.clone(),
}; };
diesel::insert_into(participants_dsl::participants) diesel::insert_into(participants_dsl::participants)
.values(&new_participant) .values(&new_participant)
.execute(conn)?; .execute(conn)?;
}
// last_insert_rowid() is connection-wide, but we are the only participant_cache.insert(handle.clone(), handle.clone());
// writer inside this transaction. Some(handle.clone())
diesel::select(diesel::dsl::sql::<diesel::sql_types::Integer>(
"last_insert_rowid()",
))
.get_result::<i32>(conn)?
};
participant_cache.insert(display_name.clone(), participant_id);
Some(participant_id)
} }
} }
}; };
// Convert the message into its DB form. // Convert the message into its DB form.
let mut db_message: MessageRecord = message.into(); let mut db_message: MessageRecord = message.into();
db_message.sender_participant_id = sender_id; db_message.sender_participant_handle = sender_handle_opt.clone();
conv_msg_records.push(InsertableConversationMessage { conv_msg_records.push(InsertableConversationMessage {
conversation_id: conversation_guid.to_string(), conversation_id: conversation_guid.to_string(),
@@ -280,10 +268,10 @@ impl<'a> Repository<'a> {
for message_record in message_records { for message_record in message_records {
let mut message: Message = message_record.clone().into(); let mut message: Message = message_record.clone().into();
// If there's a sender_participant_id, load the participant info // If the message references a sender participant, load the participant info
if let Some(pid) = message_record.sender_participant_id { if let Some(sender_handle) = message_record.sender_participant_handle {
let participant = participants let participant = participants
.find(pid) .find(sender_handle)
.first::<ParticipantRecord>(self.connection)?; .first::<ParticipantRecord>(self.connection)?;
message.sender = participant.into(); message.sender = participant.into();
} }
@@ -374,50 +362,44 @@ impl<'a> Repository<'a> {
/// Update the contact_id for an existing participant record. /// Update the contact_id for an existing participant record.
pub fn update_participant_contact( pub fn update_participant_contact(
&mut self, &mut self,
participant_db_id: i32, participant_handle: &str,
new_contact_id: &str, new_contact_id: &str,
) -> Result<()> { ) -> Result<()> {
use crate::schema::participants::dsl::*; use crate::schema::participants::dsl::*;
log::debug!(target: target::REPOSITORY, "Updating participant contact id {} => {}", participant_db_id, new_contact_id); log::debug!(target: target::REPOSITORY, "Updating participant contact {} => {}", participant_handle, new_contact_id);
diesel::update(participants.filter(id.eq(participant_db_id))) diesel::update(participants.filter(handle.eq(participant_handle)))
.set(contact_id.eq(Some(new_contact_id.to_string()))) .set(contact_id.eq(Some(new_contact_id.to_string())))
.execute(self.connection)?; .execute(self.connection)?;
Ok(()) Ok(())
} }
fn get_or_create_participant(&mut self, participant: &Participant) -> Option<i32> { fn get_or_create_participant(&mut self, participant: &Participant) -> Option<String> {
match participant { match participant {
Participant::Me => None, Participant::Me => None,
Participant::Remote { Participant::Remote { handle: p_handle, contact_id: c_id, .. } => {
display_name: p_name,
contact_id: c_id,
..
} => {
use crate::schema::participants::dsl::*; use crate::schema::participants::dsl::*;
let existing_participant = participants let existing_participant = participants
.filter(display_name.eq(p_name)) .filter(handle.eq(p_handle))
.first::<ParticipantRecord>(self.connection) .first::<ParticipantRecord>(self.connection)
.optional() .optional()
.unwrap(); .unwrap();
if let Some(participant) = existing_participant { if existing_participant.is_none() {
return Some(participant.id); let participant_record = InsertableParticipantRecord {
handle: p_handle.clone(),
is_me: false,
contact_id: c_id.clone(),
};
diesel::insert_into(participants)
.values(&participant_record)
.execute(self.connection)
.unwrap();
} }
let participant_record = InsertableParticipantRecord { Some(p_handle.clone())
display_name: Some(participant.display_name()),
is_me: false,
contact_id: c_id.clone(),
};
diesel::insert_into(participants)
.values(&participant_record)
.execute(self.connection)
.unwrap();
self.last_insert_id().ok()
} }
} }
} }

View File

@@ -12,18 +12,17 @@ diesel::table! {
} }
diesel::table! { diesel::table! {
participants (id) { participants (handle) {
id -> Integer, handle -> Text,
display_name -> Nullable<Text>,
is_me -> Bool, is_me -> Bool,
contact_id -> Nullable<Text>, contact_id -> Nullable<Text>,
} }
} }
diesel::table! { diesel::table! {
conversation_participants (conversation_id, participant_id) { conversation_participants (conversation_id, participant_handle) {
conversation_id -> Text, conversation_id -> Text,
participant_id -> Integer, participant_handle -> Text,
} }
} }
@@ -31,7 +30,7 @@ diesel::table! {
messages (id) { messages (id) {
id -> Text, // guid id -> Text, // guid
text -> Text, text -> Text,
sender_participant_id -> Nullable<Integer>, sender_participant_handle -> Nullable<Text>,
date -> Timestamp, date -> Timestamp,
file_transfer_guids -> Nullable<Text>, // JSON array of file transfer GUIDs file_transfer_guids -> Nullable<Text>, // JSON array of file transfer GUIDs
attachment_metadata -> Nullable<Text>, // JSON string of attachment metadata attachment_metadata -> Nullable<Text>, // JSON string of attachment metadata
@@ -53,13 +52,15 @@ diesel::table! {
} }
diesel::joinable!(conversation_participants -> conversations (conversation_id)); diesel::joinable!(conversation_participants -> conversations (conversation_id));
diesel::joinable!(conversation_participants -> participants (participant_id)); diesel::joinable!(conversation_participants -> participants (participant_handle));
diesel::joinable!(messages -> participants (sender_participant_handle));
diesel::joinable!(conversation_messages -> conversations (conversation_id));
diesel::joinable!(conversation_messages -> messages (message_id));
diesel::allow_tables_to_appear_in_same_query!( diesel::allow_tables_to_appear_in_same_query!(
conversations, conversations,
participants, participants,
conversation_participants conversation_participants,
messages,
conversation_messages,
settings,
); );
diesel::joinable!(conversation_messages -> conversations (conversation_id));
diesel::joinable!(conversation_messages -> messages (message_id));
diesel::allow_tables_to_appear_in_same_query!(conversations, messages, conversation_messages);

View File

@@ -12,14 +12,8 @@ fn participants_equal_ignoring_id(a: &Participant, b: &Participant) -> bool {
match (a, b) { match (a, b) {
(Participant::Me, Participant::Me) => true, (Participant::Me, Participant::Me) => true,
( (
Participant::Remote { Participant::Remote { handle: name_a, .. },
display_name: name_a, Participant::Remote { handle: name_b, .. },
..
},
Participant::Remote {
display_name: name_b,
..
},
) => name_a == name_b, ) => name_a == name_b,
_ => false, _ => false,
} }
@@ -29,9 +23,14 @@ fn participants_vec_equal_ignoring_id(a: &[Participant], b: &[Participant]) -> b
if a.len() != b.len() { if a.len() != b.len() {
return false; return false;
} }
a.iter() // For each participant in a, check if there is a matching participant in b
.zip(b.iter()) a.iter().all(|a_participant| {
.all(|(a, b)| participants_equal_ignoring_id(a, b)) b.iter().any(|b_participant| participants_equal_ignoring_id(a_participant, b_participant))
}) &&
// Also check the reverse to ensure no duplicates
b.iter().all(|b_participant| {
a.iter().any(|a_participant| participants_equal_ignoring_id(b_participant, a_participant))
})
} }
#[tokio::test] #[tokio::test]
@@ -214,8 +213,8 @@ async fn test_messages() {
// Verify second message (from Alice) // Verify second message (from Alice)
let retrieved_message2 = messages.iter().find(|m| m.id == message2.id).unwrap(); let retrieved_message2 = messages.iter().find(|m| m.id == message2.id).unwrap();
assert_eq!(retrieved_message2.text, "Hi there!"); assert_eq!(retrieved_message2.text, "Hi there!");
if let Participant::Remote { display_name, .. } = &retrieved_message2.sender { if let Participant::Remote { handle, .. } = &retrieved_message2.sender {
assert_eq!(display_name, "Alice"); assert_eq!(handle, "Alice");
} else { } else {
panic!( panic!(
"Expected Remote participant. Got: {:?}", "Expected Remote participant. Got: {:?}",
@@ -345,14 +344,8 @@ async fn test_insert_messages_batch() {
match (&original.sender, &retrieved.sender) { match (&original.sender, &retrieved.sender) {
(Participant::Me, Participant::Me) => {} (Participant::Me, Participant::Me) => {}
( (
Participant::Remote { Participant::Remote { handle: o_name, .. },
display_name: o_name, Participant::Remote { handle: r_name, .. },
..
},
Participant::Remote {
display_name: r_name,
..
},
) => assert_eq!(o_name, r_name), ) => assert_eq!(o_name, r_name),
_ => panic!( _ => panic!(
"Sender mismatch: original {:?}, retrieved {:?}", "Sender mismatch: original {:?}, retrieved {:?}",

View File

@@ -4,7 +4,10 @@ use dbus::arg::{RefArg, Variant};
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use std::collections::HashMap; use std::collections::HashMap;
use std::time::Duration; use std::time::Duration;
use std::sync::Mutex;
use std::thread;
#[derive(Clone)]
pub struct EDSContactResolverBackend; pub struct EDSContactResolverBackend;
// Cache the UID of the default local address book so we do not have to scan // Cache the UID of the default local address book so we do not have to scan
@@ -12,6 +15,42 @@ pub struct EDSContactResolverBackend;
// D-Bus round-trip that we would rather avoid on every lookup. // D-Bus round-trip that we would rather avoid on every lookup.
static ADDRESS_BOOK_SOURCE_UID: OnceCell<String> = OnceCell::new(); static ADDRESS_BOOK_SOURCE_UID: OnceCell<String> = OnceCell::new();
/// Holds a D-Bus connection and the identifiers needed to create an address-book proxy.
struct AddressBookHandle {
connection: Connection,
object_path: String,
bus_name: String,
}
impl AddressBookHandle {
fn new() -> anyhow::Result<Self> {
let connection = new_session_connection()?;
let source_uid = ensure_address_book_uid(&connection)?;
let (object_path, bus_name) = open_address_book(&connection, &source_uid)?;
Ok(Self {
connection,
object_path,
bus_name,
})
}
}
/// Obtain the global address-book handle, initialising it on the first call.
static ADDRESS_BOOK_HANDLE: OnceCell<Mutex<AddressBookHandle>> = OnceCell::new();
fn get_address_book_handle() -> Option<&'static Mutex<AddressBookHandle>> {
ADDRESS_BOOK_HANDLE
.get_or_try_init(|| AddressBookHandle::new().map(Mutex::new))
.map_err(|e| {
log::debug!(
"EDS resolver: failed to initialise address book handle: {}",
e
);
})
.ok()
}
/// Helper that returns a blocking D-Bus session connection. Creating the /// Helper that returns a blocking D-Bus session connection. Creating the
/// connection is cheap (<1 ms) but we still keep it around because the /// connection is cheap (<1 ms) but we still keep it around because the
/// underlying socket is re-used by the dbus crate. /// underlying socket is re-used by the dbus crate.
@@ -109,51 +148,41 @@ impl ContactResolverBackend for EDSContactResolverBackend {
type ContactID = String; type ContactID = String;
fn resolve_contact_id(&self, address: &str) -> Option<Self::ContactID> { fn resolve_contact_id(&self, address: &str) -> Option<Self::ContactID> {
// Only email addresses are supported for now. We fall back to NONE on let handle_mutex = match get_address_book_handle() {
// any error to keep the resolver infallible for callers. Some(h) => h,
let conn = match new_session_connection() { None => return None,
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) { let handle = handle_mutex.lock().unwrap();
Ok(u) => u, let address_book_proxy = handle.connection.with_proxy(
Err(e) => { &handle.bus_name,
log::debug!("EDS resolver: could not determine address-book UID: {}", e); &handle.object_path,
return None; Duration::from_secs(60),
} );
};
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('@') { let filter = if address.contains('@') {
format!("(is \"email\" \"{}\")", address) format!("(is \"email\" \"{}\")", address)
} else { } else {
// Remove country code, if present let normalized_address = address
let address = address.replace("+", "") .replace('+', "")
.chars() .chars()
.skip_while(|c| c.is_numeric() || *c == '(' || *c == ')') .skip_while(|c| c.is_numeric() || *c == '(' || *c == ')')
.collect::<String>(); .collect::<String>()
.chars()
// Remove any remaining non-numeric characters
let address = address.chars()
.filter(|c| c.is_numeric()) .filter(|c| c.is_numeric())
.collect::<String>(); .collect::<String>();
format!(
format!("(is \"phone\" \"{}\")", address) "(or (is \"phone\" \"{}\") (is \"phone\" \"{}\") )",
address, normalized_address
)
}; };
log::trace!(
"EDS resolver: GetContactListUids filter: {}, address: {}",
filter,
address
);
let uids_result: Result<(Vec<String>,), _> = address_book_proxy.method_call( let uids_result: Result<(Vec<String>,), _> = address_book_proxy.method_call(
"org.gnome.evolution.dataserver.AddressBook", "org.gnome.evolution.dataserver.AddressBook",
"GetContactListUids", "GetContactListUids",
@@ -172,31 +201,17 @@ impl ContactResolverBackend for EDSContactResolverBackend {
} }
fn get_contact_display_name(&self, contact_id: &Self::ContactID) -> Option<String> { fn get_contact_display_name(&self, contact_id: &Self::ContactID) -> Option<String> {
let conn = match new_session_connection() { let handle_mutex = match get_address_book_handle() {
Ok(c) => c, Some(h) => h,
Err(e) => { None => return None,
log::debug!("EDS resolver: failed to open session D-Bus: {}", e);
return None;
}
}; };
let source_uid = match ensure_address_book_uid(&conn) { let handle = handle_mutex.lock().unwrap();
Ok(u) => u, let address_book_proxy = handle.connection.with_proxy(
Err(e) => { &handle.bus_name,
log::debug!("EDS resolver: could not determine address-book UID: {}", e); &handle.object_path,
return None; Duration::from_secs(60),
} );
};
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( let vcard_result: Result<(String,), _> = address_book_proxy.method_call(
"org.gnome.evolution.dataserver.AddressBook", "org.gnome.evolution.dataserver.AddressBook",

View File

@@ -10,6 +10,7 @@ pub trait ContactResolverBackend {
pub type AnyContactID = String; pub type AnyContactID = String;
#[derive(Clone)]
pub struct ContactResolver<T: ContactResolverBackend> { pub struct ContactResolver<T: ContactResolverBackend> {
backend: T, backend: T,
} }

View File

@@ -522,15 +522,15 @@ impl Daemon {
.await? .await?
{ {
for p in &saved.participants { for p in &saved.participants {
if let DbParticipant::Remote { id: Some(pid), display_name, contact_id: None } = p { if let DbParticipant::Remote { handle, contact_id: None } = p {
log::trace!(target: target::SYNC, "Resolving contact id for participant: {}", display_name); log::trace!(target: target::SYNC, "Resolving contact id for participant: {}", handle);
if let Some(contact) = contact_resolver.resolve_contact_id(display_name) { if let Some(contact) = contact_resolver.resolve_contact_id(handle) {
log::trace!(target: target::SYNC, "Resolved contact id for participant: {}", contact); log::trace!(target: target::SYNC, "Resolved contact id for participant: {}", contact);
let _ = database let _ = database
.with_repository(|r| r.update_participant_contact(*pid, &contact)) .with_repository(|r| r.update_participant_contact(&handle, &contact))
.await; .await;
} else { } else {
log::trace!(target: target::SYNC, "No contact id found for participant: {}", display_name); log::trace!(target: target::SYNC, "No contact id found for participant: {}", handle);
} }
} }
} }

View File

@@ -5,14 +5,14 @@ use crate::daemon::attachment_store::AttachmentStore;
use crate::daemon::models::Attachment; use crate::daemon::models::Attachment;
use kordophone::model::message::AttachmentMetadata; use kordophone::model::message::AttachmentMetadata;
use kordophone::model::outgoing_message::OutgoingMessage; use kordophone::model::outgoing_message::OutgoingMessage;
use kordophone_db::models::participant::Participant as DbParticipant;
use std::collections::HashMap; use std::collections::HashMap;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum Participant { pub enum Participant {
Me, Me,
Remote { Remote {
id: Option<i32>, handle: String,
display_name: String,
contact_id: Option<String>, contact_id: Option<String>,
}, },
} }
@@ -20,8 +20,7 @@ pub enum Participant {
impl From<String> for Participant { impl From<String> for Participant {
fn from(display_name: String) -> Self { fn from(display_name: String) -> Self {
Participant::Remote { Participant::Remote {
id: None, handle: display_name,
display_name,
contact_id: None, contact_id: None,
} }
} }
@@ -30,8 +29,7 @@ impl From<String> for Participant {
impl From<&str> for Participant { impl From<&str> for Participant {
fn from(display_name: &str) -> Self { fn from(display_name: &str) -> Self {
Participant::Remote { Participant::Remote {
id: None, handle: display_name.to_string(),
display_name: display_name.to_string(),
contact_id: None, contact_id: None,
} }
} }
@@ -41,8 +39,8 @@ impl From<kordophone_db::models::Participant> for Participant {
fn from(participant: kordophone_db::models::Participant) -> Self { fn from(participant: kordophone_db::models::Participant) -> Self {
match participant { match participant {
kordophone_db::models::Participant::Me => Participant::Me, kordophone_db::models::Participant::Me => Participant::Me,
kordophone_db::models::Participant::Remote { id, display_name, contact_id } => { kordophone_db::models::Participant::Remote { handle, contact_id } => {
Participant::Remote { id, display_name, contact_id } Participant::Remote { handle, contact_id }
} }
} }
} }
@@ -52,7 +50,7 @@ impl Participant {
pub fn display_name(&self) -> String { pub fn display_name(&self) -> String {
match self { match self {
Participant::Me => "(Me)".to_string(), Participant::Me => "(Me)".to_string(),
Participant::Remote { display_name, .. } => display_name.clone(), Participant::Remote { handle, .. } => handle.clone(),
} }
} }
} }
@@ -110,8 +108,8 @@ impl From<Message> for kordophone_db::models::Message {
id: message.id, id: message.id,
sender: match message.sender { sender: match message.sender {
Participant::Me => kordophone_db::models::Participant::Me, Participant::Me => kordophone_db::models::Participant::Me,
Participant::Remote { id, display_name, contact_id } => { Participant::Remote { handle, contact_id } => {
kordophone_db::models::Participant::Remote { id, display_name, contact_id } kordophone_db::models::Participant::Remote { handle, contact_id }
} }
}, },
text: message.text, text: message.text,
@@ -146,8 +144,7 @@ impl From<kordophone::model::Message> for Message {
id: message.guid, id: message.guid,
sender: match message.sender { sender: match message.sender {
Some(sender) => Participant::Remote { Some(sender) => Participant::Remote {
id: None, handle: sender,
display_name: sender,
contact_id: None, contact_id: None,
}, },
None => Participant::Me, None => Participant::Me,
@@ -175,3 +172,12 @@ impl From<&OutgoingMessage> for Message {
} }
} }
} }
impl From<Participant> for DbParticipant {
fn from(participant: Participant) -> Self {
match participant {
Participant::Me => DbParticipant::Me,
Participant::Remote { handle, contact_id } => DbParticipant::Remote { handle, contact_id: contact_id.clone() },
}
}
}

View File

@@ -23,6 +23,7 @@ use dbus_tokio::connection;
pub struct DBusAgent { pub struct DBusAgent {
event_sink: mpsc::Sender<Event>, event_sink: mpsc::Sender<Event>,
signal_receiver: Arc<Mutex<Option<mpsc::Receiver<Signal>>>>, signal_receiver: Arc<Mutex<Option<mpsc::Receiver<Signal>>>>,
contact_resolver: ContactResolver<EDSContactResolverBackend>,
} }
impl DBusAgent { impl DBusAgent {
@@ -30,6 +31,7 @@ impl DBusAgent {
Self { Self {
event_sink, event_sink,
signal_receiver: Arc::new(Mutex::new(Some(signal_receiver))), signal_receiver: Arc::new(Mutex::new(Some(signal_receiver))),
contact_resolver: ContactResolver::new(EDSContactResolverBackend::default()),
} }
} }
@@ -172,19 +174,18 @@ impl DBusAgent {
} }
fn resolve_participant_display_name(&self, participant: &Participant) -> String { fn resolve_participant_display_name(&self, participant: &Participant) -> String {
let resolver = ContactResolver::new(EDSContactResolverBackend::default());
match participant { match participant {
// Me (we should use a special string here...) // Me (we should use a special string here...)
Participant::Me => "(Me)".to_string(), Participant::Me => "(Me)".to_string(),
// Remote participant with a resolved contact_id // Remote participant with a resolved contact_id
Participant::Remote { display_name, contact_id: Some(contact_id), .. } => { Participant::Remote { handle, contact_id: Some(contact_id), .. } => {
resolver.get_contact_display_name(contact_id).unwrap_or_else(|| display_name.clone()) self.contact_resolver.get_contact_display_name(contact_id).unwrap_or_else(|| handle.clone())
} }
// Remote participant without a resolved contact_id // Remote participant without a resolved contact_id
Participant::Remote { display_name, .. } => { Participant::Remote { handle, .. } => {
display_name.clone() handle.clone()
} }
} }
} }
@@ -278,6 +279,8 @@ impl DbusRepository for DBusAgent {
// Remove the attachment placeholder here. // Remove the attachment placeholder here.
let text = msg.text.replace("\u{FFFC}", ""); let text = msg.text.replace("\u{FFFC}", "");
log::debug!("sender: {:?}", msg.sender.clone());
map.insert("text".into(), arg::Variant(Box::new(text))); map.insert("text".into(), arg::Variant(Box::new(text)));
map.insert( map.insert(
"date".into(), "date".into(),
@@ -285,9 +288,11 @@ impl DbusRepository for DBusAgent {
); );
map.insert( map.insert(
"sender".into(), "sender".into(),
arg::Variant(Box::new(msg.sender.display_name())), arg::Variant(Box::new(self.resolve_participant_display_name(&msg.sender.into()))),
); );
// Attachments array // Attachments array
let attachments: Vec<arg::PropMap> = msg let attachments: Vec<arg::PropMap> = msg
.attachments .attachments