plumb all known attachments via dbus if known
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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"/>
|
||||
</arg>
|
||||
</method>
|
||||
|
||||
@@ -116,7 +124,7 @@
|
||||
<arg type="s" name="attachment_id"/>
|
||||
<arg type="s" name="file_path"/>
|
||||
<arg type="b" name="preview"/>
|
||||
|
||||
|
||||
<annotation name="org.freedesktop.DBus.DocString"
|
||||
value="Emitted when an attachment download completes successfully."/>
|
||||
</signal>
|
||||
|
||||
@@ -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<Mutex<Database>>, daemon_event_sink: Sender<Event>) -> 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<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());
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<T> = oneshot::Sender<T>;
|
||||
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
64
kordophoned/src/daemon/models/attachment.rs
Normal file
64
kordophoned/src/daemon/models/attachment.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
217
kordophoned/src/daemon/models/message.rs
Normal file
217
kordophoned/src/daemon/models/message.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
5
kordophoned/src/daemon/models/mod.rs
Normal file
5
kordophoned/src/daemon/models/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod attachment;
|
||||
pub mod message;
|
||||
|
||||
pub use attachment::Attachment;
|
||||
pub use message::Message;
|
||||
@@ -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<arg::PropMap> = msg.attachments
|
||||
.into_iter()
|
||||
.map(|attachment| {
|
||||
let mut attachment_map = arg::PropMap::new();
|
||||
attachment_map.insert("guid".into(), arg::Variant(Box::new(attachment.guid.clone())));
|
||||
|
||||
// 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
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user