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