Private
Public Access
1
0

plumb all known attachments via dbus if known

This commit is contained in:
2025-06-06 16:28:29 -07:00
parent 2e55f3ac9e
commit 77e1078d6a
10 changed files with 368 additions and 54 deletions

1
Cargo.lock generated
View File

@@ -1048,6 +1048,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
"chrono",
"dbus", "dbus",
"dbus-codegen", "dbus-codegen",
"dbus-crossroads", "dbus-crossroads",

View File

@@ -6,6 +6,7 @@ edition = "2021"
[dependencies] [dependencies]
anyhow = "1.0.98" anyhow = "1.0.98"
async-trait = "0.1.88" async-trait = "0.1.88"
chrono = "0.4.38"
dbus = "0.9.7" dbus = "0.9.7"
dbus-crossroads = "0.5.2" dbus-crossroads = "0.5.2"
dbus-tokio = "0.7.6" dbus-tokio = "0.7.6"

View File

@@ -65,8 +65,16 @@
'text' (string): Message body text 'text' (string): Message body text
'date' (int64): Message timestamp 'date' (int64): Message timestamp
'sender' (string): Sender display name 'sender' (string): Sender display name
'file_transfer_guids' (string, optional): JSON array of file transfer GUIDs 'attachments' (array of dictionaries): List of attachments
'attachment_metadata' (string, optional): JSON string of attachment metadata"/> '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"/>
</arg> </arg>
</method> </method>
@@ -116,7 +124,7 @@
<arg type="s" name="attachment_id"/> <arg type="s" name="attachment_id"/>
<arg type="s" name="file_path"/> <arg type="s" name="file_path"/>
<arg type="b" name="preview"/> <arg type="b" name="preview"/>
<annotation name="org.freedesktop.DBus.DocString" <annotation name="org.freedesktop.DBus.DocString"
value="Emitted when an attachment download completes successfully."/> value="Emitted when an attachment download completes successfully."/>
</signal> </signal>

View File

