Private
Public Access
1
0

cargo fmt

This commit is contained in:
2025-06-06 16:39:31 -07:00
parent 8cd72d9417
commit 1d3b2f25ba
44 changed files with 758 additions and 505 deletions

View File

@@ -1,6 +1,6 @@
use anyhow::Result; use anyhow::Result;
use diesel::prelude::*;
use async_trait::async_trait; use async_trait::async_trait;
use diesel::prelude::*;
pub use std::sync::Arc; pub use std::sync::Arc;
pub use tokio::sync::Mutex; pub use tokio::sync::Mutex;
@@ -31,7 +31,8 @@ pub struct Database {
impl Database { impl Database {
pub fn new(path: &str) -> Result<Self> { pub fn new(path: &str) -> Result<Self> {
let mut connection = SqliteConnection::establish(path)?; let mut connection = SqliteConnection::establish(path)?;
connection.run_pending_migrations(MIGRATIONS) connection
.run_pending_migrations(MIGRATIONS)
.map_err(|e| anyhow::anyhow!("Error running migrations: {}", e))?; .map_err(|e| anyhow::anyhow!("Error running migrations: {}", e))?;
Ok(Self { connection }) Ok(Self { connection })

View File

@@ -1,6 +1,6 @@
use crate::models::participant::Participant;
use chrono::{DateTime, NaiveDateTime}; use chrono::{DateTime, NaiveDateTime};
use uuid::Uuid; use uuid::Uuid;
use crate::models::participant::Participant;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Conversation { pub struct Conversation {
@@ -38,13 +38,12 @@ impl From<kordophone::model::Conversation> for Conversation {
last_message_preview: value.last_message_preview, last_message_preview: value.last_message_preview,
date: DateTime::from_timestamp( date: DateTime::from_timestamp(
value.date.unix_timestamp(), value.date.unix_timestamp(),
value.date.unix_timestamp_nanos() value.date.unix_timestamp_nanos().try_into().unwrap_or(0),
.try_into()
.unwrap_or(0),
) )
.unwrap() .unwrap()
.naive_local(), .naive_local(),
participants: value.participant_display_names participants: value
.participant_display_names
.into_iter() .into_iter()
.map(|p| p.into()) .map(|p| p.into())
.collect(), .collect(),

View File

@@ -1,9 +1,6 @@
use diesel::prelude::*; use crate::models::{db::participant::InsertableRecord as InsertableParticipant, Conversation};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use crate::models::{ use diesel::prelude::*;
Conversation,
db::participant::InsertableRecord as InsertableParticipant,
};
#[derive(Queryable, Selectable, Insertable, AsChangeset, Clone, Identifiable)] #[derive(Queryable, Selectable, Insertable, AsChangeset, Clone, Identifiable)]
#[diesel(table_name = crate::schema::conversations)] #[diesel(table_name = crate::schema::conversations)]
@@ -33,11 +30,11 @@ impl From<Conversation> for (Record, Vec<InsertableParticipant>) {
fn from(conversation: Conversation) -> Self { fn from(conversation: Conversation) -> Self {
( (
Record::from(conversation.clone()), Record::from(conversation.clone()),
conversation
conversation.participants .participants
.into_iter() .into_iter()
.map(InsertableParticipant::from) .map(InsertableParticipant::from)
.collect() .collect(),
) )
} }
} }

View File

@@ -1,6 +1,6 @@
use diesel::prelude::*;
use chrono::NaiveDateTime;
use crate::models::{Message, Participant}; use crate::models::{Message, Participant};
use chrono::NaiveDateTime;
use diesel::prelude::*;
#[derive(Queryable, Selectable, Insertable, AsChangeset, Clone, Identifiable, Debug)] #[derive(Queryable, Selectable, Insertable, AsChangeset, Clone, Identifiable, Debug)]
#[diesel(table_name = crate::schema::messages)] #[diesel(table_name = crate::schema::messages)]
@@ -22,7 +22,8 @@ impl From<Message> for Record {
Some(serde_json::to_string(&message.file_transfer_guids).unwrap_or_default()) Some(serde_json::to_string(&message.file_transfer_guids).unwrap_or_default())
}; };
let attachment_metadata = message.attachment_metadata let attachment_metadata = message
.attachment_metadata
.map(|metadata| serde_json::to_string(&metadata).unwrap_or_default()); .map(|metadata| serde_json::to_string(&metadata).unwrap_or_default());
Self { Self {
@@ -41,11 +42,13 @@ impl From<Message> for Record {
impl From<Record> for Message { impl From<Record> for Message {
fn from(record: Record) -> Self { fn from(record: Record) -> Self {
let file_transfer_guids = record.file_transfer_guids let file_transfer_guids = record
.file_transfer_guids
.and_then(|json| serde_json::from_str(&json).ok()) .and_then(|json| serde_json::from_str(&json).ok())
.unwrap_or_default(); .unwrap_or_default();
let attachment_metadata = record.attachment_metadata let attachment_metadata = record
.attachment_metadata
.and_then(|json| serde_json::from_str(&json).ok()); .and_then(|json| serde_json::from_str(&json).ok());
Self { Self {
@@ -59,4 +62,3 @@ impl From<Record> for Message {
} }
} }
} }

View File

@@ -1,3 +1,3 @@
pub mod conversation; pub mod conversation;
pub mod participant;
pub mod message; pub mod message;
pub mod participant;

View File

@@ -1,6 +1,6 @@
use diesel::prelude::*;
use crate::models::Participant; use crate::models::Participant;
use crate::schema::conversation_participants; use crate::schema::conversation_participants;
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)]
@@ -27,7 +27,7 @@ impl From<Participant> for InsertableRecord {
Participant::Remote { display_name, .. } => InsertableRecord { Participant::Remote { display_name, .. } => InsertableRecord {
display_name: Some(display_name), display_name: Some(display_name),
is_me: false, is_me: false,
} },
} }
} }
} }
@@ -67,7 +67,7 @@ impl From<Participant> for Record {
id: 0, // This will be set by the database id: 0, // This will be set by the database
display_name: Some(display_name), display_name: Some(display_name),
is_me: false, is_me: false,
} },
} }
} }
} }

View File

@@ -1,9 +1,9 @@
use crate::models::participant::Participant;
use chrono::{DateTime, NaiveDateTime}; use chrono::{DateTime, NaiveDateTime};
use kordophone::model::message::AttachmentMetadata;
use kordophone::model::outgoing_message::OutgoingMessage;
use std::collections::HashMap; use std::collections::HashMap;
use uuid::Uuid; use uuid::Uuid;
use crate::models::participant::Participant;
use kordophone::model::outgoing_message::OutgoingMessage;
use kordophone::model::message::AttachmentMetadata;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Message { pub struct Message {
@@ -35,9 +35,7 @@ impl From<kordophone::model::Message> for Message {
text: value.text, text: value.text,
date: DateTime::from_timestamp( date: DateTime::from_timestamp(
value.date.unix_timestamp(), value.date.unix_timestamp(),
value.date.unix_timestamp_nanos() value.date.unix_timestamp_nanos().try_into().unwrap_or(0),
.try_into()
.unwrap_or(0),
) )
.unwrap() .unwrap()
.naive_local(), .naive_local(),
@@ -107,7 +105,10 @@ impl MessageBuilder {
self self
} }
pub fn attachment_metadata(mut self, attachment_metadata: HashMap<String, AttachmentMetadata>) -> Self { pub fn attachment_metadata(
mut self,
attachment_metadata: HashMap<String, AttachmentMetadata>,
) -> Self {
self.attachment_metadata = Some(attachment_metadata); self.attachment_metadata = Some(attachment_metadata);
self self
} }
@@ -123,4 +124,3 @@ impl MessageBuilder {
} }
} }
} }

View File

@@ -1,8 +1,8 @@
pub mod conversation; pub mod conversation;
pub mod participant;
pub mod message;
pub mod db; pub mod db;
pub mod message;
pub mod participant;
pub use conversation::Conversation; pub use conversation::Conversation;
pub use participant::Participant;
pub use message::Message; pub use message::Message;
pub use participant::Participant;

View File

