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 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 })

View File

@@ -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(),

View File

@@ -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(),
)
}
}

View File

@@ -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 {
}
}
}

View File

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

View File

@@ -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,
}
},
}
}
}

View File

@@ -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 {
}
}
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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));

View File

@@ -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)
}
}

View File

@@ -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;
}

View File

@@ -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]

View File

@@ -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
}

View File

@@ -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),
}

View File

@@ -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 {

View File

@@ -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 {
}
}
}

View File

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

View File

@@ -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,
}
}
}

View File

@@ -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())
}
}

View File

@@ -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);
}

View File

@@ -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());

View File

@@ -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();

View File

@@ -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(),
)
}
}

View File

@@ -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(),

View File

@@ -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();
}

View File

@@ -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(())
}
}

View File

@@ -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);
});

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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);
}

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::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(())
}
}

View File

@@ -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 {

View File

@@ -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
}
}

View File

@@ -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();
}

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 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),
);
}