diff --git a/Cargo.lock b/Cargo.lock index 2e4b394..61cc498 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1036,6 +1036,7 @@ dependencies = [ "kordophone", "log", "serde", + "serde_json", "time", "tokio", "uuid", @@ -1059,6 +1060,7 @@ dependencies = [ "kordophone", "kordophone-db", "log", + "serde_json", "thiserror 2.0.12", "tokio", "tokio-condvar", @@ -1082,6 +1084,7 @@ dependencies = [ "log", "pretty", "prettytable", + "serde_json", "time", "tokio", ] diff --git a/kordophone-db/Cargo.toml b/kordophone-db/Cargo.toml index fe8fd87..446eafe 100644 --- a/kordophone-db/Cargo.toml +++ b/kordophone-db/Cargo.toml @@ -13,6 +13,7 @@ diesel_migrations = { version = "2.2.0", features = ["sqlite"] } kordophone = { path = "../kordophone" } log = "0.4.27" serde = { version = "1.0.215", features = ["derive"] } +serde_json = "1.0" time = "0.3.37" tokio = "1.44.2" uuid = { version = "1.11.0", features = ["v4"] } diff --git a/kordophone-db/migrations/2025-05-26-232206_add_attachment_metadata_to_messages/down.sql b/kordophone-db/migrations/2025-05-26-232206_add_attachment_metadata_to_messages/down.sql new file mode 100644 index 0000000..722f7e8 --- /dev/null +++ b/kordophone-db/migrations/2025-05-26-232206_add_attachment_metadata_to_messages/down.sql @@ -0,0 +1,2 @@ +-- Remove attachment_metadata column from messages table +ALTER TABLE messages DROP COLUMN attachment_metadata; \ No newline at end of file diff --git a/kordophone-db/migrations/2025-05-26-232206_add_attachment_metadata_to_messages/up.sql b/kordophone-db/migrations/2025-05-26-232206_add_attachment_metadata_to_messages/up.sql new file mode 100644 index 0000000..50d4826 --- /dev/null +++ b/kordophone-db/migrations/2025-05-26-232206_add_attachment_metadata_to_messages/up.sql @@ -0,0 +1,2 @@ +-- Add attachment_metadata column to messages table +ALTER TABLE messages ADD COLUMN attachment_metadata TEXT; \ No newline at end of file diff --git a/kordophone-db/migrations/2025-05-26-234230_add_file_transfer_guids_to_messages/down.sql b/kordophone-db/migrations/2025-05-26-234230_add_file_transfer_guids_to_messages/down.sql new file mode 100644 index 0000000..d58fd05 --- /dev/null +++ b/kordophone-db/migrations/2025-05-26-234230_add_file_transfer_guids_to_messages/down.sql @@ -0,0 +1,2 @@ +-- Remove file_transfer_guids column from messages table +ALTER TABLE messages DROP COLUMN file_transfer_guids; \ No newline at end of file diff --git a/kordophone-db/migrations/2025-05-26-234230_add_file_transfer_guids_to_messages/up.sql b/kordophone-db/migrations/2025-05-26-234230_add_file_transfer_guids_to_messages/up.sql new file mode 100644 index 0000000..6a244b7 --- /dev/null +++ b/kordophone-db/migrations/2025-05-26-234230_add_file_transfer_guids_to_messages/up.sql @@ -0,0 +1,2 @@ +-- Add file_transfer_guids column to messages table +ALTER TABLE messages ADD COLUMN file_transfer_guids TEXT; \ No newline at end of file diff --git a/kordophone-db/src/models/db/message.rs b/kordophone-db/src/models/db/message.rs index 7f94262..736c28d 100644 --- a/kordophone-db/src/models/db/message.rs +++ b/kordophone-db/src/models/db/message.rs @@ -10,10 +10,21 @@ pub struct Record { pub sender_participant_id: Option, pub text: String, pub date: NaiveDateTime, + pub file_transfer_guids: Option, // JSON array + pub attachment_metadata: Option, // JSON string } impl From for Record { fn from(message: Message) -> Self { + let file_transfer_guids = if message.file_transfer_guids.is_empty() { + None + } else { + Some(serde_json::to_string(&message.file_transfer_guids).unwrap_or_default()) + }; + + let attachment_metadata = message.attachment_metadata + .map(|metadata| serde_json::to_string(&metadata).unwrap_or_default()); + Self { id: message.id, sender_participant_id: match message.sender { @@ -22,18 +33,29 @@ impl From for Record { }, text: message.text, date: message.date, + file_transfer_guids, + attachment_metadata, } } } impl From for Message { fn from(record: Record) -> Self { + let file_transfer_guids = record.file_transfer_guids + .and_then(|json| serde_json::from_str(&json).ok()) + .unwrap_or_default(); + + let attachment_metadata = record.attachment_metadata + .and_then(|json| serde_json::from_str(&json).ok()); + Self { id: record.id, // We'll set the proper sender later when loading participant info sender: Participant::Me, text: record.text, date: record.date, + file_transfer_guids, + attachment_metadata, } } } diff --git a/kordophone-db/src/models/message.rs b/kordophone-db/src/models/message.rs index 076ea52..100ffcc 100644 --- a/kordophone-db/src/models/message.rs +++ b/kordophone-db/src/models/message.rs @@ -1,7 +1,9 @@ use chrono::{DateTime, NaiveDateTime}; +use std::collections::HashMap; use uuid::Uuid; use crate::models::participant::Participant; use kordophone::model::outgoing_message::OutgoingMessage; +use kordophone::model::message::AttachmentMetadata; #[derive(Clone, Debug)] pub struct Message { @@ -9,6 +11,8 @@ pub struct Message { pub sender: Participant, pub text: String, pub date: NaiveDateTime, + pub file_transfer_guids: Vec, + pub attachment_metadata: Option>, } impl Message { @@ -36,7 +40,9 @@ impl From for Message { .unwrap_or(0), ) .unwrap() - .naive_local() + .naive_local(), + file_transfer_guids: value.file_transfer_guids, + attachment_metadata: value.attachment_metadata, } } } @@ -48,6 +54,8 @@ impl From<&OutgoingMessage> for Message { sender: Participant::Me, text: value.text.clone(), date: value.date, + file_transfer_guids: Vec::new(), // Outgoing messages don't have file transfer GUIDs initially + attachment_metadata: None, // Outgoing messages don't have attachment metadata initially } } } @@ -57,6 +65,8 @@ pub struct MessageBuilder { sender: Option, text: Option, date: Option, + file_transfer_guids: Option>, + attachment_metadata: Option>, } impl Default for MessageBuilder { @@ -72,6 +82,8 @@ impl MessageBuilder { sender: None, text: None, date: None, + file_transfer_guids: None, + attachment_metadata: None, } } @@ -90,12 +102,24 @@ impl MessageBuilder { self } + pub fn file_transfer_guids(mut self, file_transfer_guids: Vec) -> Self { + self.file_transfer_guids = Some(file_transfer_guids); + self + } + + pub fn attachment_metadata(mut self, attachment_metadata: HashMap) -> Self { + self.attachment_metadata = Some(attachment_metadata); + 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()), + file_transfer_guids: self.file_transfer_guids.unwrap_or_default(), + attachment_metadata: self.attachment_metadata, } } } diff --git a/kordophone-db/src/schema.rs b/kordophone-db/src/schema.rs index d77e401..1cf6f3a 100644 --- a/kordophone-db/src/schema.rs +++ b/kordophone-db/src/schema.rs @@ -31,7 +31,9 @@ diesel::table! { id -> Text, // guid text -> Text, sender_participant_id -> Nullable, - date -> Timestamp, + date -> Timestamp, + file_transfer_guids -> Nullable, // JSON array of file transfer GUIDs + attachment_metadata -> Nullable, // JSON string of attachment metadata } } diff --git a/kordophone/src/model/message.rs b/kordophone/src/model/message.rs index d226604..ef90efc 100644 --- a/kordophone/src/model/message.rs +++ b/kordophone/src/model/message.rs @@ -1,4 +1,5 @@ -use serde::Deserialize; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use time::OffsetDateTime; use uuid::Uuid; @@ -6,6 +7,23 @@ use super::Identifiable; pub type MessageID = ::ID; +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AttributionInfo { + /// Picture width + #[serde(rename = "pgensh")] + pub width: Option, + + /// Picture height + #[serde(rename = "pgensw")] + pub height: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AttachmentMetadata { + #[serde(rename = "attributionInfo")] + pub attribution_info: Option, +} + #[derive(Debug, Clone, Deserialize)] pub struct Message { pub guid: String, @@ -18,6 +36,14 @@ pub struct Message { #[serde(with = "time::serde::iso8601")] pub date: OffsetDateTime, + + /// Array of file transfer GUIDs for attachments + #[serde(rename = "fileTransferGUIDs", default)] + pub file_transfer_guids: Vec, + + /// Optional attachment metadata, keyed by attachment GUID + #[serde(rename = "attachmentMetadata")] + pub attachment_metadata: Option>, } impl Message { @@ -39,7 +65,9 @@ pub struct MessageBuilder { guid: Option, text: Option, sender: Option, - date: Option, + date: Option, + file_transfer_guids: Option>, + attachment_metadata: Option>, } impl MessageBuilder { @@ -67,12 +95,24 @@ impl MessageBuilder { self } + pub fn file_transfer_guids(mut self, file_transfer_guids: Vec) -> Self { + self.file_transfer_guids = Some(file_transfer_guids); + self + } + + pub fn attachment_metadata(mut self, attachment_metadata: HashMap) -> Self { + self.attachment_metadata = Some(attachment_metadata); + self + } + pub fn build(self) -> Message { Message { guid: self.guid.unwrap_or(Uuid::new_v4().to_string()), text: self.text.unwrap_or("".to_string()), sender: self.sender, date: self.date.unwrap_or(OffsetDateTime::now_utc()), + file_transfer_guids: self.file_transfer_guids.unwrap_or_default(), + attachment_metadata: self.attachment_metadata, } } } diff --git a/kordophoned/Cargo.toml b/kordophoned/Cargo.toml index 775a381..5a25583 100644 --- a/kordophoned/Cargo.toml +++ b/kordophoned/Cargo.toml @@ -17,6 +17,7 @@ keyring = { version = "3.6.2", features = ["sync-secret-service"] } kordophone = { path = "../kordophone" } kordophone-db = { path = "../kordophone-db" } log = "0.4.25" +serde_json = "1.0" thiserror = "2.0.12" tokio = { version = "1", features = ["full"] } tokio-condvar = "0.3.0" diff --git a/kordophoned/include/net.buzzert.kordophonecd.Server.xml b/kordophoned/include/net.buzzert.kordophonecd.Server.xml index 59ddae6..da4e231 100644 --- a/kordophoned/include/net.buzzert.kordophonecd.Server.xml +++ b/kordophoned/include/net.buzzert.kordophonecd.Server.xml @@ -58,7 +58,16 @@ - + + + diff --git a/kordophoned/src/dbus/server_impl.rs b/kordophoned/src/dbus/server_impl.rs index 3ac69e6..a953573 100644 --- a/kordophoned/src/dbus/server_impl.rs +++ b/kordophoned/src/dbus/server_impl.rs @@ -128,6 +128,7 @@ impl DbusRepository for ServerImpl { messages .into_iter() .map(|msg| { + let msg_id = msg.id.clone(); // Store ID for potential error logging let mut map = arg::PropMap::new(); map.insert("id".into(), arg::Variant(Box::new(msg.id))); map.insert("text".into(), arg::Variant(Box::new(msg.text))); @@ -139,6 +140,31 @@ impl DbusRepository for ServerImpl { "sender".into(), 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))); + } + Err(e) => { + log::warn!("Failed to serialize file transfer GUIDs for message {}: {}", msg_id, e); + } + } + } + + // 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 }) .collect() diff --git a/kpcli/Cargo.toml b/kpcli/Cargo.toml index cda9f57..c83e9c3 100644 --- a/kpcli/Cargo.toml +++ b/kpcli/Cargo.toml @@ -18,6 +18,7 @@ kordophone-db = { path = "../kordophone-db" } log = "0.4.22" pretty = { version = "0.12.3", features = ["termcolor"] } prettytable = "0.10.0" +serde_json = "1.0" time = "0.3.37" tokio = "1.41.1" diff --git a/kpcli/src/printers.rs b/kpcli/src/printers.rs index b3af6c2..f9e0c0a 100644 --- a/kpcli/src/printers.rs +++ b/kpcli/src/printers.rs @@ -1,7 +1,9 @@ use std::fmt::Display; +use std::collections::HashMap; use time::OffsetDateTime; use pretty::RcDoc; use dbus::arg::{self, RefArg}; +use kordophone::model::message::AttachmentMetadata; pub struct PrintableConversation { pub guid: String, @@ -62,6 +64,8 @@ pub struct PrintableMessage { pub date: OffsetDateTime, pub sender: String, pub text: String, + pub file_transfer_guids: Vec, + pub attachment_metadata: Option>, } impl From for PrintableMessage { @@ -71,6 +75,8 @@ impl From for PrintableMessage { date: value.date, sender: value.sender.unwrap_or("".to_string()), text: value.text, + file_transfer_guids: value.file_transfer_guids, + attachment_metadata: value.attachment_metadata, } } } @@ -82,17 +88,32 @@ impl From for PrintableMessage { date: OffsetDateTime::from_unix_timestamp(value.date.and_utc().timestamp()).unwrap(), sender: value.sender.display_name(), text: value.text, + file_transfer_guids: value.file_transfer_guids, + attachment_metadata: value.attachment_metadata, } } } impl From for PrintableMessage { fn from(value: arg::PropMap) -> Self { + // Parse file transfer GUIDs from JSON if present + let file_transfer_guids = value.get("file_transfer_guids") + .and_then(|v| v.as_str()) + .and_then(|json_str| serde_json::from_str(json_str).ok()) + .unwrap_or_default(); + + // Parse attachment metadata from JSON if present + let attachment_metadata = value.get("attachment_metadata") + .and_then(|v| v.as_str()) + .and_then(|json_str| serde_json::from_str(json_str).ok()); + Self { guid: value.get("id").unwrap().as_str().unwrap().to_string(), date: OffsetDateTime::from_unix_timestamp(value.get("date").unwrap().as_i64().unwrap()).unwrap(), sender: value.get("sender").unwrap().as_str().unwrap().to_string(), text: value.get("text").unwrap().as_str().unwrap().to_string(), + file_transfer_guids, + attachment_metadata, } } } @@ -166,21 +187,57 @@ impl Display for MessagePrinter<'_> { impl<'a> MessagePrinter<'a> { pub fn new(message: &'a PrintableMessage) -> Self { - let doc = RcDoc::text(format!(""); + let mut doc = RcDoc::text(format!(""); MessagePrinter { doc } }