@@ -4,16 +4,13 @@ use diesel::query_dsl::BelongingToDsl;
use crate::{ use crate::{
models::{ models::{
Conversation,
Message,
Participant,
db::conversation::Record as ConversationRecord, db::conversation::Record as ConversationRecord,
db::participant::{
ConversationParticipant,
Record as ParticipantRecord,
InsertableRecord as InsertableParticipantRecord
},
db::message::Record as MessageRecord, db::message::Record as MessageRecord,
db::participant::{
ConversationParticipant, InsertableRecord as InsertableParticipantRecord,
Record as ParticipantRecord,
},
Conversation, Message, Participant,
}, },
schema, schema,
}; };
@@ -28,9 +25,9 @@ impl<'a> Repository<'a> {
} }
pub fn insert_conversation(&mut self, conversation: Conversation) -> Result<()> { pub fn insert_conversation(&mut self, conversation: Conversation) -> Result<()> {
use crate::schema::conversation_participants::dsl::*;
use crate::schema::conversations::dsl::*; use crate::schema::conversations::dsl::*;
use crate::schema::participants::dsl::*; use crate::schema::participants::dsl::*;
use crate::schema::conversation_participants::dsl::*;
let (db_conversation, db_participants) = conversation.into(); let (db_conversation, db_participants) = conversation.into();
@@ -76,7 +73,8 @@ impl<'a> Repository<'a> {
.load::<ParticipantRecord>(self.connection)?; .load::<ParticipantRecord>(self.connection)?;
let mut model_conversation: Conversation = conversation.into(); let mut model_conversation: Conversation = conversation.into();
model_conversation.participants = db_participants.into_iter().map(|p| p.into()).collect(); model_conversation.participants =
db_participants.into_iter().map(|p| p.into()).collect();
return Ok(Some(model_conversation)); return Ok(Some(model_conversation));
} }
@@ -102,7 +100,8 @@ impl<'a> Repository<'a> {
.load::<ParticipantRecord>(self.connection)?; .load::<ParticipantRecord>(self.connection)?;
let mut model_conversation: Conversation = db_conversation.into(); let mut model_conversation: Conversation = db_conversation.into();
model_conversation.participants = db_participants.into_iter().map(|p| p.into()).collect(); model_conversation.participants =
db_participants.into_iter().map(|p| p.into()).collect();
result.push(model_conversation); result.push(model_conversation);
} }
@@ -111,8 +110,8 @@ impl<'a> Repository<'a> {
} }
pub fn insert_message(&mut self, conversation_guid: &str, message: Message) -> Result<()> { pub fn insert_message(&mut self, conversation_guid: &str, message: Message) -> Result<()> {
use crate::schema::messages::dsl::*;
use crate::schema::conversation_messages::dsl::*; use crate::schema::conversation_messages::dsl::*;
use crate::schema::messages::dsl::*;
// 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();
@@ -136,9 +135,13 @@ impl<'a> Repository<'a> {
Ok(()) Ok(())
} }
pub fn insert_messages(&mut self, conversation_guid: &str, in_messages: Vec<Message>) -> Result<()> { pub fn insert_messages(
use crate::schema::messages::dsl::*; &mut self,
conversation_guid: &str,
in_messages: Vec<Message>,
) -> Result<()> {
use crate::schema::conversation_messages::dsl::*; use crate::schema::conversation_messages::dsl::*;
use crate::schema::messages::dsl::*;
// Local insertable struct for the join table // Local insertable struct for the join table
#[derive(Insertable)] #[derive(Insertable)]
@@ -154,7 +157,8 @@ impl<'a> Repository<'a> {
// Build the collections of insertable records // Build the collections of insertable records
let mut db_messages: Vec<MessageRecord> = Vec::with_capacity(in_messages.len()); let mut db_messages: Vec<MessageRecord> = Vec::with_capacity(in_messages.len());
let mut conv_msg_records: Vec<InsertableConversationMessage> = Vec::with_capacity(in_messages.len()); let mut conv_msg_records: Vec<InsertableConversationMessage> =
Vec::with_capacity(in_messages.len());
for message in in_messages { for message in in_messages {
// Handle participant if message has a remote sender // Handle participant if message has a remote sender
@@ -186,9 +190,12 @@ impl<'a> Repository<'a> {
Ok(()) Ok(())
} }
pub fn get_messages_for_conversation(&mut self, conversation_guid: &str) -> Result<Vec<Message>> { pub fn get_messages_for_conversation(
use crate::schema::messages::dsl::*; &mut self,
conversation_guid: &str,
) -> Result<Vec<Message>> {
use crate::schema::conversation_messages::dsl::*; use crate::schema::conversation_messages::dsl::*;
use crate::schema::messages::dsl::*;
use crate::schema::participants::dsl::*; use crate::schema::participants::dsl::*;
let message_records = conversation_messages let message_records = conversation_messages
@@ -216,9 +223,12 @@ impl<'a> Repository<'a> {
Ok(result) Ok(result)
} }
pub fn get_last_message_for_conversation(&mut self, conversation_guid: &str) -> Result<Option<Message>> { pub fn get_last_message_for_conversation(
use crate::schema::messages::dsl::*; &mut self,
conversation_guid: &str,
) -> Result<Option<Message>> {
use crate::schema::conversation_messages::dsl::*; use crate::schema::conversation_messages::dsl::*;
use crate::schema::messages::dsl::*;
let message_record = conversation_messages let message_record = conversation_messages
.filter(conversation_id.eq(conversation_guid)) .filter(conversation_id.eq(conversation_guid))
@@ -247,7 +257,11 @@ impl<'a> Repository<'a> {
let conversation = self.get_conversation_by_guid(conversation_guid)?; let conversation = self.get_conversation_by_guid(conversation_guid)?;
if let Some(mut conversation) = conversation { if let Some(mut conversation) = conversation {
if let Some(last_message) = self.get_last_message_for_conversation(conversation_guid)? { if let Some(last_message) = self.get_last_message_for_conversation(conversation_guid)? {
log::debug!("Updating conversation metadata: {} message: {:?}", conversation_guid, last_message); log::debug!(
"Updating conversation metadata: {} message: {:?}",
conversation_guid,
last_message
);
conversation.date = last_message.date; conversation.date = last_message.date;
conversation.last_message_preview = Some(last_message.text.clone()); conversation.last_message_preview = Some(last_message.text.clone());
self.insert_conversation(conversation)?; self.insert_conversation(conversation)?;
@@ -261,14 +275,21 @@ impl<'a> Repository<'a> {
// This is a workaround since the Sqlite backend doesn't support `RETURNING` // This is a workaround since the Sqlite backend doesn't support `RETURNING`
// Huge caveat with this is that it depends on whatever the last insert was, prevents concurrent inserts. // Huge caveat with this is that it depends on whatever the last insert was, prevents concurrent inserts.
fn last_insert_id(&mut self) -> Result<i32> { fn last_insert_id(&mut self) -> Result<i32> {
Ok(diesel::select(diesel::dsl::sql::<diesel::sql_types::Integer>("last_insert_rowid()")) Ok(
.get_result(self.connection)?) diesel::select(diesel::dsl::sql::<diesel::sql_types::Integer>(
"last_insert_rowid()",
))
.get_result(self.connection)?,
)
} }
fn get_or_create_participant(&mut self, participant: &Participant) -> Option<i32> { fn get_or_create_participant(&mut self, participant: &Participant) -> Option<i32> {
match participant { match participant {
Participant::Me => None, Participant::Me => None,
Participant::Remote { display_name: p_name, .. } => { Participant::Remote {
display_name: p_name,
..
} => {
use crate::schema::participants::dsl::*; use crate::schema::participants::dsl::*;
let existing_participant = participants let existing_participant = participants

View File

@@ -53,7 +53,11 @@ 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_id));
diesel::allow_tables_to_appear_in_same_query!(conversations, participants, conversation_participants); diesel::allow_tables_to_appear_in_same_query!(
conversations,
participants,
conversation_participants
);
diesel::joinable!(conversation_messages -> conversations (conversation_id)); diesel::joinable!(conversation_messages -> conversations (conversation_id));
diesel::joinable!(conversation_messages -> messages (message_id)); diesel::joinable!(conversation_messages -> messages (message_id));

View File

@@ -1,6 +1,6 @@
use diesel::*;
use serde::{Serialize, de::DeserializeOwned};
use anyhow::Result; use anyhow::Result;
use diesel::*;
use serde::{de::DeserializeOwned, Serialize};
#[derive(Insertable, Queryable, AsChangeset)] #[derive(Insertable, Queryable, AsChangeset)]
#[diesel(table_name = crate::schema::settings)] #[diesel(table_name = crate::schema::settings)]
@@ -18,16 +18,15 @@ impl<'a> Settings<'a> {
Self { connection } Self { connection }
} }
pub fn put<T: Serialize>( pub fn put<T: Serialize>(&mut self, k: &str, v: &T) -> Result<()> {
&mut self,
k: &str,
v: &T,
) -> Result<()> {
use crate::schema::settings::dsl::*; use crate::schema::settings::dsl::*;
let bytes = bincode::serialize(v)?; let bytes = bincode::serialize(v)?;
diesel::insert_into(settings) diesel::insert_into(settings)
.values(SettingsRow { key: k, value: &bytes }) .values(SettingsRow {
key: k,
value: &bytes,
})
.on_conflict(key) .on_conflict(key)
.do_update() .do_update()
.set(value.eq(&bytes)) .set(value.eq(&bytes))
@@ -36,10 +35,7 @@ impl<'a> Settings<'a> {
Ok(()) Ok(())
} }
pub fn get<T: DeserializeOwned>( pub fn get<T: DeserializeOwned>(&mut self, k: &str) -> Result<Option<T>> {
&mut self,
k: &str,
) -> Result<Option<T>> {
use crate::schema::settings::dsl::*; use crate::schema::settings::dsl::*;
let blob: Option<Vec<u8>> = settings let blob: Option<Vec<u8>> = settings
.select(value) .select(value)
@@ -60,12 +56,8 @@ impl<'a> Settings<'a> {
pub fn list_keys(&mut self) -> Result<Vec<String>> { pub fn list_keys(&mut self) -> Result<Vec<String>> {
use crate::schema::settings::dsl::*; use crate::schema::settings::dsl::*;
let keys: Vec<String> = settings let keys: Vec<String> = settings.select(key).load(self.connection)?;
.select(key)
.load(self.connection)?;
Ok(keys) Ok(keys)
} }
} }

View File

@@ -2,8 +2,8 @@ use crate::{
database::{Database, DatabaseAccess}, database::{Database, DatabaseAccess},
models::{ models::{
conversation::{Conversation, ConversationBuilder}, conversation::{Conversation, ConversationBuilder},
participant::Participant,
message::Message, message::Message,
participant::Participant,
}, },
}; };
@@ -11,9 +11,17 @@ use crate::{
fn participants_equal_ignoring_id(a: &Participant, b: &Participant) -> bool { 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 { display_name: name_a, .. }, (
Participant::Remote { display_name: name_b, .. }) => name_a == name_b, Participant::Remote {
_ => false display_name: name_a,
..
},
Participant::Remote {
display_name: name_b,
..
},
) => name_a == name_b,
_ => false,
} }
} }
@@ -21,7 +29,9 @@ 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().zip(b.iter()).all(|(a, b)| participants_equal_ignoring_id(a, b)) a.iter()
.zip(b.iter())
.all(|(a, b)| participants_equal_ignoring_id(a, b))
} }
#[tokio::test] #[tokio::test]
@@ -40,18 +50,23 @@ async fn test_add_conversation() {
.display_name("Test Conversation") .display_name("Test Conversation")
.build(); .build();
repository.insert_conversation(test_conversation.clone()).unwrap(); repository
.insert_conversation(test_conversation.clone())
.unwrap();
// Try to fetch with id now // Try to fetch with id now
let conversation = repository.get_conversation_by_guid(guid).unwrap().unwrap(); let conversation = repository.get_conversation_by_guid(guid).unwrap().unwrap();
assert_eq!(conversation.guid, "test"); assert_eq!(conversation.guid, "test");
// Modify the conversation and update it // Modify the conversation and update it
let modified_conversation = test_conversation.into_builder() let modified_conversation = test_conversation
.into_builder()
.display_name("Modified Conversation") .display_name("Modified Conversation")
.build(); .build();
repository.insert_conversation(modified_conversation.clone()).unwrap(); repository
.insert_conversation(modified_conversation.clone())
.unwrap();
// Make sure we still only have one conversation. // Make sure we still only have one conversation.
let all_conversations = repository.all_conversations(i32::MAX, 0).unwrap(); let all_conversations = repository.all_conversations(i32::MAX, 0).unwrap();
@@ -60,7 +75,8 @@ async fn test_add_conversation() {
// And make sure the display name was updated // And make sure the display name was updated
let conversation = repository.get_conversation_by_guid(guid).unwrap().unwrap(); let conversation = repository.get_conversation_by_guid(guid).unwrap().unwrap();
assert_eq!(conversation.display_name.unwrap(), "Modified Conversation"); assert_eq!(conversation.display_name.unwrap(), "Modified Conversation");
}).await; })
.await;
} }
#[tokio::test] #[tokio::test]
@@ -81,7 +97,10 @@ async fn test_conversation_participants() {
let read_conversation = repository.get_conversation_by_guid(&guid).unwrap().unwrap(); let read_conversation = repository.get_conversation_by_guid(&guid).unwrap().unwrap();
let read_participants = read_conversation.participants; let read_participants = read_conversation.participants;
assert!(participants_vec_equal_ignoring_id(&participants, &read_participants)); assert!(participants_vec_equal_ignoring_id(
&participants,
&read_participants
));
// Try making another conversation with the same participants // Try making another conversation with the same participants
let conversation = ConversationBuilder::new() let conversation = ConversationBuilder::new()
@@ -94,8 +113,12 @@ async fn test_conversation_participants() {
let read_conversation = repository.get_conversation_by_guid(&guid).unwrap().unwrap(); let read_conversation = repository.get_conversation_by_guid(&guid).unwrap().unwrap();
let read_participants: Vec<Participant> = read_conversation.participants; let read_participants: Vec<Participant> = read_conversation.participants;
assert!(participants_vec_equal_ignoring_id(&participants, &read_participants)); assert!(participants_vec_equal_ignoring_id(
}).await; &participants,
&read_participants
));
})
.await;
} }
#[tokio::test] #[tokio::test]
@@ -132,9 +155,16 @@ async fn test_all_conversations_with_participants() {
let conv1 = all_conversations.iter().find(|c| c.guid == guid1).unwrap(); let conv1 = all_conversations.iter().find(|c| c.guid == guid1).unwrap();
let conv2 = all_conversations.iter().find(|c| c.guid == guid2).unwrap(); let conv2 = all_conversations.iter().find(|c| c.guid == guid2).unwrap();
assert!(participants_vec_equal_ignoring_id(&conv1.participants, &participants1)); assert!(participants_vec_equal_ignoring_id(
assert!(participants_vec_equal_ignoring_id(&conv2.participants, &participants2)); &conv1.participants,
}).await; &participants1
));
assert!(participants_vec_equal_ignoring_id(
&conv2.participants,
&participants2
));
})
.await;
} }
#[tokio::test] #[tokio::test]
@@ -163,11 +193,17 @@ async fn test_messages() {
.build(); .build();
// Insert both messages // Insert both messages
repository.insert_message(&conversation_id, message1.clone()).unwrap(); repository
repository.insert_message(&conversation_id, message2.clone()).unwrap(); .insert_message(&conversation_id, message1.clone())
.unwrap();
repository
.insert_message(&conversation_id, message2.clone())
.unwrap();
// Retrieve messages // Retrieve messages
let messages = repository.get_messages_for_conversation(&conversation_id).unwrap(); let messages = repository
.get_messages_for_conversation(&conversation_id)
.unwrap();
assert_eq!(messages.len(), 2); assert_eq!(messages.len(), 2);
// Verify first message (from Me) // Verify first message (from Me)
@@ -181,9 +217,13 @@ async fn test_messages() {
if let Participant::Remote { display_name, .. } = &retrieved_message2.sender { if let Participant::Remote { display_name, .. } = &retrieved_message2.sender {
assert_eq!(display_name, "Alice"); assert_eq!(display_name, "Alice");
} else { } else {
panic!("Expected Remote participant. Got: {:?}", retrieved_message2.sender); panic!(
"Expected Remote participant. Got: {:?}",
retrieved_message2.sender
);
} }
}).await; })
.await;
} }
#[tokio::test] #[tokio::test]
@@ -191,9 +231,7 @@ async fn test_message_ordering() {
let mut db = Database::new_in_memory().unwrap(); let mut db = Database::new_in_memory().unwrap();
db.with_repository(|repository| { db.with_repository(|repository| {
// Create a conversation // Create a conversation
let conversation = ConversationBuilder::new() let conversation = ConversationBuilder::new().display_name("Test Chat").build();
.display_name("Test Chat")
.build();
let conversation_id = conversation.guid.clone(); let conversation_id = conversation.guid.clone();
repository.insert_conversation(conversation).unwrap(); repository.insert_conversation(conversation).unwrap();
@@ -215,19 +253,28 @@ async fn test_message_ordering() {
.build(); .build();
// Insert messages // Insert messages
repository.insert_message(&conversation_id, message1).unwrap(); repository
repository.insert_message(&conversation_id, message2).unwrap(); .insert_message(&conversation_id, message1)
repository.insert_message(&conversation_id, message3).unwrap(); .unwrap();
repository
.insert_message(&conversation_id, message2)
.unwrap();
repository
.insert_message(&conversation_id, message3)
.unwrap();
// Retrieve messages and verify order // Retrieve messages and verify order
let messages = repository.get_messages_for_conversation(&conversation_id).unwrap(); let messages = repository
.get_messages_for_conversation(&conversation_id)
.unwrap();
assert_eq!(messages.len(), 3); assert_eq!(messages.len(), 3);
// Messages should be ordered by date // Messages should be ordered by date
for i in 1..messages.len() { for i in 1..messages.len() {
assert!(messages[i].date > messages[i-1].date); assert!(messages[i].date > messages[i - 1].date);
} }
}).await; })
.await;
} }
#[tokio::test] #[tokio::test]
@@ -245,10 +292,7 @@ async fn test_insert_messages_batch() {
// Prepare a batch of messages with increasing timestamps // Prepare a batch of messages with increasing timestamps
let now = chrono::Utc::now().naive_utc(); let now = chrono::Utc::now().naive_utc();
let message1 = Message::builder() let message1 = Message::builder().text("Hi".to_string()).date(now).build();
.text("Hi".to_string())
.date(now)
.build();
let message2 = Message::builder() let message2 = Message::builder()
.text("Hello".to_string()) .text("Hello".to_string())
@@ -280,7 +324,9 @@ async fn test_insert_messages_batch() {
.unwrap(); .unwrap();
// Retrieve messages and verify // Retrieve messages and verify
let retrieved_messages = repository.get_messages_for_conversation(&conversation_id).unwrap(); let retrieved_messages = repository
.get_messages_for_conversation(&conversation_id)
.unwrap();
assert_eq!(retrieved_messages.len(), original_messages.len()); assert_eq!(retrieved_messages.len(), original_messages.len());
// Ensure ordering by date // Ensure ordering by date
@@ -299,8 +345,14 @@ 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 { display_name: o_name, .. }, Participant::Remote {
Participant::Remote { display_name: r_name, .. }, display_name: o_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 {:?}",
@@ -310,7 +362,10 @@ async fn test_insert_messages_batch() {
} }
// Make sure the last message is the last one we inserted // Make sure the last message is the last one we inserted
let last_message = repository.get_last_message_for_conversation(&conversation_id).unwrap().unwrap(); let last_message = repository
.get_last_message_for_conversation(&conversation_id)
.unwrap()
.unwrap();
assert_eq!(last_message.id, message4.id); assert_eq!(last_message.id, message4.id);
}) })
.await; .await;
@@ -342,13 +397,34 @@ async fn test_settings() {
}; };
settings.put("test_struct", &test_struct).unwrap(); settings.put("test_struct", &test_struct).unwrap();
assert_eq!(settings.get::<TestStruct>("test_struct").unwrap().unwrap(), test_struct); assert_eq!(
settings.get::<TestStruct>("test_struct").unwrap().unwrap(),
test_struct
);
// Test with an option<string> // Test with an option<string>
settings.put("test_struct_option", &Option::<String>::None).unwrap(); settings
assert!(settings.get::<Option<String>>("test_struct_option").unwrap().unwrap().is_none()); .put("test_struct_option", &Option::<String>::None)
.unwrap();
assert!(settings
.get::<Option<String>>("test_struct_option")
.unwrap()
.unwrap()
.is_none());
settings.put("test_struct_option", &Option::<String>::Some("test".to_string())).unwrap(); settings
assert_eq!(settings.get::<Option<String>>("test_struct_option").unwrap().unwrap(), Some("test".to_string())); .put(
}).await; "test_struct_option",
&Option::<String>::Some("test".to_string()),
)
.unwrap();
assert_eq!(
settings
.get::<Option<String>>("test_struct_option")
.unwrap()
.unwrap(),
Some("test".to_string())
);
})
.await;
} }

