diff --git a/Cargo.lock b/Cargo.lock
index 61cc498..01619c7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1048,6 +1048,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
+ "chrono",
"dbus",
"dbus-codegen",
"dbus-crossroads",
diff --git a/kordophoned/Cargo.toml b/kordophoned/Cargo.toml
index 5a25583..1222916 100644
--- a/kordophoned/Cargo.toml
+++ b/kordophoned/Cargo.toml
@@ -6,6 +6,7 @@ edition = "2021"
[dependencies]
anyhow = "1.0.98"
async-trait = "0.1.88"
+chrono = "0.4.38"
dbus = "0.9.7"
dbus-crossroads = "0.5.2"
dbus-tokio = "0.7.6"
diff --git a/kordophoned/include/net.buzzert.kordophonecd.Server.xml b/kordophoned/include/net.buzzert.kordophonecd.Server.xml
index 1eeee32..d771c05 100644
--- a/kordophoned/include/net.buzzert.kordophonecd.Server.xml
+++ b/kordophoned/include/net.buzzert.kordophonecd.Server.xml
@@ -65,8 +65,16 @@
'text' (string): Message body text
'date' (int64): Message timestamp
'sender' (string): Sender display name
- 'file_transfer_guids' (string, optional): JSON array of file transfer GUIDs
- 'attachment_metadata' (string, optional): JSON string of attachment metadata"/>
+ 'attachments' (array of dictionaries): List of attachments
+ 'guid' (string): Attachment GUID
+ 'path' (string): Attachment path
+ 'preview_path' (string): Preview attachment path
+ 'downloaded' (boolean): Whether the attachment is downloaded
+ 'preview_downloaded' (boolean): Whether the preview is downloaded
+ 'metadata' (dictionary, optional): Attachment metadata
+ 'attribution_info' (dictionary, optional): Attribution info
+ 'width' (int32, optional): Width
+ 'height' (int32, optional): Height"/>
@@ -116,7 +124,7 @@
-
+
diff --git a/kordophoned/src/daemon/attachment_store.rs b/kordophoned/src/daemon/attachment_store.rs
index 429c19c..e0cd3a7 100644
--- a/kordophoned/src/daemon/attachment_store.rs
+++ b/kordophoned/src/daemon/attachment_store.rs
@@ -13,6 +13,7 @@ use kordophone_db::database::DatabaseAccess;
use crate::daemon::events::Event;
use crate::daemon::events::Reply;
+use crate::daemon::models::Attachment;
use crate::daemon::Daemon;
use std::sync::Arc;
@@ -26,23 +27,6 @@ mod target {
pub static ATTACHMENTS: &str = "attachments";
}
-#[derive(Debug, Clone)]
-pub struct Attachment {
- pub guid: String,
- pub base_path: PathBuf,
-}
-
-impl Attachment {
- pub fn get_path(&self, preview: bool) -> PathBuf {
- self.base_path.with_extension(if preview { "preview" } else { "full" })
- }
-
- pub fn is_downloaded(&self, preview: bool) -> bool {
- std::fs::exists(&self.get_path(preview))
- .expect(format!("Wasn't able to check for the existence of an attachment file path at {}", &self.get_path(preview).display()).as_str())
- }
-}
-
#[derive(Debug)]
pub enum AttachmentStoreEvent {
// Get the attachment info for a given attachment guid.
@@ -75,8 +59,13 @@ pub struct AttachmentStore {
}
impl AttachmentStore {
- pub fn new(data_dir: &PathBuf, database: Arc>, daemon_event_sink: Sender) -> AttachmentStore {
- let store_path = data_dir.join("attachments");
+ pub fn get_default_store_path() -> PathBuf {
+ let data_dir = Daemon::get_data_dir().expect("Unable to get data path");
+ data_dir.join("attachments")
+ }
+
+ pub fn new(database: Arc>, daemon_event_sink: Sender) -> AttachmentStore {
+ let store_path = Self::get_default_store_path();
log::info!(target: target::ATTACHMENTS, "Attachment store path: {}", store_path.display());
// Create the attachment store if it doesn't exist
@@ -98,11 +87,16 @@ impl AttachmentStore {
self.event_sink.take().unwrap()
}
- fn get_attachment(&self, guid: &String, preview: bool) -> Attachment {
- let base_path = self.store_path.join(guid);
+ fn get_attachment(&self, guid: &String) -> Attachment {
+ Self::get_attachment_impl(&self.store_path, guid)
+ }
+
+ pub fn get_attachment_impl(store_path: &PathBuf, guid: &String) -> Attachment {
+ let base_path = store_path.join(guid);
Attachment {
guid: guid.to_owned(),
base_path: base_path,
+ metadata: None,
}
}
@@ -147,7 +141,7 @@ impl AttachmentStore {
match event {
AttachmentStoreEvent::QueueDownloadAttachment(guid, preview) => {
- let attachment = self.get_attachment(&guid, preview);
+ let attachment = self.get_attachment(&guid);
if !attachment.is_downloaded(preview) {
self.download_attachment(&attachment, preview).await.unwrap_or_else(|e| {
log::error!(target: target::ATTACHMENTS, "Error downloading attachment: {}", e);
@@ -158,7 +152,7 @@ impl AttachmentStore {
}
AttachmentStoreEvent::GetAttachmentInfo(guid, reply) => {
- let attachment = self.get_attachment(&guid, false);
+ let attachment = self.get_attachment(&guid);
reply.send(attachment).unwrap();
}
}
diff --git a/kordophoned/src/daemon/events.rs b/kordophoned/src/daemon/events.rs
index 2e11ff2..627461e 100644
--- a/kordophoned/src/daemon/events.rs
+++ b/kordophoned/src/daemon/events.rs
@@ -3,10 +3,10 @@ use uuid::Uuid;
use kordophone::model::ConversationID;
use kordophone::model::OutgoingMessage;
-use kordophone_db::models::{Conversation, Message};
+use kordophone_db::models::Conversation;
use crate::daemon::settings::Settings;
-use crate::daemon::Attachment;
+use crate::daemon::{Attachment, Message};
pub type Reply = oneshot::Sender;
diff --git a/kordophoned/src/daemon/mod.rs b/kordophoned/src/daemon/mod.rs
index 4c444e4..c735034 100644
--- a/kordophoned/src/daemon/mod.rs
+++ b/kordophoned/src/daemon/mod.rs
@@ -23,7 +23,7 @@ use uuid::Uuid;
use kordophone_db::{
database::{Database, DatabaseAccess},
- models::{Conversation, Message},
+ models::Conversation,
};
use kordophone::api::http_client::HTTPAPIClient;
@@ -41,8 +41,11 @@ mod post_office;
use post_office::Event as PostOfficeEvent;
use post_office::PostOffice;
+mod models;
+pub use models::Attachment;
+pub use models::Message;
+
mod attachment_store;
-pub use attachment_store::Attachment;
pub use attachment_store::AttachmentStore;
pub use attachment_store::AttachmentStoreEvent;
@@ -147,8 +150,7 @@ impl Daemon {
}
// Attachment store
- let data_path = Self::get_data_dir().expect("Unable to get data path");
- let mut attachment_store = AttachmentStore::new(&data_path, self.database.clone(), self.event_sender.clone());
+ let mut attachment_store = AttachmentStore::new(self.database.clone(), self.event_sender.clone());
self.attachment_store_sink = Some(attachment_store.get_event_sink());
tokio::spawn(async move {
attachment_store.run().await;
@@ -270,7 +272,7 @@ impl Daemon {
self.database
.lock()
.await
- .with_repository(|r| r.insert_message(&conversation_id, message))
+ .with_repository(|r| r.insert_message(&conversation_id, message.into()))
.await
.unwrap();
@@ -355,6 +357,7 @@ impl Daemon {
r.get_messages_for_conversation(&conversation_id)
.unwrap()
.into_iter()
+ .map(|m| m.into()) // Convert db::Message to daemon::Message
.chain(outgoing_messages.into_iter().map(|m| m.into()))
.collect()
})
diff --git a/kordophoned/src/daemon/models/attachment.rs b/kordophoned/src/daemon/models/attachment.rs
new file mode 100644
index 0000000..51ab0c9
--- /dev/null
+++ b/kordophoned/src/daemon/models/attachment.rs
@@ -0,0 +1,64 @@
+use std::path::PathBuf;
+
+#[derive(Debug, Clone)]
+pub struct AttachmentMetadata {
+ pub attribution_info: Option,
+}
+
+#[derive(Debug, Clone)]
+pub struct AttributionInfo {
+ pub width: Option,
+ pub height: Option,
+}
+
+#[derive(Debug, Clone)]
+pub struct Attachment {
+ pub guid: String,
+ pub base_path: PathBuf,
+ pub metadata: Option,
+}
+
+impl Attachment {
+ pub fn get_path(&self, preview: bool) -> PathBuf {
+ self.base_path.with_extension(if preview { "preview" } else { "full" })
+ }
+
+ pub fn is_downloaded(&self, preview: bool) -> bool {
+ std::fs::exists(&self.get_path(preview))
+ .expect(format!("Wasn't able to check for the existence of an attachment file path at {}", &self.get_path(preview).display()).as_str())
+ }
+}
+
+impl From for AttachmentMetadata {
+ fn from(metadata: kordophone::model::message::AttachmentMetadata) -> Self {
+ Self {
+ attribution_info: metadata.attribution_info.map(|info| info.into()),
+ }
+ }
+}
+
+impl From for AttributionInfo {
+ fn from(info: kordophone::model::message::AttributionInfo) -> Self {
+ Self {
+ width: info.width,
+ height: info.height,
+ }
+ }
+}
+
+impl From for kordophone::model::message::AttachmentMetadata {
+ fn from(metadata: AttachmentMetadata) -> Self {
+ Self {
+ attribution_info: metadata.attribution_info.map(|info| info.into()),
+ }
+ }
+}
+
+impl From for kordophone::model::message::AttributionInfo {
+ fn from(info: AttributionInfo) -> Self {
+ Self {
+ width: info.width,
+ height: info.height,
+ }
+ }
+}
\ No newline at end of file
diff --git a/kordophoned/src/daemon/models/message.rs b/kordophoned/src/daemon/models/message.rs
new file mode 100644
index 0000000..da40f80
--- /dev/null
+++ b/kordophoned/src/daemon/models/message.rs
@@ -0,0 +1,217 @@
+use chrono::NaiveDateTime;
+use chrono::DateTime;
+
+use std::collections::HashMap;
+use uuid::Uuid;
+use kordophone::model::message::AttachmentMetadata;
+use kordophone::model::outgoing_message::OutgoingMessage;
+use crate::daemon::models::Attachment;
+use crate::daemon::attachment_store::AttachmentStore;
+
+#[derive(Clone, Debug)]
+pub enum Participant {
+ Me,
+ Remote {
+ id: Option,
+ display_name: String,
+ },
+}
+
+impl From for Participant {
+ fn from(display_name: String) -> Self {
+ Participant::Remote {
+ id: None,
+ display_name,
+ }
+ }
+}
+
+impl From<&str> for Participant {
+ fn from(display_name: &str) -> Self {
+ Participant::Remote {
+ id: None,
+ display_name: display_name.to_string(),
+ }
+ }
+}
+
+impl From for Participant {
+ fn from(participant: kordophone_db::models::Participant) -> Self {
+ match participant {
+ kordophone_db::models::Participant::Me => Participant::Me,
+ kordophone_db::models::Participant::Remote { id, display_name } => {
+ Participant::Remote { id, display_name }
+ }
+ }
+ }
+}
+
+impl Participant {
+ pub fn display_name(&self) -> String {
+ match self {
+ Participant::Me => "(Me)".to_string(),
+ Participant::Remote { display_name, .. } => display_name.clone(),
+ }
+ }
+}
+
+#[derive(Clone, Debug)]
+pub struct Message {
+ pub id: String,
+ pub sender: Participant,
+ pub text: String,
+ pub date: NaiveDateTime,
+ pub attachments: Vec,
+}
+
+impl Message {
+ pub fn builder() -> MessageBuilder {
+ MessageBuilder::new()
+ }
+}
+
+fn attachments_from(file_transfer_guids: &Vec, attachment_metadata: &Option>) -> Vec {
+ file_transfer_guids
+ .iter()
+ .map(|guid| {
+ let mut attachment = AttachmentStore::get_attachment_impl(&AttachmentStore::get_default_store_path(), guid);
+ attachment.metadata = match attachment_metadata {
+ Some(attachment_metadata) => attachment_metadata.get(guid).cloned().map(|metadata| metadata.into()),
+ None => None,
+ };
+
+ attachment
+ })
+ .collect()
+}
+
+impl From for Message {
+ fn from(message: kordophone_db::models::Message) -> Self {
+ let attachments = attachments_from(&message.file_transfer_guids, &message.attachment_metadata);
+ Self {
+ id: message.id,
+ sender: message.sender.into(),
+ text: message.text,
+ date: message.date,
+ attachments,
+ }
+ }
+}
+
+impl From for kordophone_db::models::Message {
+ fn from(message: Message) -> Self {
+ Self {
+ id: message.id,
+ sender: match message.sender {
+ Participant::Me => kordophone_db::models::Participant::Me,
+ Participant::Remote { id, display_name } => {
+ kordophone_db::models::Participant::Remote { id, display_name }
+ }
+ },
+ text: message.text,
+ date: message.date,
+ file_transfer_guids: message.attachments.iter().map(|a| a.guid.clone()).collect(),
+ attachment_metadata: {
+ let metadata_map: HashMap = message.attachments
+ .iter()
+ .filter_map(|a| a.metadata.as_ref().map(|m| (a.guid.clone(), m.clone().into())))
+ .collect();
+ if metadata_map.is_empty() { None } else { Some(metadata_map) }
+ },
+ }
+ }
+}
+
+impl From for Message {
+ fn from(message: kordophone::model::Message) -> Self {
+ let attachments = attachments_from(&message.file_transfer_guids, &message.attachment_metadata);
+ Self {
+ id: message.guid,
+ sender: match message.sender {
+ Some(sender) => Participant::Remote {
+ id: None,
+ display_name: sender,
+ },
+ None => Participant::Me,
+ },
+ text: message.text,
+ date: DateTime::from_timestamp(
+ message.date.unix_timestamp(),
+ message.date.unix_timestamp_nanos()
+ .try_into()
+ .unwrap_or(0),
+ )
+ .unwrap()
+ .naive_local(),
+ attachments,
+ }
+ }
+}
+
+impl From<&OutgoingMessage> for Message {
+ fn from(value: &OutgoingMessage) -> Self {
+ Self {
+ id: value.guid.to_string(),
+ sender: Participant::Me,
+ text: value.text.clone(),
+ date: value.date,
+ attachments: Vec::new(), // Outgoing messages don't have attachments initially
+ }
+ }
+}
+
+pub struct MessageBuilder {
+ id: Option,
+ sender: Option,
+ text: Option,
+ date: Option,
+ attachments: Vec,
+}
+
+impl Default for MessageBuilder {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl MessageBuilder {
+ pub fn new() -> Self {
+ Self {
+ id: None,
+ sender: None,
+ text: None,
+ date: None,
+ attachments: Vec::new(),
+ }
+ }
+
+ pub fn sender(mut self, sender: Participant) -> Self {
+ self.sender = Some(sender);
+ self
+ }
+
+ pub fn text(mut self, text: String) -> Self {
+ self.text = Some(text);
+ self
+ }
+
+ pub fn date(mut self, date: NaiveDateTime) -> Self {
+ self.date = Some(date);
+ self
+ }
+
+ pub fn attachments(mut self, attachments: Vec) -> Self {
+ self.attachments = attachments;
+ self
+ }
+
+ pub fn build(self) -> Message {
+ Message {
+ id: self.id.unwrap_or_else(|| Uuid::new_v4().to_string()),
+ sender: self.sender.unwrap_or(Participant::Me),
+ text: self.text.unwrap_or_default(),
+ date: self.date.unwrap_or_else(|| chrono::Utc::now().naive_utc()),
+ attachments: self.attachments,
+ }
+ }
+}
\ No newline at end of file
diff --git a/kordophoned/src/daemon/models/mod.rs b/kordophoned/src/daemon/models/mod.rs
new file mode 100644
index 0000000..8a63c4a
--- /dev/null
+++ b/kordophoned/src/daemon/models/mod.rs
@@ -0,0 +1,5 @@
+pub mod attachment;
+pub mod message;
+
+pub use attachment::Attachment;
+pub use message::Message;
\ No newline at end of file
diff --git a/kordophoned/src/dbus/server_impl.rs b/kordophoned/src/dbus/server_impl.rs
index 2e7155f..f66f676 100644
--- a/kordophoned/src/dbus/server_impl.rs
+++ b/kordophoned/src/dbus/server_impl.rs
@@ -138,29 +138,50 @@ impl DbusRepository for ServerImpl {
arg::Variant(Box::new(msg.sender.display_name())),
);
- // Add file transfer GUIDs if present
- if !msg.file_transfer_guids.is_empty() {
- match serde_json::to_string(&msg.file_transfer_guids) {
- Ok(json_str) => {
- map.insert("file_transfer_guids".into(), arg::Variant(Box::new(json_str)));
+ // Add attachments array
+ let attachments: Vec = msg.attachments
+ .into_iter()
+ .map(|attachment| {
+ let mut attachment_map = arg::PropMap::new();
+ attachment_map.insert("guid".into(), arg::Variant(Box::new(attachment.guid.clone())));
+
+ // Get attachment paths and download status
+ let path = attachment.get_path(false);
+ let preview_path = attachment.get_path(true);
+ let downloaded = attachment.is_downloaded(false);
+ let preview_downloaded = attachment.is_downloaded(true);
+
+ attachment_map.insert("path".into(), arg::Variant(Box::new(path.to_string_lossy().to_string())));
+ attachment_map.insert("preview_path".into(), arg::Variant(Box::new(preview_path.to_string_lossy().to_string())));
+ attachment_map.insert("downloaded".into(), arg::Variant(Box::new(downloaded)));
+ attachment_map.insert("preview_downloaded".into(), arg::Variant(Box::new(preview_downloaded)));
+
+ // Add metadata if present
+ if let Some(ref metadata) = attachment.metadata {
+ let mut metadata_map = arg::PropMap::new();
+
+ // Add attribution_info if present
+ if let Some(ref attribution_info) = metadata.attribution_info {
+ let mut attribution_map = arg::PropMap::new();
+
+ if let Some(width) = attribution_info.width {
+ attribution_map.insert("width".into(), arg::Variant(Box::new(width as i32)));
+ }
+ if let Some(height) = attribution_info.height {
+ attribution_map.insert("height".into(), arg::Variant(Box::new(height as i32)));
+ }
+
+ metadata_map.insert("attribution_info".into(), arg::Variant(Box::new(attribution_map)));
+ }
+
+ attachment_map.insert("metadata".into(), arg::Variant(Box::new(metadata_map)));
}
- Err(e) => {
- log::warn!("Failed to serialize file transfer GUIDs for message {}: {}", msg_id, e);
- }
- }
- }
+
+ attachment_map
+ })
+ .collect();
- // Add attachment metadata if present
- if let Some(ref attachment_metadata) = msg.attachment_metadata {
- match serde_json::to_string(attachment_metadata) {
- Ok(json_str) => {
- map.insert("attachment_metadata".into(), arg::Variant(Box::new(json_str)));
- }
- Err(e) => {
- log::warn!("Failed to serialize attachment metadata for message {}: {}", msg_id, e);
- }
- }
- }
+ map.insert("attachments".into(), arg::Variant(Box::new(attachments)));
map
})