@@ -13,6 +13,7 @@ use kordophone_db::database::DatabaseAccess;
use crate::daemon::events::Event; use crate::daemon::events::Event;
use crate::daemon::events::Reply; use crate::daemon::events::Reply;
use crate::daemon::models::Attachment;
use crate::daemon::Daemon; use crate::daemon::Daemon;
use std::sync::Arc; use std::sync::Arc;
@@ -26,23 +27,6 @@ mod target {
pub static ATTACHMENTS: &str = "attachments"; 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)] #[derive(Debug)]
pub enum AttachmentStoreEvent { pub enum AttachmentStoreEvent {
// Get the attachment info for a given attachment guid. // Get the attachment info for a given attachment guid.
@@ -75,8 +59,13 @@ pub struct AttachmentStore {
} }
impl AttachmentStore { impl AttachmentStore {
pub fn new(data_dir: &PathBuf, database: Arc<Mutex<Database>>, daemon_event_sink: Sender<Event>) -> AttachmentStore { pub fn get_default_store_path() -> PathBuf {
let store_path = data_dir.join("attachments"); let data_dir = Daemon::get_data_dir().expect("Unable to get data path");
data_dir.join("attachments")
}
pub fn new(database: Arc<Mutex<Database>>, daemon_event_sink: Sender<Event>) -> AttachmentStore {
let store_path = Self::get_default_store_path();
log::info!(target: target::ATTACHMENTS, "Attachment store path: {}", store_path.display()); log::info!(target: target::ATTACHMENTS, "Attachment store path: {}", store_path.display());
// Create the attachment store if it doesn't exist // Create the attachment store if it doesn't exist
@@ -98,11 +87,16 @@ impl AttachmentStore {
self.event_sink.take().unwrap() self.event_sink.take().unwrap()
} }
fn get_attachment(&self, guid: &String, preview: bool) -> Attachment { fn get_attachment(&self, guid: &String) -> Attachment {
let base_path = self.store_path.join(guid); 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 { Attachment {
guid: guid.to_owned(), guid: guid.to_owned(),
base_path: base_path, base_path: base_path,
metadata: None,
} }
} }
@@ -147,7 +141,7 @@ impl AttachmentStore {
match event { match event {
AttachmentStoreEvent::QueueDownloadAttachment(guid, preview) => { AttachmentStoreEvent::QueueDownloadAttachment(guid, preview) => {
let attachment = self.get_attachment(&guid, preview); let attachment = self.get_attachment(&guid);
if !attachment.is_downloaded(preview) { if !attachment.is_downloaded(preview) {
self.download_attachment(&attachment, preview).await.unwrap_or_else(|e| { self.download_attachment(&attachment, preview).await.unwrap_or_else(|e| {
log::error!(target: target::ATTACHMENTS, "Error downloading attachment: {}", e); log::error!(target: target::ATTACHMENTS, "Error downloading attachment: {}", e);
@@ -158,7 +152,7 @@ impl AttachmentStore {
} }
AttachmentStoreEvent::GetAttachmentInfo(guid, reply) => { AttachmentStoreEvent::GetAttachmentInfo(guid, reply) => {
let attachment = self.get_attachment(&guid, false); let attachment = self.get_attachment(&guid);
reply.send(attachment).unwrap(); reply.send(attachment).unwrap();
} }
} }

View File

@@ -3,10 +3,10 @@ use uuid::Uuid;
use kordophone::model::ConversationID; use kordophone::model::ConversationID;
use kordophone::model::OutgoingMessage; use kordophone::model::OutgoingMessage;
use kordophone_db::models::{Conversation, Message}; use kordophone_db::models::Conversation;
use crate::daemon::settings::Settings; use crate::daemon::settings::Settings;
use crate::daemon::Attachment; use crate::daemon::{Attachment, Message};
pub type Reply<T> = oneshot::Sender<T>; pub type Reply<T> = oneshot::Sender<T>;

View File

@@ -23,7 +23,7 @@ use uuid::Uuid;
use kordophone_db::{ use kordophone_db::{
database::{Database, DatabaseAccess}, database::{Database, DatabaseAccess},
models::{Conversation, Message}, models::Conversation,
}; };
use kordophone::api::http_client::HTTPAPIClient; use kordophone::api::http_client::HTTPAPIClient;
@@ -41,8 +41,11 @@ mod post_office;
use post_office::Event as PostOfficeEvent; use post_office::Event as PostOfficeEvent;
use post_office::PostOffice; use post_office::PostOffice;
mod models;
pub use models::Attachment;
pub use models::Message;
mod attachment_store; mod attachment_store;
pub use attachment_store::Attachment;
pub use attachment_store::AttachmentStore; pub use attachment_store::AttachmentStore;
pub use attachment_store::AttachmentStoreEvent; pub use attachment_store::AttachmentStoreEvent;
@@ -147,8 +150,7 @@ impl Daemon {
} }
// Attachment store // Attachment store
let data_path = Self::get_data_dir().expect("Unable to get data path"); let mut attachment_store = AttachmentStore::new(self.database.clone(), self.event_sender.clone());
let mut attachment_store = AttachmentStore::new(&data_path, 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;
@@ -270,7 +272,7 @@ impl Daemon {
self.database self.database
.lock() .lock()
.await .await
.with_repository(|r| r.insert_message(&conversation_id, message)) .with_repository(|r| r.insert_message(&conversation_id, message.into()))
.await .await
.unwrap(); .unwrap();
@@ -355,6 +357,7 @@ impl Daemon {
r.get_messages_for_conversation(&conversation_id) r.get_messages_for_conversation(&conversation_id)
.unwrap() .unwrap()
.into_iter() .into_iter()
.map(|m| m.into()) // Convert db::Message to daemon::Message
.chain(outgoing_messages.into_iter().map(|m| m.into())) .chain(outgoing_messages.into_iter().map(|m| m.into()))
.collect() .collect()
}) })

View File

@@ -0,0 +1,64 @@
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct AttachmentMetadata {
pub attribution_info: Option<AttributionInfo>,
}
#[derive(Debug, Clone)]
pub struct AttributionInfo {
pub width: Option<u32>,
pub height: Option<u32>,
}
#[derive(Debug, Clone)]
pub struct Attachment {
pub guid: String,
pub base_path: PathBuf,
pub metadata: Option<AttachmentMetadata>,
}
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<kordophone::model::message::AttachmentMetadata> for AttachmentMetadata {
fn from(metadata: kordophone::model::message::AttachmentMetadata) -> Self {
Self {
attribution_info: metadata.attribution_info.map(|info| info.into()),
}
}
}
impl From<kordophone::model::message::AttributionInfo> for AttributionInfo {
fn from(info: kordophone::model::message::AttributionInfo) -> Self {
Self {
width: info.width,
height: info.height,
}
}
}
impl From<AttachmentMetadata> for kordophone::model::message::AttachmentMetadata {
fn from(metadata: AttachmentMetadata) -> Self {
Self {
attribution_info: metadata.attribution_info.map(|info| info.into()),
}
}
}
impl From<AttributionInfo> for kordophone::model::message::AttributionInfo {
fn from(info: AttributionInfo) -> Self {
Self {
width: info.width,
height: info.height,
}
}
}

View File

@@ -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<i32>,
display_name: String,
},
}
impl From<String> 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<kordophone_db::models::Participant> 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<Attachment>,
}
impl Message {
pub fn builder() -> MessageBuilder {
MessageBuilder::new()
}
}
fn attachments_from(file_transfer_guids: &Vec<String>, attachment_metadata: &Option<HashMap<String, AttachmentMetadata>>) -> Vec<Attachment> {
file_transfer_guids
.iter()
.map(|guid| {
let mut attachment = AttachmentStore::get_attachment_impl(&AttachmentStore::get_default_store_path(), guid);
attachment.metadata = match attachment_metadata {
Some(attachment_metadata) => attachment_metadata.get(guid).cloned().map(|metadata| metadata.into()),
None => None,
};
attachment
})
.collect()
}
impl From<kordophone_db::models::Message> for Message {
fn from(message: kordophone_db::models::Message) -> Self {
let attachments = attachments_from(&message.file_transfer_guids, &message.attachment_metadata);
Self {
id: message.id,
sender: message.sender.into(),
text: message.text,
date: message.date,
attachments,
}
}
}
impl From<Message> 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<String, kordophone::model::message::AttachmentMetadata> = 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<kordophone::model::Message> 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<String>,
sender: Option<Participant>,
text: Option<String>,
date: Option<NaiveDateTime>,
attachments: Vec<Attachment>,
}
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<Attachment>) -> 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,
}
}
}

View File

@@ -0,0 +1,5 @@
pub mod attachment;
pub mod message;
pub use attachment::Attachment;
pub use message::Message;

View File

@@ -138,29 +138,50 @@ impl DbusRepository for ServerImpl {
arg::Variant(Box::new(msg.sender.display_name())), arg::Variant(Box::new(msg.sender.display_name())),
); );
// Add file transfer GUIDs if present // Add attachments array
if !msg.file_transfer_guids.is_empty() { let attachments: Vec<arg::PropMap> = msg.attachments
match serde_json::to_string(&msg.file_transfer_guids) { .into_iter()
Ok(json_str) => { .map(|attachment| {
map.insert("file_transfer_guids".into(), arg::Variant(Box::new(json_str))); 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 map.insert("attachments".into(), arg::Variant(Box::new(attachments)));
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 map
}) })