View File

@@ -1,6 +1,6 @@
use async_trait::async_trait;
use crate::model::update::UpdateItem;
use crate::model::event::Event; use crate::model::event::Event;
use crate::model::update::UpdateItem;
use async_trait::async_trait;
use futures_util::stream::Stream; use futures_util::stream::Stream;
#[async_trait] #[async_trait]

View File

@@ -85,7 +85,10 @@ impl ConversationBuilder {
self self
} }
pub fn display_name<T>(mut self, display_name: T) -> Self where T: Into<String> { pub fn display_name<T>(mut self, display_name: T) -> Self
where
T: Into<String>,
{
self.display_name = Some(display_name.into()); self.display_name = Some(display_name.into());
self self
} }

View File

@@ -15,11 +15,23 @@ pub enum EventData {
impl From<UpdateItem> for Event { impl From<UpdateItem> for Event {
fn from(update: UpdateItem) -> Self { fn from(update: UpdateItem) -> Self {
match update { match update {
UpdateItem { conversation: Some(conversation), message: None, .. } UpdateItem {
=> Event { data: EventData::ConversationChanged(conversation), update_seq: update.seq }, conversation: Some(conversation),
message: None,
..
} => Event {
data: EventData::ConversationChanged(conversation),
update_seq: update.seq,
},
UpdateItem { conversation: Some(conversation), message: Some(message), .. } UpdateItem {
=> Event { data: EventData::MessageReceived(conversation, message), update_seq: update.seq }, conversation: Some(conversation),
message: Some(message),
..
} => Event {
data: EventData::MessageReceived(conversation, message),
update_seq: update.seq,
},
_ => panic!("Invalid update item: {:?}", update), _ => panic!("Invalid update item: {:?}", update),
} }

View File

@@ -82,7 +82,9 @@ impl JwtToken {
let payload: JwtPayload = serde_json::from_slice(&payload)?; let payload: JwtPayload = serde_json::from_slice(&payload)?;
// Parse jwt expiration date // Parse jwt expiration date
let timestamp = DateTime::from_timestamp(payload.exp, 0).unwrap().naive_utc(); let timestamp = DateTime::from_timestamp(payload.exp, 0)
.unwrap()
.naive_utc();
let expiration_date = DateTime::from_naive_utc_and_offset(timestamp, Utc); let expiration_date = DateTime::from_naive_utc_and_offset(timestamp, Utc);
Ok(JwtToken { Ok(JwtToken {

View File

@@ -100,7 +100,10 @@ impl MessageBuilder {
self self
} }
pub fn attachment_metadata(mut self, attachment_metadata: HashMap<String, AttachmentMetadata>) -> Self { pub fn attachment_metadata(
mut self,
attachment_metadata: HashMap<String, AttachmentMetadata>,
) -> Self {
self.attachment_metadata = Some(attachment_metadata); self.attachment_metadata = Some(attachment_metadata);
self self
} }
@@ -116,4 +119,3 @@ impl MessageBuilder {
} }
} }
} }

View File

@@ -1,6 +1,6 @@
use serde::Serialize;
use super::conversation::ConversationID; use super::conversation::ConversationID;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use serde::Serialize;
use uuid::Uuid; use uuid::Uuid;
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]

View File

@@ -1,6 +1,6 @@
use serde::Deserialize;
use super::conversation::Conversation; use super::conversation::Conversation;
use super::message::Message; use super::message::Message;
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct UpdateItem { pub struct UpdateItem {
@@ -16,6 +16,10 @@ pub struct UpdateItem {
impl Default for UpdateItem { impl Default for UpdateItem {
fn default() -> Self { fn default() -> Self {
Self { seq: 0, conversation: None, message: None } Self {
seq: 0,
conversation: None,
message: None,
}
} }
} }

View File

@@ -14,9 +14,9 @@ use crate::{
}, },
}; };
use bytes::Bytes;
use futures_util::stream::BoxStream; use futures_util::stream::BoxStream;
use futures_util::StreamExt; use futures_util::StreamExt;
use bytes::Bytes;
pub struct TestClient { pub struct TestClient {
pub version: &'static str, pub version: &'static str,
@@ -120,7 +120,11 @@ impl APIInterface for TestClient {
Ok(TestEventSocket::new()) Ok(TestEventSocket::new())
} }
async fn fetch_attachment_data(&mut self, guid: &String, preview: bool) -> Result<Self::ResponseStream, Self::Error> { async fn fetch_attachment_data(
&mut self,
guid: &String,
preview: bool,
) -> Result<Self::ResponseStream, Self::Error> {
Ok(futures_util::stream::iter(vec![Ok(Bytes::from_static(b"test"))]).boxed()) Ok(futures_util::stream::iter(vec![Ok(Bytes::from_static(b"test"))]).boxed())
} }
} }

View File

@@ -11,14 +11,12 @@ fn main() {
..Default::default() ..Default::default()
}; };
let xml = std::fs::read_to_string(KORDOPHONE_XML) let xml = std::fs::read_to_string(KORDOPHONE_XML).expect("Error reading server dbus interface");
.expect("Error reading server dbus interface");
let output = dbus_codegen::generate(&xml, &opts) let output =
.expect("Error generating server dbus interface"); dbus_codegen::generate(&xml, &opts).expect("Error generating server dbus interface");
std::fs::write(out_path, output) std::fs::write(out_path, output).expect("Error writing server dbus code");
.expect("Error writing server dbus code");
println!("cargo:rerun-if-changed={}", KORDOPHONE_XML); println!("cargo:rerun-if-changed={}", KORDOPHONE_XML);
} }

