cargo fmt
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
use anyhow::Result;
|
||||
use diesel::prelude::*;
|
||||
use async_trait::async_trait;
|
||||
use diesel::prelude::*;
|
||||
|
||||
pub use std::sync::Arc;
|
||||
pub use tokio::sync::Mutex;
|
||||
@@ -31,7 +31,8 @@ pub struct Database {
|
||||
impl Database {
|
||||
pub fn new(path: &str) -> Result<Self> {
|
||||
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))?;
|
||||
|
||||
Ok(Self { connection })
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::models::participant::Participant;
|
||||
use chrono::{DateTime, NaiveDateTime};
|
||||
use uuid::Uuid;
|
||||
use crate::models::participant::Participant;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Conversation {
|
||||
@@ -38,13 +38,12 @@ impl From<kordophone::model::Conversation> for Conversation {
|
||||
last_message_preview: value.last_message_preview,
|
||||
date: DateTime::from_timestamp(
|
||||
value.date.unix_timestamp(),
|
||||
value.date.unix_timestamp_nanos()
|
||||
.try_into()
|
||||
.unwrap_or(0),
|
||||
value.date.unix_timestamp_nanos().try_into().unwrap_or(0),
|
||||
)
|
||||
.unwrap()
|
||||
.naive_local(),
|
||||
participants: value.participant_display_names
|
||||
participants: value
|
||||
.participant_display_names
|
||||
.into_iter()
|
||||
.map(|p| p.into())
|
||||
.collect(),
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
use diesel::prelude::*;
|
||||
use crate::models::{db::participant::InsertableRecord as InsertableParticipant, Conversation};
|
||||
use chrono::NaiveDateTime;
|
||||
use crate::models::{
|
||||
Conversation,
|
||||
db::participant::InsertableRecord as InsertableParticipant,
|
||||
};
|
||||
use diesel::prelude::*;
|
||||
|
||||
#[derive(Queryable, Selectable, Insertable, AsChangeset, Clone, Identifiable)]
|
||||
#[diesel(table_name = crate::schema::conversations)]
|
||||
@@ -33,11 +30,11 @@ impl From<Conversation> for (Record, Vec<InsertableParticipant>) {
|
||||
fn from(conversation: Conversation) -> Self {
|
||||
(
|
||||
Record::from(conversation.clone()),
|
||||
|
||||
conversation.participants
|
||||
conversation
|
||||
.participants
|
||||
.into_iter()
|
||||
.map(InsertableParticipant::from)
|
||||
.collect()
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use diesel::prelude::*;
|
||||
use chrono::NaiveDateTime;
|
||||
use crate::models::{Message, Participant};
|
||||
use chrono::NaiveDateTime;
|
||||
use diesel::prelude::*;
|
||||
|
||||
#[derive(Queryable, Selectable, Insertable, AsChangeset, Clone, Identifiable, Debug)]
|
||||
#[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())
|
||||
};
|
||||
|
||||
let attachment_metadata = message.attachment_metadata
|
||||
let attachment_metadata = message
|
||||
.attachment_metadata
|
||||
.map(|metadata| serde_json::to_string(&metadata).unwrap_or_default());
|
||||
|
||||
Self {
|
||||
@@ -41,11 +42,13 @@ impl From<Message> for Record {
|
||||
|
||||
impl From<Record> for Message {
|
||||
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())
|
||||
.unwrap_or_default();
|
||||
|
||||
let attachment_metadata = record.attachment_metadata
|
||||
let attachment_metadata = record
|
||||
.attachment_metadata
|
||||
.and_then(|json| serde_json::from_str(&json).ok());
|
||||
|
||||
Self {
|
||||
@@ -59,4 +62,3 @@ impl From<Record> for Message {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
pub mod conversation;
|
||||
pub mod participant;
|
||||
pub mod message;
|
||||
pub mod participant;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use diesel::prelude::*;
|
||||
use crate::models::Participant;
|
||||
use crate::schema::conversation_participants;
|
||||
use diesel::prelude::*;
|
||||
|
||||
#[derive(Queryable, Selectable, AsChangeset, Identifiable)]
|
||||
#[diesel(table_name = crate::schema::participants)]
|
||||
@@ -27,7 +27,7 @@ impl From<Participant> for InsertableRecord {
|
||||
Participant::Remote { display_name, .. } => InsertableRecord {
|
||||
display_name: Some(display_name),
|
||||
is_me: false,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,7 @@ impl From<Participant> for Record {
|
||||
id: 0, // This will be set by the database
|
||||
display_name: Some(display_name),
|
||||
is_me: false,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::models::participant::Participant;
|
||||
use chrono::{DateTime, NaiveDateTime};
|
||||
use kordophone::model::message::AttachmentMetadata;
|
||||
use kordophone::model::outgoing_message::OutgoingMessage;
|
||||
use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
use crate::models::participant::Participant;
|
||||
use kordophone::model::outgoing_message::OutgoingMessage;
|
||||
use kordophone::model::message::AttachmentMetadata;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Message {
|
||||
@@ -35,9 +35,7 @@ impl From<kordophone::model::Message> for Message {
|
||||
text: value.text,
|
||||
date: DateTime::from_timestamp(
|
||||
value.date.unix_timestamp(),
|
||||
value.date.unix_timestamp_nanos()
|
||||
.try_into()
|
||||
.unwrap_or(0),
|
||||
value.date.unix_timestamp_nanos().try_into().unwrap_or(0),
|
||||
)
|
||||
.unwrap()
|
||||
.naive_local(),
|
||||
@@ -107,7 +105,10 @@ impl MessageBuilder {
|
||||
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
|
||||
}
|
||||
@@ -123,4 +124,3 @@ impl MessageBuilder {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
pub mod conversation;
|
||||
pub mod participant;
|
||||
pub mod message;
|
||||
pub mod db;
|
||||
pub mod message;
|
||||
pub mod participant;
|
||||
|
||||
pub use conversation::Conversation;
|
||||
pub use participant::Participant;
|
||||
pub use message::Message;
|
||||
pub use participant::Participant;
|
||||
|
||||
@@ -4,16 +4,13 @@ use diesel::query_dsl::BelongingToDsl;
|
||||
|
||||
use crate::{
|
||||
models::{
|
||||
Conversation,
|
||||
Message,
|
||||
Participant,
|
||||
db::conversation::Record as ConversationRecord,
|
||||
db::participant::{
|
||||
ConversationParticipant,
|
||||
Record as ParticipantRecord,
|
||||
InsertableRecord as InsertableParticipantRecord
|
||||
},
|
||||
db::message::Record as MessageRecord,
|
||||
db::participant::{
|
||||
ConversationParticipant, InsertableRecord as InsertableParticipantRecord,
|
||||
Record as ParticipantRecord,
|
||||
},
|
||||
Conversation, Message, Participant,
|
||||
},
|
||||
schema,
|
||||
};
|
||||
@@ -28,9 +25,9 @@ impl<'a> Repository<'a> {
|
||||
}
|
||||
|
||||
pub fn insert_conversation(&mut self, conversation: Conversation) -> Result<()> {
|
||||
use crate::schema::conversation_participants::dsl::*;
|
||||
use crate::schema::conversations::dsl::*;
|
||||
use crate::schema::participants::dsl::*;
|
||||
use crate::schema::conversation_participants::dsl::*;
|
||||
|
||||
let (db_conversation, db_participants) = conversation.into();
|
||||
|
||||
@@ -76,7 +73,8 @@ impl<'a> Repository<'a> {
|
||||
.load::<ParticipantRecord>(self.connection)?;
|
||||
|
||||
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));
|
||||
}
|
||||
@@ -102,7 +100,8 @@ impl<'a> Repository<'a> {
|
||||
.load::<ParticipantRecord>(self.connection)?;
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -111,8 +110,8 @@ impl<'a> Repository<'a> {
|
||||
}
|
||||
|
||||
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::messages::dsl::*;
|
||||
|
||||
// Handle participant if message has a remote sender
|
||||
let sender = message.sender.clone();
|
||||
@@ -136,9 +135,13 @@ impl<'a> Repository<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn insert_messages(&mut self, conversation_guid: &str, in_messages: Vec<Message>) -> Result<()> {
|
||||
use crate::schema::messages::dsl::*;
|
||||
pub fn insert_messages(
|
||||
&mut self,
|
||||
conversation_guid: &str,
|
||||
in_messages: Vec<Message>,
|
||||
) -> Result<()> {
|
||||
use crate::schema::conversation_messages::dsl::*;
|
||||
use crate::schema::messages::dsl::*;
|
||||
|
||||
// Local insertable struct for the join table
|
||||
#[derive(Insertable)]
|
||||
@@ -154,7 +157,8 @@ impl<'a> Repository<'a> {
|
||||
|
||||
// Build the collections of insertable records
|
||||
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 {
|
||||
// Handle participant if message has a remote sender
|
||||
@@ -186,9 +190,12 @@ impl<'a> Repository<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_messages_for_conversation(&mut self, conversation_guid: &str) -> Result<Vec<Message>> {
|
||||
use crate::schema::messages::dsl::*;
|
||||
pub fn get_messages_for_conversation(
|
||||
&mut self,
|
||||
conversation_guid: &str,
|
||||
) -> Result<Vec<Message>> {
|
||||
use crate::schema::conversation_messages::dsl::*;
|
||||
use crate::schema::messages::dsl::*;
|
||||
use crate::schema::participants::dsl::*;
|
||||
|
||||
let message_records = conversation_messages
|
||||
@@ -216,9 +223,12 @@ impl<'a> Repository<'a> {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn get_last_message_for_conversation(&mut self, conversation_guid: &str) -> Result<Option<Message>> {
|
||||
use crate::schema::messages::dsl::*;
|
||||
pub fn get_last_message_for_conversation(
|
||||
&mut self,
|
||||
conversation_guid: &str,
|
||||
) -> Result<Option<Message>> {
|
||||
use crate::schema::conversation_messages::dsl::*;
|
||||
use crate::schema::messages::dsl::*;
|
||||
|
||||
let message_record = conversation_messages
|
||||
.filter(conversation_id.eq(conversation_guid))
|
||||
@@ -247,7 +257,11 @@ impl<'a> Repository<'a> {
|
||||
let conversation = self.get_conversation_by_guid(conversation_guid)?;
|
||||
if let Some(mut conversation) = conversation {
|
||||
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.last_message_preview = Some(last_message.text.clone());
|
||||
self.insert_conversation(conversation)?;
|
||||
@@ -261,14 +275,21 @@ impl<'a> Repository<'a> {
|
||||
// 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.
|
||||
fn last_insert_id(&mut self) -> Result<i32> {
|
||||
Ok(diesel::select(diesel::dsl::sql::<diesel::sql_types::Integer>("last_insert_rowid()"))
|
||||
.get_result(self.connection)?)
|
||||
Ok(
|
||||
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> {
|
||||
match participant {
|
||||
Participant::Me => None,
|
||||
Participant::Remote { display_name: p_name, .. } => {
|
||||
Participant::Remote {
|
||||
display_name: p_name,
|
||||
..
|
||||
} => {
|
||||
use crate::schema::participants::dsl::*;
|
||||
|
||||
let existing_participant = participants
|
||||
|
||||
@@ -53,7 +53,11 @@ diesel::table! {
|
||||
|
||||
diesel::joinable!(conversation_participants -> conversations (conversation_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 -> messages (message_id));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use diesel::*;
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use anyhow::Result;
|
||||
use diesel::*;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
|
||||
#[derive(Insertable, Queryable, AsChangeset)]
|
||||
#[diesel(table_name = crate::schema::settings)]
|
||||
@@ -18,16 +18,15 @@ impl<'a> Settings<'a> {
|
||||
Self { connection }
|
||||
}
|
||||
|
||||
pub fn put<T: Serialize>(
|
||||
&mut self,
|
||||
k: &str,
|
||||
v: &T,
|
||||
) -> Result<()> {
|
||||
pub fn put<T: Serialize>(&mut self, k: &str, v: &T) -> Result<()> {
|
||||
use crate::schema::settings::dsl::*;
|
||||
let bytes = bincode::serialize(v)?;
|
||||
|
||||
diesel::insert_into(settings)
|
||||
.values(SettingsRow { key: k, value: &bytes })
|
||||
.values(SettingsRow {
|
||||
key: k,
|
||||
value: &bytes,
|
||||
})
|
||||
.on_conflict(key)
|
||||
.do_update()
|
||||
.set(value.eq(&bytes))
|
||||
@@ -36,10 +35,7 @@ impl<'a> Settings<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get<T: DeserializeOwned>(
|
||||
&mut self,
|
||||
k: &str,
|
||||
) -> Result<Option<T>> {
|
||||
pub fn get<T: DeserializeOwned>(&mut self, k: &str) -> Result<Option<T>> {
|
||||
use crate::schema::settings::dsl::*;
|
||||
let blob: Option<Vec<u8>> = settings
|
||||
.select(value)
|
||||
@@ -60,12 +56,8 @@ impl<'a> Settings<'a> {
|
||||
|
||||
pub fn list_keys(&mut self) -> Result<Vec<String>> {
|
||||
use crate::schema::settings::dsl::*;
|
||||
let keys: Vec<String> = settings
|
||||
.select(key)
|
||||
.load(self.connection)?;
|
||||
let keys: Vec<String> = settings.select(key).load(self.connection)?;
|
||||
|
||||
Ok(keys)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ use crate::{
|
||||
database::{Database, DatabaseAccess},
|
||||
models::{
|
||||
conversation::{Conversation, ConversationBuilder},
|
||||
participant::Participant,
|
||||
message::Message,
|
||||
participant::Participant,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -11,9 +11,17 @@ use crate::{
|
||||
fn participants_equal_ignoring_id(a: &Participant, b: &Participant) -> bool {
|
||||
match (a, b) {
|
||||
(Participant::Me, Participant::Me) => true,
|
||||
(Participant::Remote { display_name: name_a, .. },
|
||||
Participant::Remote { display_name: name_b, .. }) => name_a == name_b,
|
||||
_ => false
|
||||
(
|
||||
Participant::Remote {
|
||||
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() {
|
||||
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]
|
||||
@@ -40,18 +50,23 @@ async fn test_add_conversation() {
|
||||
.display_name("Test Conversation")
|
||||
.build();
|
||||
|
||||
repository.insert_conversation(test_conversation.clone()).unwrap();
|
||||
repository
|
||||
.insert_conversation(test_conversation.clone())
|
||||
.unwrap();
|
||||
|
||||
// Try to fetch with id now
|
||||
let conversation = repository.get_conversation_by_guid(guid).unwrap().unwrap();
|
||||
assert_eq!(conversation.guid, "test");
|
||||
|
||||
// Modify the conversation and update it
|
||||
let modified_conversation = test_conversation.into_builder()
|
||||
let modified_conversation = test_conversation
|
||||
.into_builder()
|
||||
.display_name("Modified Conversation")
|
||||
.build();
|
||||
|
||||
repository.insert_conversation(modified_conversation.clone()).unwrap();
|
||||
repository
|
||||
.insert_conversation(modified_conversation.clone())
|
||||
.unwrap();
|
||||
|
||||
// Make sure we still only have one conversation.
|
||||
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
|
||||
let conversation = repository.get_conversation_by_guid(guid).unwrap().unwrap();
|
||||
assert_eq!(conversation.display_name.unwrap(), "Modified Conversation");
|
||||
}).await;
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -81,7 +97,10 @@ async fn test_conversation_participants() {
|
||||
let read_conversation = repository.get_conversation_by_guid(&guid).unwrap().unwrap();
|
||||
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
|
||||
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_participants: Vec<Participant> = read_conversation.participants;
|
||||
|
||||
assert!(participants_vec_equal_ignoring_id(&participants, &read_participants));
|
||||
}).await;
|
||||
assert!(participants_vec_equal_ignoring_id(
|
||||
&participants,
|
||||
&read_participants
|
||||
));
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[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 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(&conv2.participants, &participants2));
|
||||
}).await;
|
||||
assert!(participants_vec_equal_ignoring_id(
|
||||
&conv1.participants,
|
||||
&participants1
|
||||
));
|
||||
assert!(participants_vec_equal_ignoring_id(
|
||||
&conv2.participants,
|
||||
&participants2
|
||||
));
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -163,11 +193,17 @@ async fn test_messages() {
|
||||
.build();
|
||||
|
||||
// Insert both messages
|
||||
repository.insert_message(&conversation_id, message1.clone()).unwrap();
|
||||
repository.insert_message(&conversation_id, message2.clone()).unwrap();
|
||||
repository
|
||||
.insert_message(&conversation_id, message1.clone())
|
||||
.unwrap();
|
||||
repository
|
||||
.insert_message(&conversation_id, message2.clone())
|
||||
.unwrap();
|
||||
|
||||
// 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);
|
||||
|
||||
// Verify first message (from Me)
|
||||
@@ -181,9 +217,13 @@ async fn test_messages() {
|
||||
if let Participant::Remote { display_name, .. } = &retrieved_message2.sender {
|
||||
assert_eq!(display_name, "Alice");
|
||||
} else {
|
||||
panic!("Expected Remote participant. Got: {:?}", retrieved_message2.sender);
|
||||
panic!(
|
||||
"Expected Remote participant. Got: {:?}",
|
||||
retrieved_message2.sender
|
||||
);
|
||||
}
|
||||
}).await;
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -191,9 +231,7 @@ async fn test_message_ordering() {
|
||||
let mut db = Database::new_in_memory().unwrap();
|
||||
db.with_repository(|repository| {
|
||||
// Create a conversation
|
||||
let conversation = ConversationBuilder::new()
|
||||
.display_name("Test Chat")
|
||||
.build();
|
||||
let conversation = ConversationBuilder::new().display_name("Test Chat").build();
|
||||
let conversation_id = conversation.guid.clone();
|
||||
repository.insert_conversation(conversation).unwrap();
|
||||
|
||||
@@ -215,19 +253,28 @@ async fn test_message_ordering() {
|
||||
.build();
|
||||
|
||||
// Insert messages
|
||||
repository.insert_message(&conversation_id, message1).unwrap();
|
||||
repository.insert_message(&conversation_id, message2).unwrap();
|
||||
repository.insert_message(&conversation_id, message3).unwrap();
|
||||
repository
|
||||
.insert_message(&conversation_id, message1)
|
||||
.unwrap();
|
||||
repository
|
||||
.insert_message(&conversation_id, message2)
|
||||
.unwrap();
|
||||
repository
|
||||
.insert_message(&conversation_id, message3)
|
||||
.unwrap();
|
||||
|
||||
// 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);
|
||||
|
||||
// Messages should be ordered by date
|
||||
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]
|
||||
@@ -245,10 +292,7 @@ async fn test_insert_messages_batch() {
|
||||
|
||||
// Prepare a batch of messages with increasing timestamps
|
||||
let now = chrono::Utc::now().naive_utc();
|
||||
let message1 = Message::builder()
|
||||
.text("Hi".to_string())
|
||||
.date(now)
|
||||
.build();
|
||||
let message1 = Message::builder().text("Hi".to_string()).date(now).build();
|
||||
|
||||
let message2 = Message::builder()
|
||||
.text("Hello".to_string())
|
||||
@@ -280,7 +324,9 @@ async fn test_insert_messages_batch() {
|
||||
.unwrap();
|
||||
|
||||
// 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());
|
||||
|
||||
// Ensure ordering by date
|
||||
@@ -299,8 +345,14 @@ async fn test_insert_messages_batch() {
|
||||
match (&original.sender, &retrieved.sender) {
|
||||
(Participant::Me, Participant::Me) => {}
|
||||
(
|
||||
Participant::Remote { display_name: o_name, .. },
|
||||
Participant::Remote { display_name: r_name, .. },
|
||||
Participant::Remote {
|
||||
display_name: o_name,
|
||||
..
|
||||
},
|
||||
Participant::Remote {
|
||||
display_name: r_name,
|
||||
..
|
||||
},
|
||||
) => assert_eq!(o_name, r_name),
|
||||
_ => panic!(
|
||||
"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
|
||||
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);
|
||||
})
|
||||
.await;
|
||||
@@ -342,13 +397,34 @@ async fn test_settings() {
|
||||
};
|
||||
|
||||
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>
|
||||
settings.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>::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();
|
||||
assert_eq!(settings.get::<Option<String>>("test_struct_option").unwrap().unwrap(), Some("test".to_string()));
|
||||
}).await;
|
||||
settings
|
||||
.put(
|
||||
"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;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use async_trait::async_trait;
|
||||
use crate::model::update::UpdateItem;
|
||||
use crate::model::event::Event;
|
||||
use crate::model::update::UpdateItem;
|
||||
use async_trait::async_trait;
|
||||
use futures_util::stream::Stream;
|
||||
|
||||
#[async_trait]
|
||||
|
||||
@@ -85,7 +85,10 @@ impl ConversationBuilder {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -15,11 +15,23 @@ pub enum EventData {
|
||||
impl From<UpdateItem> for Event {
|
||||
fn from(update: UpdateItem) -> Self {
|
||||
match update {
|
||||
UpdateItem { conversation: Some(conversation), message: None, .. }
|
||||
=> Event { data: EventData::ConversationChanged(conversation), update_seq: update.seq },
|
||||
UpdateItem {
|
||||
conversation: Some(conversation),
|
||||
message: None,
|
||||
..
|
||||
} => Event {
|
||||
data: EventData::ConversationChanged(conversation),
|
||||
update_seq: update.seq,
|
||||
},
|
||||
|
||||
UpdateItem { conversation: Some(conversation), message: Some(message), .. }
|
||||
=> Event { data: EventData::MessageReceived(conversation, message), update_seq: update.seq },
|
||||
UpdateItem {
|
||||
conversation: Some(conversation),
|
||||
message: Some(message),
|
||||
..
|
||||
} => Event {
|
||||
data: EventData::MessageReceived(conversation, message),
|
||||
update_seq: update.seq,
|
||||
},
|
||||
|
||||
_ => panic!("Invalid update item: {:?}", update),
|
||||
}
|
||||
|
||||
@@ -82,7 +82,9 @@ impl JwtToken {
|
||||
let payload: JwtPayload = serde_json::from_slice(&payload)?;
|
||||
|
||||
// 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);
|
||||
|
||||
Ok(JwtToken {
|
||||
|
||||
@@ -100,7 +100,10 @@ impl MessageBuilder {
|
||||
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
|
||||
}
|
||||
@@ -116,4 +119,3 @@ impl MessageBuilder {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use serde::Serialize;
|
||||
use super::conversation::ConversationID;
|
||||
use chrono::NaiveDateTime;
|
||||
use serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use serde::Deserialize;
|
||||
use super::conversation::Conversation;
|
||||
use super::message::Message;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct UpdateItem {
|
||||
@@ -16,6 +16,10 @@ pub struct UpdateItem {
|
||||
|
||||
impl Default for UpdateItem {
|
||||
fn default() -> Self {
|
||||
Self { seq: 0, conversation: None, message: None }
|
||||
Self {
|
||||
seq: 0,
|
||||
conversation: None,
|
||||
message: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,9 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
use bytes::Bytes;
|
||||
use futures_util::stream::BoxStream;
|
||||
use futures_util::StreamExt;
|
||||
use bytes::Bytes;
|
||||
|
||||
pub struct TestClient {
|
||||
pub version: &'static str,
|
||||
@@ -120,7 +120,11 @@ impl APIInterface for TestClient {
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,14 +11,12 @@ fn main() {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let xml = std::fs::read_to_string(KORDOPHONE_XML)
|
||||
.expect("Error reading server dbus interface");
|
||||
let xml = std::fs::read_to_string(KORDOPHONE_XML).expect("Error reading server dbus interface");
|
||||
|
||||
let output = dbus_codegen::generate(&xml, &opts)
|
||||
.expect("Error generating server dbus interface");
|
||||
let output =
|
||||
dbus_codegen::generate(&xml, &opts).expect("Error generating server dbus interface");
|
||||
|
||||
std::fs::write(out_path, output)
|
||||
.expect("Error writing server dbus code");
|
||||
std::fs::write(out_path, output).expect("Error writing server dbus code");
|
||||
|
||||
println!("cargo:rerun-if-changed={}", KORDOPHONE_XML);
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ use crate::daemon::models::Attachment;
|
||||
use crate::daemon::Daemon;
|
||||
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use tokio::pin;
|
||||
|
||||
@@ -62,7 +62,10 @@ impl AttachmentStore {
|
||||
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();
|
||||
log::info!(target: target::ATTACHMENTS, "Attachment store path: {}", store_path.display());
|
||||
|
||||
|
||||
@@ -150,7 +150,8 @@ impl Daemon {
|
||||
}
|
||||
|
||||
// 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());
|
||||
tokio::spawn(async move {
|
||||
attachment_store.run().await;
|
||||
@@ -304,7 +305,10 @@ impl Daemon {
|
||||
self.attachment_store_sink
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.send(AttachmentStoreEvent::QueueDownloadAttachment(attachment_id, preview))
|
||||
.send(AttachmentStoreEvent::QueueDownloadAttachment(
|
||||
attachment_id,
|
||||
preview,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -20,12 +20,18 @@ pub struct Attachment {
|
||||
|
||||
impl Attachment {
|
||||
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 {
|
||||
std::fs::exists(&self.get_path(preview))
|
||||
.expect(format!("Wasn't able to check for the existence of an attachment file path at {}", &self.get_path(preview).display()).as_str())
|
||||
std::fs::exists(&self.get_path(preview)).expect(
|
||||
format!(
|
||||
"Wasn't able to check for the existence of an attachment file path at {}",
|
||||
&self.get_path(preview).display()
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use chrono::NaiveDateTime;
|
||||
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::outgoing_message::OutgoingMessage;
|
||||
use crate::daemon::models::Attachment;
|
||||
use crate::daemon::attachment_store::AttachmentStore;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Participant {
|
||||
@@ -63,13 +63,22 @@ pub struct Message {
|
||||
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
|
||||
.iter()
|
||||
.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 {
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -80,7 +89,8 @@ fn attachments_from(file_transfer_guids: &Vec<String>, attachment_metadata: &Opt
|
||||
|
||||
impl From<kordophone_db::models::Message> for Message {
|
||||
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 {
|
||||
id: message.id,
|
||||
sender: message.sender.into(),
|
||||
@@ -105,11 +115,21 @@ impl From<Message> for kordophone_db::models::Message {
|
||||
date: message.date,
|
||||
file_transfer_guids: message.attachments.iter().map(|a| a.guid.clone()).collect(),
|
||||
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()
|
||||
.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();
|
||||
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 {
|
||||
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 {
|
||||
id: message.guid,
|
||||
sender: match message.sender {
|
||||
@@ -130,9 +151,7 @@ impl From<kordophone::model::Message> for Message {
|
||||
text: message.text,
|
||||
date: DateTime::from_timestamp(
|
||||
message.date.unix_timestamp(),
|
||||
message.date.unix_timestamp_nanos()
|
||||
.try_into()
|
||||
.unwrap_or(0),
|
||||
message.date.unix_timestamp_nanos().try_into().unwrap_or(0),
|
||||
)
|
||||
.unwrap()
|
||||
.naive_local(),
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::time::Duration;
|
||||
|
||||
use tokio::sync::mpsc::{Sender, Receiver};
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use tokio::sync::Mutex;
|
||||
use tokio_condvar::Condvar;
|
||||
|
||||
use crate::daemon::events::Event as DaemonEvent;
|
||||
use kordophone::model::outgoing_message::OutgoingMessage;
|
||||
use kordophone::api::APIInterface;
|
||||
use kordophone::model::outgoing_message::OutgoingMessage;
|
||||
|
||||
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> {
|
||||
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 {
|
||||
event_source,
|
||||
event_sink,
|
||||
@@ -87,9 +91,8 @@ impl<C: APIInterface, F: AsyncFnMut() -> Result<C>> PostOffice<C, F> {
|
||||
async fn try_send_message(
|
||||
make_client: &mut F,
|
||||
event_sink: &Sender<DaemonEvent>,
|
||||
message: OutgoingMessage
|
||||
) -> Vec<OutgoingMessage>
|
||||
{
|
||||
message: OutgoingMessage,
|
||||
) -> Vec<OutgoingMessage> {
|
||||
let mut retry_messages = Vec::new();
|
||||
|
||||
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);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use kordophone_db::settings::Settings as DbSettings;
|
||||
use anyhow::Result;
|
||||
use kordophone_db::settings::Settings as DbSettings;
|
||||
|
||||
pub mod keys {
|
||||
pub static SERVER_URL: &str = "ServerURL";
|
||||
@@ -7,8 +7,7 @@ pub mod keys {
|
||||
pub static TOKEN: &str = "Token";
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Default)]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Settings {
|
||||
pub server_url: Option<String>,
|
||||
pub username: Option<String>,
|
||||
@@ -47,4 +46,3 @@ impl Settings {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
use crate::daemon::{
|
||||
Daemon,
|
||||
DaemonResult,
|
||||
|
||||
events::{Event, Reply},
|
||||
target,
|
||||
target, Daemon, DaemonResult,
|
||||
};
|
||||
|
||||
use kordophone::APIInterface;
|
||||
use kordophone::api::event_socket::EventSocket;
|
||||
use kordophone::model::event::Event as UpdateEvent;
|
||||
use kordophone::model::event::EventData as UpdateEventData;
|
||||
use kordophone::APIInterface;
|
||||
|
||||
use kordophone_db::database::Database;
|
||||
use kordophone_db::database::DatabaseAccess;
|
||||
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
pub struct UpdateMonitor {
|
||||
database: Arc<Mutex<Database>>,
|
||||
@@ -42,7 +39,8 @@ impl UpdateMonitor {
|
||||
make_event: impl FnOnce(Reply<T>) -> Event,
|
||||
) -> DaemonResult<T> {
|
||||
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
|
||||
.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).
|
||||
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) {
|
||||
(Some(message), Some(conversation_message)) => {
|
||||
if message.id == conversation_message.guid {
|
||||
@@ -80,10 +82,12 @@ impl UpdateMonitor {
|
||||
};
|
||||
|
||||
// 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);
|
||||
self.send_event(|r| Event::SyncConversation(conversation.guid, r)).await
|
||||
self.send_event(|r| Event::SyncConversation(conversation.guid, r))
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
log::error!("Failed to send daemon event: {}", e);
|
||||
});
|
||||
@@ -92,7 +96,8 @@ impl UpdateMonitor {
|
||||
UpdateEventData::MessageReceived(conversation, message) => {
|
||||
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);
|
||||
self.send_event(|r| Event::SyncConversation(conversation.guid, r)).await
|
||||
self.send_event(|r| Event::SyncConversation(conversation.guid, r))
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
log::error!("Failed to send daemon event: {}", e);
|
||||
});
|
||||
|
||||
@@ -7,7 +7,8 @@ use tokio::sync::oneshot;
|
||||
|
||||
use crate::daemon::{
|
||||
events::{Event, Reply},
|
||||
settings::Settings, DaemonResult,
|
||||
settings::Settings,
|
||||
DaemonResult,
|
||||
};
|
||||
|
||||
use crate::dbus::interface::NetBuzzertKordophoneRepository as DbusRepository;
|
||||
@@ -138,11 +139,15 @@ impl DbusRepository for ServerImpl {
|
||||
);
|
||||
|
||||
// Add attachments array
|
||||
let attachments: Vec<arg::PropMap> = msg.attachments
|
||||
let attachments: Vec<arg::PropMap> = msg
|
||||
.attachments
|
||||
.into_iter()
|
||||
.map(|attachment| {
|
||||
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
|
||||
let path = attachment.get_path(false);
|
||||
@@ -150,10 +155,24 @@ impl DbusRepository for ServerImpl {
|
||||
let downloaded = attachment.is_downloaded(false);
|
||||
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("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)));
|
||||
attachment_map.insert(
|
||||
"path".into(),
|
||||
arg::Variant(Box::new(path.to_string_lossy().to_string())),
|
||||
);
|
||||
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
|
||||
if let Some(ref metadata) = attachment.metadata {
|
||||
@@ -164,16 +183,28 @@ impl DbusRepository for ServerImpl {
|
||||
let mut attribution_map = arg::PropMap::new();
|
||||
|
||||
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 {
|
||||
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
|
||||
@@ -216,20 +247,21 @@ impl DbusRepository for ServerImpl {
|
||||
(
|
||||
// - path: string
|
||||
path.to_string_lossy().to_string(),
|
||||
|
||||
// - preview_path: string
|
||||
preview_path.to_string_lossy().to_string(),
|
||||
|
||||
// - downloaded: boolean
|
||||
downloaded,
|
||||
|
||||
// - preview_downloaded: boolean
|
||||
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
|
||||
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>
|
||||
where
|
||||
T: Send,
|
||||
|
||||
@@ -60,14 +60,12 @@ async fn main() {
|
||||
// Create and register server implementation
|
||||
let server = ServerImpl::new(daemon.event_sender.clone());
|
||||
|
||||
dbus_registry.register_object(
|
||||
interface::OBJECT_PATH,
|
||||
server,
|
||||
|cr| vec![
|
||||
dbus_registry.register_object(interface::OBJECT_PATH, server, |cr| {
|
||||
vec![
|
||||
interface::register_net_buzzert_kordophone_repository(cr),
|
||||
interface::register_net_buzzert_kordophone_settings(cr),
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
let mut signal_receiver = daemon.obtain_signal_receiver();
|
||||
tokio::spawn(async move {
|
||||
|
||||
@@ -10,14 +10,12 @@ fn main() {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let xml = std::fs::read_to_string(KORDOPHONE_XML)
|
||||
.expect("Error reading server dbus interface");
|
||||
let xml = std::fs::read_to_string(KORDOPHONE_XML).expect("Error reading server dbus interface");
|
||||
|
||||
let output = dbus_codegen::generate(&xml, &opts)
|
||||
.expect("Error generating client dbus interface");
|
||||
let output =
|
||||
dbus_codegen::generate(&xml, &opts).expect("Error generating client dbus interface");
|
||||
|
||||
std::fs::write(out_path, output)
|
||||
.expect("Error writing client dbus code");
|
||||
std::fs::write(out_path, output).expect("Error writing client dbus code");
|
||||
|
||||
println!("cargo:rerun-if-changed={}", KORDOPHONE_XML);
|
||||
}
|
||||
|
||||
@@ -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::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 clap::Subcommand;
|
||||
use crate::printers::{ConversationPrinter, MessagePrinter};
|
||||
use kordophone::model::event::EventData;
|
||||
use kordophone::model::outgoing_message::OutgoingMessage;
|
||||
|
||||
@@ -16,18 +16,18 @@ pub fn make_api_client_from_env() -> HTTPAPIClient<InMemoryAuthenticationStore>
|
||||
dotenv::dotenv().ok();
|
||||
|
||||
// read from env
|
||||
let base_url = std::env::var("KORDOPHONE_API_URL")
|
||||
.expect("KORDOPHONE_API_URL must be set");
|
||||
let base_url = std::env::var("KORDOPHONE_API_URL").expect("KORDOPHONE_API_URL must be set");
|
||||
|
||||
let credentials = Credentials {
|
||||
username: std::env::var("KORDOPHONE_USERNAME")
|
||||
.expect("KORDOPHONE_USERNAME must be set"),
|
||||
username: std::env::var("KORDOPHONE_USERNAME").expect("KORDOPHONE_USERNAME must be set"),
|
||||
|
||||
password: std::env::var("KORDOPHONE_PASSWORD")
|
||||
.expect("KORDOPHONE_PASSWORD must be set"),
|
||||
password: std::env::var("KORDOPHONE_PASSWORD").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)]
|
||||
@@ -36,9 +36,7 @@ pub enum Commands {
|
||||
Conversations,
|
||||
|
||||
/// Prints all messages in a conversation.
|
||||
Messages {
|
||||
conversation_id: String,
|
||||
},
|
||||
Messages { conversation_id: String },
|
||||
|
||||
/// Prints the server Kordophone version.
|
||||
Version,
|
||||
@@ -65,7 +63,10 @@ impl Commands {
|
||||
Commands::Messages { conversation_id } => client.print_messages(conversation_id).await,
|
||||
Commands::RawUpdates => client.print_raw_updates().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<()> {
|
||||
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 {
|
||||
println!("{}", MessagePrinter::new(&message.into()));
|
||||
}
|
||||
@@ -113,7 +117,10 @@ impl ClientCli {
|
||||
println!("Conversation changed: {}", conversation.guid);
|
||||
}
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::printers::{ConversationPrinter, MessagePrinter};
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use dbus::blocking::{Connection, Proxy};
|
||||
use prettytable::table;
|
||||
use crate::printers::{ConversationPrinter, MessagePrinter};
|
||||
|
||||
const DBUS_NAME: &str = "net.buzzert.kordophonecd";
|
||||
const DBUS_PATH: &str = "/net/buzzert/kordophonecd/daemon";
|
||||
@@ -21,9 +21,7 @@ pub enum Commands {
|
||||
Conversations,
|
||||
|
||||
/// Runs a full sync operation for a conversation and its messages.
|
||||
Sync {
|
||||
conversation_id: Option<String>,
|
||||
},
|
||||
Sync { conversation_id: Option<String> },
|
||||
|
||||
/// Runs a sync operation for the conversation list.
|
||||
SyncList,
|
||||
@@ -62,14 +60,10 @@ pub enum ConfigCommands {
|
||||
Print,
|
||||
|
||||
/// Sets the server URL.
|
||||
SetServerUrl {
|
||||
url: String,
|
||||
},
|
||||
SetServerUrl { url: String },
|
||||
|
||||
/// Sets the username.
|
||||
SetUsername {
|
||||
username: String,
|
||||
},
|
||||
SetUsername { username: String },
|
||||
}
|
||||
|
||||
impl Commands {
|
||||
@@ -82,9 +76,19 @@ impl Commands {
|
||||
Commands::SyncList => client.sync_conversations_list().await,
|
||||
Commands::Config { command } => client.config(command).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::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 {
|
||||
pub fn new() -> Result<Self> {
|
||||
Ok(Self {
|
||||
conn: Connection::new_session()?
|
||||
conn: Connection::new_session()?,
|
||||
})
|
||||
}
|
||||
|
||||
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<()> {
|
||||
@@ -136,8 +141,16 @@ impl DaemonCli {
|
||||
.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<()> {
|
||||
let messages = KordophoneRepository::get_messages(&self.proxy(), &conversation_id, &last_message_id.unwrap_or_default())?;
|
||||
pub async fn print_messages(
|
||||
&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());
|
||||
|
||||
for message in messages {
|
||||
@@ -147,8 +160,13 @@ impl DaemonCli {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn enqueue_outgoing_message(&mut self, conversation_id: String, text: String) -> Result<()> {
|
||||
let outgoing_message_id = KordophoneRepository::send_message(&self.proxy(), &conversation_id, &text)?;
|
||||
pub async fn enqueue_outgoing_message(
|
||||
&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);
|
||||
Ok(())
|
||||
}
|
||||
@@ -159,10 +177,12 @@ impl DaemonCli {
|
||||
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");
|
||||
true
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
println!("Waiting for signals...");
|
||||
loop {
|
||||
|
||||
@@ -3,27 +3,30 @@ use clap::Subcommand;
|
||||
use kordophone::APIInterface;
|
||||
use std::{env, path::PathBuf};
|
||||
|
||||
use crate::{
|
||||
client,
|
||||
printers::{ConversationPrinter, MessagePrinter},
|
||||
};
|
||||
use kordophone_db::database::{Database, DatabaseAccess};
|
||||
use crate::{client, printers::{ConversationPrinter, MessagePrinter}};
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Commands {
|
||||
/// For dealing with the table of cached conversations.
|
||||
Conversations {
|
||||
#[clap(subcommand)]
|
||||
command: ConversationCommands
|
||||
command: ConversationCommands,
|
||||
},
|
||||
|
||||
/// For dealing with the table of cached messages.
|
||||
Messages {
|
||||
#[clap(subcommand)]
|
||||
command: MessageCommands
|
||||
command: MessageCommands,
|
||||
},
|
||||
|
||||
/// For managing settings in the database.
|
||||
Settings {
|
||||
#[clap(subcommand)]
|
||||
command: SettingsCommands
|
||||
command: SettingsCommands,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -39,9 +42,7 @@ pub enum ConversationCommands {
|
||||
#[derive(Subcommand)]
|
||||
pub enum MessageCommands {
|
||||
/// Prints all messages in a conversation.
|
||||
List {
|
||||
conversation_id: String
|
||||
},
|
||||
List { conversation_id: String },
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
@@ -49,7 +50,7 @@ pub enum SettingsCommands {
|
||||
/// Lists all settings or gets a specific setting.
|
||||
Get {
|
||||
/// The key to get. If not provided, all settings will be listed.
|
||||
key: Option<String>
|
||||
key: Option<String>,
|
||||
},
|
||||
|
||||
/// Sets a setting value.
|
||||
@@ -76,7 +77,9 @@ impl Commands {
|
||||
ConversationCommands::Sync => db.sync_with_client().await,
|
||||
},
|
||||
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 {
|
||||
SettingsCommands::Get { key } => db.get_setting(key).await,
|
||||
@@ -88,15 +91,17 @@ impl Commands {
|
||||
}
|
||||
|
||||
struct DbClient {
|
||||
database: Database
|
||||
database: Database,
|
||||
}
|
||||
|
||||
impl DbClient {
|
||||
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();
|
||||
temp_dir.join("kpcli_chat.db").to_str().unwrap().to_string()
|
||||
}).into()
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn new() -> Result<Self> {
|
||||
@@ -106,13 +111,14 @@ impl DbClient {
|
||||
println!("kpcli: Using db at {}", path_str);
|
||||
|
||||
let db = Database::new(path_str)?;
|
||||
Ok( Self { database: db })
|
||||
Ok(Self { database: db })
|
||||
}
|
||||
|
||||
pub async fn print_conversations(&mut self) -> Result<()> {
|
||||
let all_conversations = self.database.with_repository(|repository| {
|
||||
repository.all_conversations(i32::MAX, 0)
|
||||
}).await?;
|
||||
let all_conversations = self
|
||||
.database
|
||||
.with_repository(|repository| repository.all_conversations(i32::MAX, 0))
|
||||
.await?;
|
||||
|
||||
println!("{} Conversations: ", all_conversations.len());
|
||||
for conversation in all_conversations {
|
||||
@@ -123,9 +129,10 @@ impl DbClient {
|
||||
}
|
||||
|
||||
pub async fn print_messages(&mut self, conversation_id: &str) -> Result<()> {
|
||||
let messages = self.database.with_repository(|repository| {
|
||||
repository.get_messages_for_conversation(conversation_id)
|
||||
}).await?;
|
||||
let messages = self
|
||||
.database
|
||||
.with_repository(|repository| repository.get_messages_for_conversation(conversation_id))
|
||||
.await?;
|
||||
|
||||
for message in messages {
|
||||
println!("{}", MessagePrinter::new(&message.into()));
|
||||
@@ -136,7 +143,8 @@ impl DbClient {
|
||||
pub async fn sync_with_client(&mut self) -> Result<()> {
|
||||
let mut client = client::make_api_client_from_env();
|
||||
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)
|
||||
.collect();
|
||||
|
||||
@@ -145,31 +153,37 @@ impl DbClient {
|
||||
let conversation_id = conversation.guid.clone();
|
||||
|
||||
// Insert the conversation
|
||||
self.database.with_repository(|repository| {
|
||||
repository.insert_conversation(conversation)
|
||||
}).await?;
|
||||
self.database
|
||||
.with_repository(|repository| repository.insert_conversation(conversation))
|
||||
.await?;
|
||||
|
||||
// Fetch and sync messages for this conversation
|
||||
let messages = client.get_messages(&conversation_id, None, None, None).await?;
|
||||
let db_messages: Vec<kordophone_db::models::Message> = messages.into_iter()
|
||||
let messages = client
|
||||
.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)
|
||||
.collect();
|
||||
|
||||
// Insert each message
|
||||
self.database.with_repository(|repository| -> Result<()> {
|
||||
self.database
|
||||
.with_repository(|repository| -> Result<()> {
|
||||
for message in db_messages {
|
||||
repository.insert_message(&conversation_id, message)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}).await?;
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_setting(&mut self, key: Option<String>) -> Result<()> {
|
||||
self.database.with_settings(|settings| {
|
||||
self.database
|
||||
.with_settings(|settings| {
|
||||
match key {
|
||||
Some(key) => {
|
||||
// Get a specific setting
|
||||
@@ -178,7 +192,7 @@ impl DbClient {
|
||||
Some(v) => println!("{} = {}", key, v),
|
||||
None => println!("Setting '{}' not found", key),
|
||||
}
|
||||
},
|
||||
}
|
||||
None => {
|
||||
// List all settings
|
||||
let keys = settings.list_keys()?;
|
||||
@@ -198,23 +212,28 @@ impl DbClient {
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}).await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
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)?;
|
||||
Ok(())
|
||||
}).await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
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)?;
|
||||
if count == 0 {
|
||||
println!("Setting '{}' not found", key);
|
||||
}
|
||||
Ok(())
|
||||
}).await
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
mod client;
|
||||
mod daemon;
|
||||
mod db;
|
||||
mod printers;
|
||||
mod daemon;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
@@ -33,7 +33,7 @@ enum Commands {
|
||||
Daemon {
|
||||
#[command(subcommand)]
|
||||
command: daemon::Commands,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
async fn run_command(command: Commands) -> Result<()> {
|
||||
@@ -62,7 +62,8 @@ async fn main() {
|
||||
|
||||
let cli = Cli::parse();
|
||||
|
||||
run_command(cli.command).await
|
||||
run_command(cli.command)
|
||||
.await
|
||||
.map_err(|e| println!("Error: {}", e))
|
||||
.err();
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::fmt::Display;
|
||||
use std::collections::HashMap;
|
||||
use time::OffsetDateTime;
|
||||
use pretty::RcDoc;
|
||||
use dbus::arg::{self, RefArg};
|
||||
use kordophone::model::message::AttachmentMetadata;
|
||||
use pretty::RcDoc;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
pub struct PrintableConversation {
|
||||
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(),
|
||||
unread_count: value.unread_count.into(),
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -44,17 +48,33 @@ impl From<arg::PropMap> for PrintableConversation {
|
||||
fn from(value: arg::PropMap) -> Self {
|
||||
Self {
|
||||
guid: value.get("guid").unwrap().as_str().unwrap().to_string(),
|
||||
date: OffsetDateTime::from_unix_timestamp(value.get("date").unwrap().as_i64().unwrap()).unwrap(),
|
||||
unread_count: value.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")
|
||||
date: OffsetDateTime::from_unix_timestamp(value.get("date").unwrap().as_i64().unwrap())
|
||||
.unwrap(),
|
||||
unread_count: value
|
||||
.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()
|
||||
.0
|
||||
.as_iter()
|
||||
.unwrap()
|
||||
.map(|s| s.as_str().unwrap().to_string())
|
||||
.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 {
|
||||
fn from(value: arg::PropMap) -> Self {
|
||||
// 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(|json_str| serde_json::from_str(json_str).ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
// 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(|json_str| serde_json::from_str(json_str).ok());
|
||||
|
||||
Self {
|
||||
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(),
|
||||
text: value.get("text").unwrap().as_str().unwrap().to_string(),
|
||||
file_transfer_guids,
|
||||
@@ -119,12 +142,13 @@ impl From<arg::PropMap> for PrintableMessage {
|
||||
}
|
||||
|
||||
pub struct ConversationPrinter<'a> {
|
||||
doc: RcDoc<'a, PrintableConversation>
|
||||
doc: RcDoc<'a, PrintableConversation>,
|
||||
}
|
||||
|
||||
impl<'a> ConversationPrinter<'a> {
|
||||
pub fn new(conversation: &'a PrintableConversation) -> Self {
|
||||
let preview = conversation.last_message_preview
|
||||
let preview = conversation
|
||||
.last_message_preview
|
||||
.as_deref()
|
||||
.unwrap_or("<null>")
|
||||
.replace('\n', " ");
|
||||
@@ -143,24 +167,22 @@ impl<'a> ConversationPrinter<'a> {
|
||||
.append(RcDoc::line())
|
||||
.append("Participants: ")
|
||||
.append("[")
|
||||
.append(RcDoc::line()
|
||||
.append(
|
||||
conversation.participants
|
||||
RcDoc::line()
|
||||
.append(
|
||||
conversation
|
||||
.participants
|
||||
.iter()
|
||||
.map(|name|
|
||||
RcDoc::text(name)
|
||||
.append(",")
|
||||
.append(RcDoc::line())
|
||||
.map(|name| RcDoc::text(name).append(",").append(RcDoc::line()))
|
||||
.fold(RcDoc::nil(), |acc, x| acc.append(x)),
|
||||
)
|
||||
.fold(RcDoc::nil(), |acc, x| acc.append(x))
|
||||
)
|
||||
.nest(4)
|
||||
.nest(4),
|
||||
)
|
||||
.append("]")
|
||||
.append(RcDoc::line())
|
||||
.append("Last Message Preview: ")
|
||||
.append(preview)
|
||||
.nest(4)
|
||||
.nest(4),
|
||||
)
|
||||
.append(RcDoc::line())
|
||||
.append(">");
|
||||
@@ -176,7 +198,7 @@ impl Display for ConversationPrinter<'_> {
|
||||
}
|
||||
|
||||
pub struct MessagePrinter<'a> {
|
||||
doc: RcDoc<'a, PrintableMessage>
|
||||
doc: RcDoc<'a, PrintableMessage>,
|
||||
}
|
||||
|
||||
impl Display for MessagePrinter<'_> {
|
||||
@@ -187,8 +209,7 @@ impl Display for MessagePrinter<'_> {
|
||||
|
||||
impl<'a> MessagePrinter<'a> {
|
||||
pub fn new(message: &'a PrintableMessage) -> Self {
|
||||
let mut doc = RcDoc::text(format!("<Message: \"{}\"", &message.guid))
|
||||
.append(
|
||||
let mut doc = RcDoc::text(format!("<Message: \"{}\"", &message.guid)).append(
|
||||
RcDoc::line()
|
||||
.append("Date: ")
|
||||
.append(message.date.to_string())
|
||||
@@ -198,26 +219,30 @@ impl<'a> MessagePrinter<'a> {
|
||||
.append(RcDoc::line())
|
||||
.append("Body: ")
|
||||
.append(&message.text)
|
||||
.nest(4)
|
||||
.nest(4),
|
||||
);
|
||||
|
||||
// Add file transfer GUIDs and attachment metadata if present
|
||||
if !message.file_transfer_guids.is_empty() {
|
||||
doc = doc.append(RcDoc::line())
|
||||
.append(
|
||||
doc = doc.append(RcDoc::line()).append(
|
||||
RcDoc::line()
|
||||
.append("Attachments:")
|
||||
.append(
|
||||
message.file_transfer_guids.iter().map(|guid| {
|
||||
let mut attachment_doc = RcDoc::line()
|
||||
.append("- ")
|
||||
.append(guid);
|
||||
message
|
||||
.file_transfer_guids
|
||||
.iter()
|
||||
.map(|guid| {
|
||||
let mut attachment_doc = RcDoc::line().append("- ").append(guid);
|
||||
|
||||
// Add metadata if available for this GUID
|
||||
if let Some(ref metadata) = message.attachment_metadata {
|
||||
if let Some(attachment_meta) = metadata.get(guid) {
|
||||
if let Some(ref attribution) = attachment_meta.attribution_info {
|
||||
if let (Some(width), Some(height)) = (attribution.width, attribution.height) {
|
||||
if let Some(ref attribution) =
|
||||
attachment_meta.attribution_info
|
||||
{
|
||||
if let (Some(width), Some(height)) =
|
||||
(attribution.width, attribution.height)
|
||||
{
|
||||
attachment_doc = attachment_doc
|
||||
.append(RcDoc::line())
|
||||
.append(" Dimensions: ")
|
||||
@@ -231,9 +256,9 @@ impl<'a> MessagePrinter<'a> {
|
||||
|
||||
attachment_doc
|
||||
})
|
||||
.fold(RcDoc::nil(), |acc, x| acc.append(x))
|
||||
.fold(RcDoc::nil(), |acc, x| acc.append(x)),
|
||||
)
|
||||
.nest(4)
|
||||
.nest(4),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user