View File

@@ -16,8 +16,8 @@ use crate::daemon::models::Attachment;
use crate::daemon::Daemon; use crate::daemon::Daemon;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::Mutex;
use tokio::sync::mpsc::{Receiver, Sender}; use tokio::sync::mpsc::{Receiver, Sender};
use tokio::sync::Mutex;
use tokio::pin; use tokio::pin;
@@ -62,7 +62,10 @@ impl AttachmentStore {
data_dir.join("attachments") data_dir.join("attachments")
} }
pub fn new(database: Arc<Mutex<Database>>, daemon_event_sink: Sender<Event>) -> AttachmentStore { pub fn new(
database: Arc<Mutex<Database>>,
daemon_event_sink: Sender<Event>,
) -> AttachmentStore {
let store_path = Self::get_default_store_path(); let store_path = Self::get_default_store_path();
log::info!(target: target::ATTACHMENTS, "Attachment store path: {}", store_path.display()); log::info!(target: target::ATTACHMENTS, "Attachment store path: {}", store_path.display());

View File

@@ -150,7 +150,8 @@ impl Daemon {
} }
// Attachment store // Attachment store
let mut attachment_store = AttachmentStore::new(self.database.clone(), self.event_sender.clone()); let mut attachment_store =
AttachmentStore::new(self.database.clone(), self.event_sender.clone());
self.attachment_store_sink = Some(attachment_store.get_event_sink()); self.attachment_store_sink = Some(attachment_store.get_event_sink());
tokio::spawn(async move { tokio::spawn(async move {
attachment_store.run().await; attachment_store.run().await;
@@ -304,7 +305,10 @@ impl Daemon {
self.attachment_store_sink self.attachment_store_sink
.as_ref() .as_ref()
.unwrap() .unwrap()
.send(AttachmentStoreEvent::QueueDownloadAttachment(attachment_id, preview)) .send(AttachmentStoreEvent::QueueDownloadAttachment(
attachment_id,
preview,
))
.await .await
.unwrap(); .unwrap();

View File

@@ -20,12 +20,18 @@ pub struct Attachment {
impl Attachment { impl Attachment {
pub fn get_path(&self, preview: bool) -> PathBuf { pub fn get_path(&self, preview: bool) -> PathBuf {
self.base_path.with_extension(if preview { "preview" } else { "full" }) self.base_path
.with_extension(if preview { "preview" } else { "full" })
} }
pub fn is_downloaded(&self, preview: bool) -> bool { pub fn is_downloaded(&self, preview: bool) -> bool {
std::fs::exists(&self.get_path(preview)) std::fs::exists(&self.get_path(preview)).expect(
.expect(format!("Wasn't able to check for the existence of an attachment file path at {}", &self.get_path(preview).display()).as_str()) format!(
"Wasn't able to check for the existence of an attachment file path at {}",
&self.get_path(preview).display()
)
.as_str(),
)
} }
} }

View File

@@ -1,11 +1,11 @@
use chrono::NaiveDateTime;
use chrono::DateTime; use chrono::DateTime;
use chrono::NaiveDateTime;
use std::collections::HashMap; use crate::daemon::attachment_store::AttachmentStore;
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 crate::daemon::models::Attachment; use std::collections::HashMap;
use crate::daemon::attachment_store::AttachmentStore;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum Participant { pub enum Participant {
@@ -63,13 +63,22 @@ pub struct Message {
pub attachments: Vec<Attachment>, pub attachments: Vec<Attachment>,
} }
fn attachments_from(file_transfer_guids: &Vec<String>, attachment_metadata: &Option<HashMap<String, AttachmentMetadata>>) -> Vec<Attachment> { fn attachments_from(
file_transfer_guids: &Vec<String>,
attachment_metadata: &Option<HashMap<String, AttachmentMetadata>>,
) -> Vec<Attachment> {
file_transfer_guids file_transfer_guids
.iter() .iter()
.map(|guid| { .map(|guid| {
let mut attachment = AttachmentStore::get_attachment_impl(&AttachmentStore::get_default_store_path(), guid); let mut attachment = AttachmentStore::get_attachment_impl(
&AttachmentStore::get_default_store_path(),
guid,
);
attachment.metadata = match attachment_metadata { attachment.metadata = match attachment_metadata {
Some(attachment_metadata) => attachment_metadata.get(guid).cloned().map(|metadata| metadata.into()), Some(attachment_metadata) => attachment_metadata
.get(guid)
.cloned()
.map(|metadata| metadata.into()),
None => None, None => None,
}; };
@@ -80,7 +89,8 @@ fn attachments_from(file_transfer_guids: &Vec<String>, attachment_metadata: &Opt
impl From<kordophone_db::models::Message> for Message { impl From<kordophone_db::models::Message> for Message {
fn from(message: kordophone_db::models::Message) -> Self { fn from(message: kordophone_db::models::Message) -> Self {
let attachments = attachments_from(&message.file_transfer_guids, &message.attachment_metadata); let attachments =
attachments_from(&message.file_transfer_guids, &message.attachment_metadata);
Self { Self {
id: message.id, id: message.id,
sender: message.sender.into(), sender: message.sender.into(),
@@ -105,11 +115,21 @@ impl From<Message> for kordophone_db::models::Message {
date: message.date, date: message.date,
file_transfer_guids: message.attachments.iter().map(|a| a.guid.clone()).collect(), file_transfer_guids: message.attachments.iter().map(|a| a.guid.clone()).collect(),
attachment_metadata: { attachment_metadata: {
let metadata_map: HashMap<String, kordophone::model::message::AttachmentMetadata> = message.attachments let metadata_map: HashMap<String, kordophone::model::message::AttachmentMetadata> =
message
.attachments
.iter() .iter()
.filter_map(|a| a.metadata.as_ref().map(|m| (a.guid.clone(), m.clone().into()))) .filter_map(|a| {
a.metadata
.as_ref()
.map(|m| (a.guid.clone(), m.clone().into()))
})
.collect(); .collect();
if metadata_map.is_empty() { None } else { Some(metadata_map) } if metadata_map.is_empty() {
None
} else {
Some(metadata_map)
}
}, },
} }
} }
@@ -117,7 +137,8 @@ impl From<Message> for kordophone_db::models::Message {
impl From<kordophone::model::Message> for Message { impl From<kordophone::model::Message> for Message {
fn from(message: kordophone::model::Message) -> Self { fn from(message: kordophone::model::Message) -> Self {
let attachments = attachments_from(&message.file_transfer_guids, &message.attachment_metadata); let attachments =
attachments_from(&message.file_transfer_guids, &message.attachment_metadata);
Self { Self {
id: message.guid, id: message.guid,
sender: match message.sender { sender: match message.sender {
@@ -130,9 +151,7 @@ impl From<kordophone::model::Message> for Message {
text: message.text, text: message.text,
date: DateTime::from_timestamp( date: DateTime::from_timestamp(
message.date.unix_timestamp(), message.date.unix_timestamp(),
message.date.unix_timestamp_nanos() message.date.unix_timestamp_nanos().try_into().unwrap_or(0),
.try_into()
.unwrap_or(0),
) )
.unwrap() .unwrap()
.naive_local(), .naive_local(),

View File

@@ -1,13 +1,13 @@
use std::collections::VecDeque; use std::collections::VecDeque;
use std::time::Duration; use std::time::Duration;
use tokio::sync::mpsc::{Sender, Receiver}; use tokio::sync::mpsc::{Receiver, Sender};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tokio_condvar::Condvar; use tokio_condvar::Condvar;
use crate::daemon::events::Event as DaemonEvent; use crate::daemon::events::Event as DaemonEvent;
use kordophone::model::outgoing_message::OutgoingMessage;
use kordophone::api::APIInterface; use kordophone::api::APIInterface;
use kordophone::model::outgoing_message::OutgoingMessage;
use anyhow::Result; use anyhow::Result;
@@ -29,7 +29,11 @@ pub struct PostOffice<C: APIInterface, F: AsyncFnMut() -> Result<C>> {
} }
impl<C: APIInterface, F: AsyncFnMut() -> Result<C>> PostOffice<C, F> { impl<C: APIInterface, F: AsyncFnMut() -> Result<C>> PostOffice<C, F> {
pub fn new(event_source: Receiver<Event>, event_sink: Sender<DaemonEvent>, make_client: F) -> Self { pub fn new(
event_source: Receiver<Event>,
event_sink: Sender<DaemonEvent>,
make_client: F,
) -> Self {
Self { Self {
event_source, event_source,
event_sink, event_sink,
@@ -87,9 +91,8 @@ impl<C: APIInterface, F: AsyncFnMut() -> Result<C>> PostOffice<C, F> {
async fn try_send_message( async fn try_send_message(
make_client: &mut F, make_client: &mut F,
event_sink: &Sender<DaemonEvent>, event_sink: &Sender<DaemonEvent>,
message: OutgoingMessage message: OutgoingMessage,
) -> Vec<OutgoingMessage> ) -> Vec<OutgoingMessage> {
{
let mut retry_messages = Vec::new(); let mut retry_messages = Vec::new();
match (make_client)().await { match (make_client)().await {
@@ -100,7 +103,8 @@ impl<C: APIInterface, F: AsyncFnMut() -> Result<C>> PostOffice<C, F> {
log::info!(target: target::POST_OFFICE, "Message sent successfully: {}", message.guid); log::info!(target: target::POST_OFFICE, "Message sent successfully: {}", message.guid);
let conversation_id = message.conversation_id.clone(); let conversation_id = message.conversation_id.clone();
let event = DaemonEvent::MessageSent(sent_message.into(), message, conversation_id); let event =
DaemonEvent::MessageSent(sent_message.into(), message, conversation_id);
event_sink.send(event).await.unwrap(); event_sink.send(event).await.unwrap();
} }

View File

@@ -1,5 +1,5 @@
use kordophone_db::settings::Settings as DbSettings;
use anyhow::Result; use anyhow::Result;
use kordophone_db::settings::Settings as DbSettings;
pub mod keys { pub mod keys {
pub static SERVER_URL: &str = "ServerURL"; pub static SERVER_URL: &str = "ServerURL";
@@ -7,8 +7,7 @@ pub mod keys {
pub static TOKEN: &str = "Token"; pub static TOKEN: &str = "Token";
} }
#[derive(Debug)] #[derive(Debug, Default)]
#[derive(Default)]
pub struct Settings { pub struct Settings {
pub server_url: Option<String>, pub server_url: Option<String>,
pub username: Option<String>, pub username: Option<String>,
@@ -47,4 +46,3 @@ impl Settings {
Ok(()) Ok(())
} }
} }

View File

@@ -1,24 +1,21 @@
use crate::daemon::{ use crate::daemon::{
Daemon,
DaemonResult,
events::{Event, Reply}, events::{Event, Reply},
target, target, Daemon, DaemonResult,
}; };
use kordophone::APIInterface;
use kordophone::api::event_socket::EventSocket; use kordophone::api::event_socket::EventSocket;
use kordophone::model::event::Event as UpdateEvent; use kordophone::model::event::Event as UpdateEvent;
use kordophone::model::event::EventData as UpdateEventData; use kordophone::model::event::EventData as UpdateEventData;
use kordophone::APIInterface;
use kordophone_db::database::Database; use kordophone_db::database::Database;
use kordophone_db::database::DatabaseAccess; use kordophone_db::database::DatabaseAccess;
use tokio::sync::mpsc::Sender;
use std::sync::Arc;
use tokio::sync::Mutex;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use tokio::sync::mpsc::Sender;
use tokio::sync::Mutex;
pub struct UpdateMonitor { pub struct UpdateMonitor {
database: Arc<Mutex<Database>>, database: Arc<Mutex<Database>>,
@@ -42,7 +39,8 @@ impl UpdateMonitor {
make_event: impl FnOnce(Reply<T>) -> Event, make_event: impl FnOnce(Reply<T>) -> Event,
) -> DaemonResult<T> { ) -> DaemonResult<T> {
let (reply_tx, reply_rx) = tokio::sync::oneshot::channel(); let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
self.event_sender.send(make_event(reply_tx)) self.event_sender
.send(make_event(reply_tx))
.await .await
.map_err(|_| "Failed to send event")?; .map_err(|_| "Failed to send event")?;
@@ -68,7 +66,11 @@ impl UpdateMonitor {
} }
// This is the non-hacky path once we can reason about chat items with associatedMessageGUIDs (e.g., reactions). // This is the non-hacky path once we can reason about chat items with associatedMessageGUIDs (e.g., reactions).
let last_message = self.database.with_repository(|r| r.get_last_message_for_conversation(&conversation.guid)).await.unwrap_or_default(); let last_message = self
.database
.with_repository(|r| r.get_last_message_for_conversation(&conversation.guid))
.await
.unwrap_or_default();
match (&last_message, &conversation.last_message) { match (&last_message, &conversation.last_message) {
(Some(message), Some(conversation_message)) => { (Some(message), Some(conversation_message)) => {
if message.id == conversation_message.guid { if message.id == conversation_message.guid {
@@ -80,10 +82,12 @@ impl UpdateMonitor {
}; };
// Update the last sync time and proceed with sync // Update the last sync time and proceed with sync
self.last_sync_times.insert(conversation.guid.clone(), Instant::now()); self.last_sync_times
.insert(conversation.guid.clone(), Instant::now());
log::info!(target: target::UPDATES, "Syncing new messages for conversation id: {}", conversation.guid); log::info!(target: target::UPDATES, "Syncing new messages for conversation id: {}", conversation.guid);
self.send_event(|r| Event::SyncConversation(conversation.guid, r)).await self.send_event(|r| Event::SyncConversation(conversation.guid, r))
.await
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
log::error!("Failed to send daemon event: {}", e); log::error!("Failed to send daemon event: {}", e);
}); });
@@ -92,7 +96,8 @@ impl UpdateMonitor {
UpdateEventData::MessageReceived(conversation, message) => { UpdateEventData::MessageReceived(conversation, message) => {
log::info!(target: target::UPDATES, "Message received: msgid:{:?}, convid:{:?}", message.guid, conversation.guid); log::info!(target: target::UPDATES, "Message received: msgid:{:?}, convid:{:?}", message.guid, conversation.guid);
log::info!(target: target::UPDATES, "Triggering message sync for conversation id: {}", conversation.guid); log::info!(target: target::UPDATES, "Triggering message sync for conversation id: {}", conversation.guid);
self.send_event(|r| Event::SyncConversation(conversation.guid, r)).await self.send_event(|r| Event::SyncConversation(conversation.guid, r))
.await
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
log::error!("Failed to send daemon event: {}", e); log::error!("Failed to send daemon event: {}", e);
}); });

View File

@@ -7,7 +7,8 @@ use tokio::sync::oneshot;
use crate::daemon::{ use crate::daemon::{
events::{Event, Reply}, events::{Event, Reply},
settings::Settings, DaemonResult, settings::Settings,
DaemonResult,
}; };
use crate::dbus::interface::NetBuzzertKordophoneRepository as DbusRepository; use crate::dbus::interface::NetBuzzertKordophoneRepository as DbusRepository;
@@ -138,11 +139,15 @@ impl DbusRepository for ServerImpl {
); );
// Add attachments array // Add attachments array
let attachments: Vec<arg::PropMap> = msg.attachments let attachments: Vec<arg::PropMap> = msg
.attachments
.into_iter() .into_iter()
.map(|attachment| { .map(|attachment| {
let mut attachment_map = arg::PropMap::new(); let mut attachment_map = arg::PropMap::new();
attachment_map.insert("guid".into(), arg::Variant(Box::new(attachment.guid.clone()))); attachment_map.insert(
"guid".into(),
arg::Variant(Box::new(attachment.guid.clone())),
);
// Get attachment paths and download status // Get attachment paths and download status
let path = attachment.get_path(false); let path = attachment.get_path(false);
@@ -150,10 +155,24 @@ impl DbusRepository for ServerImpl {
let downloaded = attachment.is_downloaded(false); let downloaded = attachment.is_downloaded(false);
let preview_downloaded = attachment.is_downloaded(true); let preview_downloaded = attachment.is_downloaded(true);
attachment_map.insert("path".into(), arg::Variant(Box::new(path.to_string_lossy().to_string()))); attachment_map.insert(
attachment_map.insert("preview_path".into(), arg::Variant(Box::new(preview_path.to_string_lossy().to_string()))); "path".into(),
attachment_map.insert("downloaded".into(), arg::Variant(Box::new(downloaded))); arg::Variant(Box::new(path.to_string_lossy().to_string())),
attachment_map.insert("preview_downloaded".into(), arg::Variant(Box::new(preview_downloaded))); );
attachment_map.insert(
"preview_path".into(),
arg::Variant(Box::new(
preview_path.to_string_lossy().to_string(),
)),
);
attachment_map.insert(
"downloaded".into(),
arg::Variant(Box::new(downloaded)),
);
attachment_map.insert(
"preview_downloaded".into(),
arg::Variant(Box::new(preview_downloaded)),
);
// Add metadata if present // Add metadata if present
if let Some(ref metadata) = attachment.metadata { if let Some(ref metadata) = attachment.metadata {
@@ -164,16 +183,28 @@ impl DbusRepository for ServerImpl {
let mut attribution_map = arg::PropMap::new(); let mut attribution_map = arg::PropMap::new();
if let Some(width) = attribution_info.width { if let Some(width) = attribution_info.width {
attribution_map.insert("width".into(), arg::Variant(Box::new(width as i32))); attribution_map.insert(
"width".into(),
arg::Variant(Box::new(width as i32)),
);
} }
if let Some(height) = attribution_info.height { if let Some(height) = attribution_info.height {
attribution_map.insert("height".into(), arg::Variant(Box::new(height as i32))); attribution_map.insert(
"height".into(),
arg::Variant(Box::new(height as i32)),
);
} }
metadata_map.insert("attribution_info".into(), arg::Variant(Box::new(attribution_map))); metadata_map.insert(
"attribution_info".into(),
arg::Variant(Box::new(attribution_map)),
);
} }
attachment_map.insert("metadata".into(), arg::Variant(Box::new(metadata_map))); attachment_map.insert(
"metadata".into(),
arg::Variant(Box::new(metadata_map)),
);
} }
attachment_map attachment_map
@@ -216,20 +247,21 @@ impl DbusRepository for ServerImpl {
( (
// - path: string // - path: string
path.to_string_lossy().to_string(), path.to_string_lossy().to_string(),
// - preview_path: string // - preview_path: string
preview_path.to_string_lossy().to_string(), preview_path.to_string_lossy().to_string(),
// - downloaded: boolean // - downloaded: boolean
downloaded, downloaded,
// - preview_downloaded: boolean // - preview_downloaded: boolean
preview_downloaded, preview_downloaded,
) )
}) })
} }
fn download_attachment(&mut self, attachment_id: String, preview: bool) -> Result<(), dbus::MethodErr> { fn download_attachment(
&mut self,
attachment_id: String,
preview: bool,
) -> Result<(), dbus::MethodErr> {
// For now, just trigger the download event - we'll implement the actual download logic later // For now, just trigger the download event - we'll implement the actual download logic later
self.send_event_sync(|r| Event::DownloadAttachment(attachment_id, preview, r)) self.send_event_sync(|r| Event::DownloadAttachment(attachment_id, preview, r))
} }
@@ -286,7 +318,6 @@ impl DbusSettings for ServerImpl {
} }
} }
fn run_sync_future<F, T>(f: F) -> Result<T, MethodErr> fn run_sync_future<F, T>(f: F) -> Result<T, MethodErr>
where where
T: Send, T: Send,

View File

@@ -60,14 +60,12 @@ async fn main() {
// Create and register server implementation // Create and register server implementation
let server = ServerImpl::new(daemon.event_sender.clone()); let server = ServerImpl::new(daemon.event_sender.clone());
dbus_registry.register_object( dbus_registry.register_object(interface::OBJECT_PATH, server, |cr| {
interface::OBJECT_PATH, vec![
server,
|cr| vec![
interface::register_net_buzzert_kordophone_repository(cr), interface::register_net_buzzert_kordophone_repository(cr),
interface::register_net_buzzert_kordophone_settings(cr), interface::register_net_buzzert_kordophone_settings(cr),
] ]
); });
let mut signal_receiver = daemon.obtain_signal_receiver(); let mut signal_receiver = daemon.obtain_signal_receiver();
tokio::spawn(async move { tokio::spawn(async move {

View File

@@ -10,14 +10,12 @@ fn main() {
..Default::default() ..Default::default()
}; };
let xml = std::fs::read_to_string(KORDOPHONE_XML) let xml = std::fs::read_to_string(KORDOPHONE_XML).expect("Error reading server dbus interface");
.expect("Error reading server dbus interface");
let output = dbus_codegen::generate(&xml, &opts) let output =
.expect("Error generating client dbus interface"); dbus_codegen::generate(&xml, &opts).expect("Error generating client dbus interface");
std::fs::write(out_path, output) std::fs::write(out_path, output).expect("Error writing client dbus code");
.expect("Error writing client dbus code");
println!("cargo:rerun-if-changed={}", KORDOPHONE_XML); println!("cargo:rerun-if-changed={}", KORDOPHONE_XML);
} }

View File

@@ -1,12 +1,12 @@
use kordophone::APIInterface;
use kordophone::api::http_client::HTTPAPIClient;
use kordophone::api::http_client::Credentials;
use kordophone::api::InMemoryAuthenticationStore;
use kordophone::api::event_socket::EventSocket; use kordophone::api::event_socket::EventSocket;
use kordophone::api::http_client::Credentials;
use kordophone::api::http_client::HTTPAPIClient;
use kordophone::api::InMemoryAuthenticationStore;
use kordophone::APIInterface;
use crate::printers::{ConversationPrinter, MessagePrinter};
use anyhow::Result; use anyhow::Result;
use clap::Subcommand; use clap::Subcommand;
use crate::printers::{ConversationPrinter, MessagePrinter};
use kordophone::model::event::EventData; use kordophone::model::event::EventData;
use kordophone::model::outgoing_message::OutgoingMessage; use kordophone::model::outgoing_message::OutgoingMessage;
@@ -16,18 +16,18 @@ pub fn make_api_client_from_env() -> HTTPAPIClient<InMemoryAuthenticationStore>
dotenv::dotenv().ok(); dotenv::dotenv().ok();
// read from env // read from env
let base_url = std::env::var("KORDOPHONE_API_URL") let base_url = std::env::var("KORDOPHONE_API_URL").expect("KORDOPHONE_API_URL must be set");
.expect("KORDOPHONE_API_URL must be set");
let credentials = Credentials { let credentials = Credentials {
username: std::env::var("KORDOPHONE_USERNAME") username: std::env::var("KORDOPHONE_USERNAME").expect("KORDOPHONE_USERNAME must be set"),
.expect("KORDOPHONE_USERNAME must be set"),
password: std::env::var("KORDOPHONE_PASSWORD") password: std::env::var("KORDOPHONE_PASSWORD").expect("KORDOPHONE_PASSWORD must be set"),
.expect("KORDOPHONE_PASSWORD must be set"),
}; };
HTTPAPIClient::new(base_url.parse().unwrap(), InMemoryAuthenticationStore::new(Some(credentials))) HTTPAPIClient::new(
base_url.parse().unwrap(),
InMemoryAuthenticationStore::new(Some(credentials)),
)
} }
#[derive(Subcommand)] #[derive(Subcommand)]
@@ -36,9 +36,7 @@ pub enum Commands {
Conversations, Conversations,
/// Prints all messages in a conversation. /// Prints all messages in a conversation.
Messages { Messages { conversation_id: String },
conversation_id: String,
},
/// Prints the server Kordophone version. /// Prints the server Kordophone version.
Version, Version,
@@ -65,7 +63,10 @@ impl Commands {
Commands::Messages { conversation_id } => client.print_messages(conversation_id).await, Commands::Messages { conversation_id } => client.print_messages(conversation_id).await,
Commands::RawUpdates => client.print_raw_updates().await, Commands::RawUpdates => client.print_raw_updates().await,
Commands::Events => client.print_events().await, Commands::Events => client.print_events().await,
Commands::SendMessage { conversation_id, message } => client.send_message(conversation_id, message).await, Commands::SendMessage {
conversation_id,
message,
} => client.send_message(conversation_id, message).await,
} }
} }
} }
@@ -96,7 +97,10 @@ impl ClientCli {
} }
pub async fn print_messages(&mut self, conversation_id: String) -> Result<()> { pub async fn print_messages(&mut self, conversation_id: String) -> Result<()> {
let messages = self.api.get_messages(&conversation_id, None, None, None).await?; let messages = self
.api
.get_messages(&conversation_id, None, None, None)
.await?;
for message in messages { for message in messages {
println!("{}", MessagePrinter::new(&message.into())); println!("{}", MessagePrinter::new(&message.into()));
} }
@@ -113,7 +117,10 @@ impl ClientCli {
println!("Conversation changed: {}", conversation.guid); println!("Conversation changed: {}", conversation.guid);
} }
EventData::MessageReceived(conversation, message) => { EventData::MessageReceived(conversation, message) => {
println!("Message received: msg: {} conversation: {}", message.guid, conversation.guid); println!(
"Message received: msg: {} conversation: {}",
message.guid, conversation.guid
);
} }
} }
} }
@@ -143,5 +150,3 @@ impl ClientCli {
Ok(()) Ok(())
} }
} }

View File

@@ -1,8 +1,8 @@
use crate::printers::{ConversationPrinter, MessagePrinter};
use anyhow::Result; use anyhow::Result;
use clap::Subcommand; use clap::Subcommand;
use dbus::blocking::{Connection, Proxy}; use dbus::blocking::{Connection, Proxy};
use prettytable::table; use prettytable::table;
use crate::printers::{ConversationPrinter, MessagePrinter};
const DBUS_NAME: &str = "net.buzzert.kordophonecd"; const DBUS_NAME: &str = "net.buzzert.kordophonecd";
const DBUS_PATH: &str = "/net/buzzert/kordophonecd/daemon"; const DBUS_PATH: &str = "/net/buzzert/kordophonecd/daemon";
@@ -21,9 +21,7 @@ pub enum Commands {
Conversations, Conversations,
/// Runs a full sync operation for a conversation and its messages. /// Runs a full sync operation for a conversation and its messages.
Sync { Sync { conversation_id: Option<String> },
conversation_id: Option<String>,
},
/// Runs a sync operation for the conversation list. /// Runs a sync operation for the conversation list.
SyncList, SyncList,
@@ -62,14 +60,10 @@ pub enum ConfigCommands {
Print, Print,
/// Sets the server URL. /// Sets the server URL.
SetServerUrl { SetServerUrl { url: String },
url: String,
},
/// Sets the username. /// Sets the username.
SetUsername { SetUsername { username: String },
username: String,
},
} }
impl Commands { impl Commands {
@@ -82,9 +76,19 @@ impl Commands {
Commands::SyncList => client.sync_conversations_list().await, Commands::SyncList => client.sync_conversations_list().await,
Commands::Config { command } => client.config(command).await, Commands::Config { command } => client.config(command).await,
Commands::Signals => client.wait_for_signals().await, Commands::Signals => client.wait_for_signals().await,
Commands::Messages { conversation_id, last_message_id } => client.print_messages(conversation_id, last_message_id).await, Commands::Messages {
conversation_id,
last_message_id,
} => {
client
.print_messages(conversation_id, last_message_id)
.await
}
Commands::DeleteAllConversations => client.delete_all_conversations().await, Commands::DeleteAllConversations => client.delete_all_conversations().await,
Commands::SendMessage { conversation_id, text } => client.enqueue_outgoing_message(conversation_id, text).await, Commands::SendMessage {
conversation_id,
text,
} => client.enqueue_outgoing_message(conversation_id, text).await,
} }
} }
} }
@@ -96,12 +100,13 @@ struct DaemonCli {
impl DaemonCli { impl DaemonCli {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self> {
Ok(Self { Ok(Self {
conn: Connection::new_session()? conn: Connection::new_session()?,
}) })
} }
fn proxy(&self) -> Proxy<&Connection> { fn proxy(&self) -> Proxy<&Connection> {
self.conn.with_proxy(DBUS_NAME, DBUS_PATH, std::time::Duration::from_millis(5000)) self.conn
.with_proxy(DBUS_NAME, DBUS_PATH, std::time::Duration::from_millis(5000))
} }
pub async fn print_version(&mut self) -> Result<()> { pub async fn print_version(&mut self) -> Result<()> {
@@ -136,8 +141,16 @@ impl DaemonCli {
.map_err(|e| anyhow::anyhow!("Failed to sync conversations: {}", e)) .map_err(|e| anyhow::anyhow!("Failed to sync conversations: {}", e))
} }
pub async fn print_messages(&mut self, conversation_id: String, last_message_id: Option<String>) -> Result<()> { pub async fn print_messages(
let messages = KordophoneRepository::get_messages(&self.proxy(), &conversation_id, &last_message_id.unwrap_or_default())?; &mut self,
conversation_id: String,
last_message_id: Option<String>,
) -> Result<()> {
let messages = KordophoneRepository::get_messages(
&self.proxy(),
&conversation_id,
&last_message_id.unwrap_or_default(),
)?;
println!("Number of messages: {}", messages.len()); println!("Number of messages: {}", messages.len());
for message in messages { for message in messages {
@@ -147,8 +160,13 @@ impl DaemonCli {
Ok(()) Ok(())
} }
pub async fn enqueue_outgoing_message(&mut self, conversation_id: String, text: String) -> Result<()> { pub async fn enqueue_outgoing_message(
let outgoing_message_id = KordophoneRepository::send_message(&self.proxy(), &conversation_id, &text)?; &mut self,
conversation_id: String,
text: String,
) -> Result<()> {
let outgoing_message_id =
KordophoneRepository::send_message(&self.proxy(), &conversation_id, &text)?;
println!("Outgoing message ID: {}", outgoing_message_id); println!("Outgoing message ID: {}", outgoing_message_id);
Ok(()) Ok(())
} }
@@ -159,10 +177,12 @@ impl DaemonCli {
pub use super::dbus_interface::NetBuzzertKordophoneRepositoryConversationsUpdated as ConversationsUpdated; pub use super::dbus_interface::NetBuzzertKordophoneRepositoryConversationsUpdated as ConversationsUpdated;
} }
let _id = self.proxy().match_signal(|h: dbus_signals::ConversationsUpdated, _: &Connection, _: &Message| { let _id = self.proxy().match_signal(
|h: dbus_signals::ConversationsUpdated, _: &Connection, _: &Message| {
println!("Signal: Conversations updated"); println!("Signal: Conversations updated");
true true
}); },
);
println!("Waiting for signals..."); println!("Waiting for signals...");
loop { loop {

View File

@@ -3,27 +3,30 @@ use clap::Subcommand;
use kordophone::APIInterface; use kordophone::APIInterface;
use std::{env, path::PathBuf}; use std::{env, path::PathBuf};
use crate::{
client,
printers::{ConversationPrinter, MessagePrinter},
};
use kordophone_db::database::{Database, DatabaseAccess}; use kordophone_db::database::{Database, DatabaseAccess};
use crate::{client, printers::{ConversationPrinter, MessagePrinter}};
#[derive(Subcommand)] #[derive(Subcommand)]
pub enum Commands { pub enum Commands {
/// For dealing with the table of cached conversations. /// For dealing with the table of cached conversations.
Conversations { Conversations {
#[clap(subcommand)] #[clap(subcommand)]
command: ConversationCommands command: ConversationCommands,
}, },
/// For dealing with the table of cached messages. /// For dealing with the table of cached messages.
Messages { Messages {
#[clap(subcommand)] #[clap(subcommand)]
command: MessageCommands command: MessageCommands,
}, },
/// For managing settings in the database. /// For managing settings in the database.
Settings { Settings {
#[clap(subcommand)] #[clap(subcommand)]
command: SettingsCommands command: SettingsCommands,
}, },
} }
@@ -39,9 +42,7 @@ pub enum ConversationCommands {
#[derive(Subcommand)] #[derive(Subcommand)]
pub enum MessageCommands { pub enum MessageCommands {
/// Prints all messages in a conversation. /// Prints all messages in a conversation.
List { List { conversation_id: String },
conversation_id: String
},
} }
#[derive(Subcommand)] #[derive(Subcommand)]
@@ -49,7 +50,7 @@ pub enum SettingsCommands {
/// Lists all settings or gets a specific setting. /// Lists all settings or gets a specific setting.
Get { Get {
/// The key to get. If not provided, all settings will be listed. /// The key to get. If not provided, all settings will be listed.
key: Option<String> key: Option<String>,
}, },
/// Sets a setting value. /// Sets a setting value.
@@ -76,7 +77,9 @@ impl Commands {
ConversationCommands::Sync => db.sync_with_client().await, ConversationCommands::Sync => db.sync_with_client().await,
}, },
Commands::Messages { command: cmd } => match cmd { Commands::Messages { command: cmd } => match cmd {
MessageCommands::List { conversation_id } => db.print_messages(&conversation_id).await, MessageCommands::List { conversation_id } => {
db.print_messages(&conversation_id).await
}
}, },
Commands::Settings { command: cmd } => match cmd { Commands::Settings { command: cmd } => match cmd {
SettingsCommands::Get { key } => db.get_setting(key).await, SettingsCommands::Get { key } => db.get_setting(key).await,
@@ -88,15 +91,17 @@ impl Commands {
} }
struct DbClient { struct DbClient {
database: Database database: Database,
} }
impl DbClient { impl DbClient {
fn database_path() -> PathBuf { fn database_path() -> PathBuf {
env::var("KORDOPHONE_DB_PATH").unwrap_or_else(|_| { env::var("KORDOPHONE_DB_PATH")
.unwrap_or_else(|_| {
let temp_dir = env::temp_dir(); let temp_dir = env::temp_dir();
temp_dir.join("kpcli_chat.db").to_str().unwrap().to_string() temp_dir.join("kpcli_chat.db").to_str().unwrap().to_string()
}).into() })
.into()
} }
pub fn new() -> Result<Self> { pub fn new() -> Result<Self> {
@@ -106,13 +111,14 @@ impl DbClient {
println!("kpcli: Using db at {}", path_str); println!("kpcli: Using db at {}", path_str);
let db = Database::new(path_str)?; let db = Database::new(path_str)?;
Ok( Self { database: db }) Ok(Self { database: db })
} }
pub async fn print_conversations(&mut self) -> Result<()> { pub async fn print_conversations(&mut self) -> Result<()> {
let all_conversations = self.database.with_repository(|repository| { let all_conversations = self
repository.all_conversations(i32::MAX, 0) .database
}).await?; .with_repository(|repository| repository.all_conversations(i32::MAX, 0))
.await?;
println!("{} Conversations: ", all_conversations.len()); println!("{} Conversations: ", all_conversations.len());
for conversation in all_conversations { for conversation in all_conversations {
@@ -123,9 +129,10 @@ impl DbClient {
} }
pub async fn print_messages(&mut self, conversation_id: &str) -> Result<()> { pub async fn print_messages(&mut self, conversation_id: &str) -> Result<()> {
let messages = self.database.with_repository(|repository| { let messages = self
repository.get_messages_for_conversation(conversation_id) .database
}).await?; .with_repository(|repository| repository.get_messages_for_conversation(conversation_id))
.await?;
for message in messages { for message in messages {
println!("{}", MessagePrinter::new(&message.into())); println!("{}", MessagePrinter::new(&message.into()));
@@ -136,7 +143,8 @@ impl DbClient {
pub async fn sync_with_client(&mut self) -> Result<()> { pub async fn sync_with_client(&mut self) -> Result<()> {
let mut client = client::make_api_client_from_env(); let mut client = client::make_api_client_from_env();
let fetched_conversations = client.get_conversations().await?; let fetched_conversations = client.get_conversations().await?;
let db_conversations: Vec<kordophone_db::models::Conversation> = fetched_conversations.into_iter() let db_conversations: Vec<kordophone_db::models::Conversation> = fetched_conversations
.into_iter()
.map(kordophone_db::models::Conversation::from) .map(kordophone_db::models::Conversation::from)
.collect(); .collect();
@@ -145,31 +153,37 @@ impl DbClient {
let conversation_id = conversation.guid.clone(); let conversation_id = conversation.guid.clone();
// Insert the conversation // Insert the conversation
self.database.with_repository(|repository| { self.database
repository.insert_conversation(conversation) .with_repository(|repository| repository.insert_conversation(conversation))
}).await?; .await?;
// Fetch and sync messages for this conversation // Fetch and sync messages for this conversation
let messages = client.get_messages(&conversation_id, None, None, None).await?; let messages = client
let db_messages: Vec<kordophone_db::models::Message> = messages.into_iter() .get_messages(&conversation_id, None, None, None)
.await?;
let db_messages: Vec<kordophone_db::models::Message> = messages
.into_iter()
.map(kordophone_db::models::Message::from) .map(kordophone_db::models::Message::from)
.collect(); .collect();
// Insert each message // Insert each message
self.database.with_repository(|repository| -> Result<()> { self.database
.with_repository(|repository| -> Result<()> {
for message in db_messages { for message in db_messages {
repository.insert_message(&conversation_id, message)?; repository.insert_message(&conversation_id, message)?;
} }
Ok(()) Ok(())
}).await?; })
.await?;
} }
Ok(()) Ok(())
} }
pub async fn get_setting(&mut self, key: Option<String>) -> Result<()> { pub async fn get_setting(&mut self, key: Option<String>) -> Result<()> {
self.database.with_settings(|settings| { self.database
.with_settings(|settings| {
match key { match key {
Some(key) => { Some(key) => {
// Get a specific setting // Get a specific setting
@@ -178,7 +192,7 @@ impl DbClient {
Some(v) => println!("{} = {}", key, v), Some(v) => println!("{} = {}", key, v),
None => println!("Setting '{}' not found", key), None => println!("Setting '{}' not found", key),
} }
}, }
None => { None => {
// List all settings // List all settings
let keys = settings.list_keys()?; let keys = settings.list_keys()?;
@@ -198,23 +212,28 @@ impl DbClient {
} }
Ok(()) Ok(())
}).await })
.await
} }
pub async fn put_setting(&mut self, key: String, value: String) -> Result<()> { pub async fn put_setting(&mut self, key: String, value: String) -> Result<()> {
self.database.with_settings(|settings| { self.database
.with_settings(|settings| {
settings.put(&key, &value)?; settings.put(&key, &value)?;
Ok(()) Ok(())
}).await })
.await
} }
pub async fn delete_setting(&mut self, key: String) -> Result<()> { pub async fn delete_setting(&mut self, key: String) -> Result<()> {
self.database.with_settings(|settings| { self.database
.with_settings(|settings| {
let count = settings.del(&key)?; let count = settings.del(&key)?;
if count == 0 { if count == 0 {
println!("Setting '{}' not found", key); println!("Setting '{}' not found", key);
} }
Ok(()) Ok(())
}).await })
.await
} }
} }

View File

@@ -1,7 +1,7 @@
mod client; mod client;
mod daemon;
mod db; mod db;
mod printers; mod printers;
mod daemon;
use anyhow::Result; use anyhow::Result;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
@@ -33,7 +33,7 @@ enum Commands {
Daemon { Daemon {
#[command(subcommand)] #[command(subcommand)]
command: daemon::Commands, command: daemon::Commands,
} },
} }
async fn run_command(command: Commands) -> Result<()> { async fn run_command(command: Commands) -> Result<()> {
@@ -62,7 +62,8 @@ async fn main() {
let cli = Cli::parse(); let cli = Cli::parse();
run_command(cli.command).await run_command(cli.command)
.await
.map_err(|e| println!("Error: {}", e)) .map_err(|e| println!("Error: {}", e))
.err(); .err();
} }

View File

@@ -1,9 +1,9 @@
use std::fmt::Display;
use std::collections::HashMap;
use time::OffsetDateTime;
use pretty::RcDoc;
use dbus::arg::{self, RefArg}; use dbus::arg::{self, RefArg};
use kordophone::model::message::AttachmentMetadata; use kordophone::model::message::AttachmentMetadata;
use pretty::RcDoc;
use std::collections::HashMap;
use std::fmt::Display;
use time::OffsetDateTime;
pub struct PrintableConversation { pub struct PrintableConversation {
pub guid: String, pub guid: String,
@@ -34,7 +34,11 @@ impl From<kordophone_db::models::Conversation> for PrintableConversation {
date: OffsetDateTime::from_unix_timestamp(value.date.and_utc().timestamp()).unwrap(), date: OffsetDateTime::from_unix_timestamp(value.date.and_utc().timestamp()).unwrap(),
unread_count: value.unread_count.into(), unread_count: value.unread_count.into(),
last_message_preview: value.last_message_preview, last_message_preview: value.last_message_preview,
participants: value.participants.into_iter().map(|p| p.display_name()).collect(), participants: value
.participants
.into_iter()
.map(|p| p.display_name())
.collect(),
display_name: value.display_name, display_name: value.display_name,
} }
} }
@@ -44,17 +48,33 @@ impl From<arg::PropMap> for PrintableConversation {
fn from(value: arg::PropMap) -> Self { fn from(value: arg::PropMap) -> Self {
Self { Self {
guid: value.get("guid").unwrap().as_str().unwrap().to_string(), guid: value.get("guid").unwrap().as_str().unwrap().to_string(),
date: OffsetDateTime::from_unix_timestamp(value.get("date").unwrap().as_i64().unwrap()).unwrap(), date: OffsetDateTime::from_unix_timestamp(value.get("date").unwrap().as_i64().unwrap())
unread_count: value.get("unread_count").unwrap().as_i64().unwrap().try_into().unwrap(), .unwrap(),
last_message_preview: value.get("last_message_preview").unwrap().as_str().map(|s| s.to_string()), unread_count: value
participants: value.get("participants") .get("unread_count")
.unwrap()
.as_i64()
.unwrap()
.try_into()
.unwrap(),
last_message_preview: value
.get("last_message_preview")
.unwrap()
.as_str()
.map(|s| s.to_string()),
participants: value
.get("participants")
.unwrap() .unwrap()
.0 .0
.as_iter() .as_iter()
.unwrap() .unwrap()
.map(|s| s.as_str().unwrap().to_string()) .map(|s| s.as_str().unwrap().to_string())
.collect(), .collect(),
display_name: value.get("display_name").unwrap().as_str().map(|s| s.to_string()), display_name: value
.get("display_name")
.unwrap()
.as_str()
.map(|s| s.to_string()),
} }
} }
} }
@@ -97,19 +117,22 @@ impl From<kordophone_db::models::Message> for PrintableMessage {
impl From<arg::PropMap> for PrintableMessage { impl From<arg::PropMap> for PrintableMessage {
fn from(value: arg::PropMap) -> Self { fn from(value: arg::PropMap) -> Self {
// Parse file transfer GUIDs from JSON if present // Parse file transfer GUIDs from JSON if present
let file_transfer_guids = value.get("file_transfer_guids") let file_transfer_guids = value
.get("file_transfer_guids")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.and_then(|json_str| serde_json::from_str(json_str).ok()) .and_then(|json_str| serde_json::from_str(json_str).ok())
.unwrap_or_default(); .unwrap_or_default();
// Parse attachment metadata from JSON if present // Parse attachment metadata from JSON if present
let attachment_metadata = value.get("attachment_metadata") let attachment_metadata = value
.get("attachment_metadata")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.and_then(|json_str| serde_json::from_str(json_str).ok()); .and_then(|json_str| serde_json::from_str(json_str).ok());
Self { Self {
guid: value.get("id").unwrap().as_str().unwrap().to_string(), guid: value.get("id").unwrap().as_str().unwrap().to_string(),
date: OffsetDateTime::from_unix_timestamp(value.get("date").unwrap().as_i64().unwrap()).unwrap(), date: OffsetDateTime::from_unix_timestamp(value.get("date").unwrap().as_i64().unwrap())
.unwrap(),
sender: value.get("sender").unwrap().as_str().unwrap().to_string(), sender: value.get("sender").unwrap().as_str().unwrap().to_string(),
text: value.get("text").unwrap().as_str().unwrap().to_string(), text: value.get("text").unwrap().as_str().unwrap().to_string(),
file_transfer_guids, file_transfer_guids,
@@ -119,12 +142,13 @@ impl From<arg::PropMap> for PrintableMessage {
} }
pub struct ConversationPrinter<'a> { pub struct ConversationPrinter<'a> {
doc: RcDoc<'a, PrintableConversation> doc: RcDoc<'a, PrintableConversation>,
} }
impl<'a> ConversationPrinter<'a> { impl<'a> ConversationPrinter<'a> {
pub fn new(conversation: &'a PrintableConversation) -> Self { pub fn new(conversation: &'a PrintableConversation) -> Self {
let preview = conversation.last_message_preview let preview = conversation
.last_message_preview
.as_deref() .as_deref()
.unwrap_or("<null>") .unwrap_or("<null>")
.replace('\n', " "); .replace('\n', " ");
@@ -143,24 +167,22 @@ impl<'a> ConversationPrinter<'a> {
.append(RcDoc::line()) .append(RcDoc::line())
.append("Participants: ") .append("Participants: ")
.append("[") .append("[")
.append(RcDoc::line()
.append( .append(
conversation.participants RcDoc::line()
.append(
conversation
.participants
.iter() .iter()
.map(|name| .map(|name| RcDoc::text(name).append(",").append(RcDoc::line()))
RcDoc::text(name) .fold(RcDoc::nil(), |acc, x| acc.append(x)),
.append(",")
.append(RcDoc::line())
) )
.fold(RcDoc::nil(), |acc, x| acc.append(x)) .nest(4),
)
.nest(4)
) )
.append("]") .append("]")
.append(RcDoc::line()) .append(RcDoc::line())
.append("Last Message Preview: ") .append("Last Message Preview: ")
.append(preview) .append(preview)
.nest(4) .nest(4),
) )
.append(RcDoc::line()) .append(RcDoc::line())
.append(">"); .append(">");
@@ -176,7 +198,7 @@ impl Display for ConversationPrinter<'_> {
} }
pub struct MessagePrinter<'a> { pub struct MessagePrinter<'a> {
doc: RcDoc<'a, PrintableMessage> doc: RcDoc<'a, PrintableMessage>,
} }
impl Display for MessagePrinter<'_> { impl Display for MessagePrinter<'_> {
@@ -187,8 +209,7 @@ impl Display for MessagePrinter<'_> {
impl<'a> MessagePrinter<'a> { impl<'a> MessagePrinter<'a> {
pub fn new(message: &'a PrintableMessage) -> Self { pub fn new(message: &'a PrintableMessage) -> Self {
let mut doc = RcDoc::text(format!("<Message: \"{}\"", &message.guid)) let mut doc = RcDoc::text(format!("<Message: \"{}\"", &message.guid)).append(
.append(
RcDoc::line() RcDoc::line()
.append("Date: ") .append("Date: ")
.append(message.date.to_string()) .append(message.date.to_string())
@@ -198,26 +219,30 @@ impl<'a> MessagePrinter<'a> {
.append(RcDoc::line()) .append(RcDoc::line())
.append("Body: ") .append("Body: ")
.append(&message.text) .append(&message.text)
.nest(4) .nest(4),
); );
// Add file transfer GUIDs and attachment metadata if present // Add file transfer GUIDs and attachment metadata if present
if !message.file_transfer_guids.is_empty() { if !message.file_transfer_guids.is_empty() {
doc = doc.append(RcDoc::line()) doc = doc.append(RcDoc::line()).append(
.append(
RcDoc::line() RcDoc::line()
.append("Attachments:") .append("Attachments:")
.append( .append(
message.file_transfer_guids.iter().map(|guid| { message
let mut attachment_doc = RcDoc::line() .file_transfer_guids
.append("- ") .iter()
.append(guid); .map(|guid| {
let mut attachment_doc = RcDoc::line().append("- ").append(guid);
// Add metadata if available for this GUID // Add metadata if available for this GUID
if let Some(ref metadata) = message.attachment_metadata { if let Some(ref metadata) = message.attachment_metadata {
if let Some(attachment_meta) = metadata.get(guid) { if let Some(attachment_meta) = metadata.get(guid) {
if let Some(ref attribution) = attachment_meta.attribution_info { if let Some(ref attribution) =
if let (Some(width), Some(height)) = (attribution.width, attribution.height) { attachment_meta.attribution_info
{
if let (Some(width), Some(height)) =
(attribution.width, attribution.height)
{
attachment_doc = attachment_doc attachment_doc = attachment_doc
.append(RcDoc::line()) .append(RcDoc::line())
.append(" Dimensions: ") .append(" Dimensions: ")
@@ -231,9 +256,9 @@ impl<'a> MessagePrinter<'a> {
attachment_doc attachment_doc
}) })
.fold(RcDoc::nil(), |acc, x| acc.append(x)) .fold(RcDoc::nil(), |acc, x| acc.append(x)),
) )
.nest(4) .nest(4),
); );
} }