Private
Public Access
1
0

cargo fmt

This commit is contained in:
2025-06-06 16:39:31 -07:00
parent 8cd72d9417
commit 1d3b2f25ba
44 changed files with 758 additions and 505 deletions

View File

@@ -11,14 +11,12 @@ fn main() {
..Default::default()
};
let xml = std::fs::read_to_string(KORDOPHONE_XML)
.expect("Error reading server dbus interface");
let xml = std::fs::read_to_string(KORDOPHONE_XML).expect("Error reading server dbus interface");
let output = dbus_codegen::generate(&xml, &opts)
.expect("Error generating server dbus interface");
let output =
dbus_codegen::generate(&xml, &opts).expect("Error generating server dbus interface");
std::fs::write(out_path, output)
.expect("Error writing server dbus code");
std::fs::write(out_path, output).expect("Error writing server dbus code");
println!("cargo:rerun-if-changed={}", KORDOPHONE_XML);
}

View File

@@ -16,8 +16,8 @@ use crate::daemon::models::Attachment;
use crate::daemon::Daemon;
use std::sync::Arc;
use tokio::sync::Mutex;
use tokio::sync::mpsc::{Receiver, Sender};
use tokio::sync::Mutex;
use tokio::pin;
@@ -32,7 +32,7 @@ pub enum AttachmentStoreEvent {
GetAttachmentInfo(String, Reply<Attachment>),
// Queue a download for a given attachment guid.
// Args:
// Args:
// - attachment guid
// - preview: whether to download the preview (true) or full attachment (false)
QueueDownloadAttachment(String, bool),
@@ -62,7 +62,10 @@ impl AttachmentStore {
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();
log::info!(target: target::ATTACHMENTS, "Attachment store path: {}", store_path.display());
@@ -97,7 +100,7 @@ impl AttachmentStore {
metadata: None,
}
}
async fn download_attachment(&mut self, attachment: &Attachment, preview: bool) -> Result<()> {
if attachment.is_downloaded(preview) {
log::info!(target: target::ATTACHMENTS, "Attachment already downloaded: {}", attachment.guid);
@@ -130,7 +133,7 @@ impl AttachmentStore {
log::info!(target: target::ATTACHMENTS, "Completed download for attachment: {}", attachment.guid);
Ok(())
}
pub async fn run(&mut self) {
loop {
tokio::select! {

View File

@@ -150,7 +150,8 @@ impl Daemon {
}
// 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());
tokio::spawn(async move {
attachment_store.run().await;
@@ -304,7 +305,10 @@ impl Daemon {
self.attachment_store_sink
.as_ref()
.unwrap()
.send(AttachmentStoreEvent::QueueDownloadAttachment(attachment_id, preview))
.send(AttachmentStoreEvent::QueueDownloadAttachment(
attachment_id,
preview,
))
.await
.unwrap();

View File

@@ -20,14 +20,20 @@ pub struct Attachment {
impl Attachment {
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 {
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())
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 {
@@ -61,4 +67,4 @@ impl From<AttributionInfo> for kordophone::model::message::AttributionInfo {
height: info.height,
}
}
}
}

View File

@@ -1,11 +1,11 @@
use chrono::NaiveDateTime;
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::outgoing_message::OutgoingMessage;
use crate::daemon::models::Attachment;
use crate::daemon::attachment_store::AttachmentStore;
use std::collections::HashMap;
#[derive(Clone, Debug)]
pub enum Participant {
@@ -54,7 +54,7 @@ impl Participant {
}
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug)]
pub struct Message {
pub id: String,
pub sender: Participant,
@@ -63,24 +63,34 @@ pub struct Message {
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
.iter()
.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 {
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,
};
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);
let attachments =
attachments_from(&message.file_transfer_guids, &message.attachment_metadata);
Self {
id: message.id,
sender: message.sender.into(),
@@ -105,11 +115,21 @@ impl From<Message> for kordophone_db::models::Message {
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) }
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)
}
},
}
}
@@ -117,7 +137,8 @@ impl From<Message> for kordophone_db::models::Message {
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);
let attachments =
attachments_from(&message.file_transfer_guids, &message.attachment_metadata);
Self {
id: message.guid,
sender: match message.sender {
@@ -130,12 +151,10 @@ impl From<kordophone::model::Message> for Message {
text: message.text,
date: DateTime::from_timestamp(
message.date.unix_timestamp(),
message.date.unix_timestamp_nanos()
.try_into()
.unwrap_or(0),
)
.unwrap()
.naive_local(),
message.date.unix_timestamp_nanos().try_into().unwrap_or(0),
)
.unwrap()
.naive_local(),
attachments,
}
}

View File

@@ -2,4 +2,4 @@ pub mod attachment;
pub mod message;
pub use attachment::Attachment;
pub use message::Message;
pub use message::Message;

View File

@@ -1,13 +1,13 @@
use std::collections::VecDeque;
use std::time::Duration;
use tokio::sync::mpsc::{Sender, Receiver};
use tokio::sync::mpsc::{Receiver, Sender};
use tokio::sync::Mutex;
use tokio_condvar::Condvar;
use crate::daemon::events::Event as DaemonEvent;
use kordophone::model::outgoing_message::OutgoingMessage;
use kordophone::api::APIInterface;
use kordophone::model::outgoing_message::OutgoingMessage;
use anyhow::Result;
@@ -25,15 +25,19 @@ pub struct PostOffice<C: APIInterface, F: AsyncFnMut() -> Result<C>> {
event_sink: Sender<DaemonEvent>,
make_client: F,
message_queue: Mutex<VecDeque<OutgoingMessage>>,
message_available: Condvar,
message_available: Condvar,
}
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 {
Self {
pub fn new(
event_source: Receiver<Event>,
event_sink: Sender<DaemonEvent>,
make_client: F,
) -> Self {
Self {
event_source,
event_sink,
make_client,
event_sink,
make_client,
message_queue: Mutex::new(VecDeque::new()),
message_available: Condvar::new(),
}
@@ -85,13 +89,12 @@ impl<C: APIInterface, F: AsyncFnMut() -> Result<C>> PostOffice<C, F> {
}
async fn try_send_message(
make_client: &mut F,
event_sink: &Sender<DaemonEvent>,
message: OutgoingMessage
) -> Vec<OutgoingMessage>
{
make_client: &mut F,
event_sink: &Sender<DaemonEvent>,
message: OutgoingMessage,
) -> Vec<OutgoingMessage> {
let mut retry_messages = Vec::new();
match (make_client)().await {
Ok(mut client) => {
log::debug!(target: target::POST_OFFICE, "Obtained client, sending message.");
@@ -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);
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();
}
@@ -123,4 +127,4 @@ impl<C: APIInterface, F: AsyncFnMut() -> Result<C>> PostOffice<C, F> {
retry_messages
}
}
}

View File

@@ -1,5 +1,5 @@
use kordophone_db::settings::Settings as DbSettings;
use anyhow::Result;
use kordophone_db::settings::Settings as DbSettings;
pub mod keys {
pub static SERVER_URL: &str = "ServerURL";
@@ -7,8 +7,7 @@ pub mod keys {
pub static TOKEN: &str = "Token";
}
#[derive(Debug)]
#[derive(Default)]
#[derive(Debug, Default)]
pub struct Settings {
pub server_url: Option<String>,
pub username: Option<String>,
@@ -20,7 +19,7 @@ impl Settings {
let server_url = db_settings.get(keys::SERVER_URL)?;
let username = db_settings.get(keys::USERNAME)?;
let token = db_settings.get(keys::TOKEN)?;
// Create the settings struct with the results
let settings = Self {
server_url,
@@ -30,7 +29,7 @@ impl Settings {
// Load bearing
log::debug!("Loaded settings: {:?}", settings);
Ok(settings)
}
@@ -47,4 +46,3 @@ impl Settings {
Ok(())
}
}

View File

@@ -1,24 +1,21 @@
use crate::daemon::{
Daemon,
DaemonResult,
events::{Event, Reply},
target,
target, Daemon, DaemonResult,
};
use kordophone::APIInterface;
use kordophone::api::event_socket::EventSocket;
use kordophone::model::event::Event as UpdateEvent;
use kordophone::model::event::EventData as UpdateEventData;
use kordophone::APIInterface;
use kordophone_db::database::Database;
use kordophone_db::database::DatabaseAccess;
use tokio::sync::mpsc::Sender;
use std::sync::Arc;
use tokio::sync::Mutex;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::mpsc::Sender;
use tokio::sync::Mutex;
pub struct UpdateMonitor {
database: Arc<Mutex<Database>>,
@@ -29,8 +26,8 @@ pub struct UpdateMonitor {
impl UpdateMonitor {
pub fn new(database: Arc<Mutex<Database>>, event_sender: Sender<Event>) -> Self {
Self {
database,
Self {
database,
event_sender,
last_sync_times: HashMap::new(),
update_seq: None,
@@ -42,23 +39,24 @@ impl UpdateMonitor {
make_event: impl FnOnce(Reply<T>) -> Event,
) -> DaemonResult<T> {
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
.map_err(|_| "Failed to send event")?;
reply_rx.await.map_err(|_| "Failed to receive reply".into())
}
async fn handle_update(&mut self, update: UpdateEvent) {
self.update_seq = Some(update.update_seq);
match update.data {
UpdateEventData::ConversationChanged(conversation) => {
log::info!(target: target::UPDATES, "Conversation changed: {:?}", conversation);
// Check if we've synced this conversation recently (within 5 seconds)
// This is currently a hack/workaround to prevent an infinite loop of sync events, because for some reason
// imagent will post a conversation changed notification when we call getMessages.
// imagent will post a conversation changed notification when we call getMessages.
if let Some(last_sync) = self.last_sync_times.get(&conversation.guid) {
if last_sync.elapsed() < Duration::from_secs(5) {
log::info!(target: target::UPDATES, "Skipping sync for conversation id: {}. Last sync was {} seconds ago.",
@@ -67,8 +65,12 @@ impl UpdateMonitor {
}
}
// 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();
// 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();
match (&last_message, &conversation.last_message) {
(Some(message), Some(conversation_message)) => {
if message.id == conversation_message.guid {
@@ -80,10 +82,12 @@ impl UpdateMonitor {
};
// 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);
self.send_event(|r| Event::SyncConversation(conversation.guid, r)).await
self.send_event(|r| Event::SyncConversation(conversation.guid, r))
.await
.unwrap_or_else(|e| {
log::error!("Failed to send daemon event: {}", e);
});
@@ -92,14 +96,15 @@ impl UpdateMonitor {
UpdateEventData::MessageReceived(conversation, message) => {
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);
self.send_event(|r| Event::SyncConversation(conversation.guid, r)).await
self.send_event(|r| Event::SyncConversation(conversation.guid, r))
.await
.unwrap_or_else(|e| {
log::error!("Failed to send daemon event: {}", e);
});
}
}
}
pub async fn run(&mut self) {
use futures_util::stream::StreamExt;
@@ -130,15 +135,15 @@ impl UpdateMonitor {
log::debug!(target: target::UPDATES, "Starting event stream");
let mut event_stream = socket.events().await;
// We won't know if the websocket is dead until we try to send a message, so time out waiting for
// a message every 30 seconds.
// We won't know if the websocket is dead until we try to send a message, so time out waiting for
// a message every 30 seconds.
let mut timeout = tokio::time::interval(Duration::from_secs(30));
timeout.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
// First tick will happen immediately
timeout.tick().await;
loop {
tokio::select! {
Some(result) = event_stream.next() => {
@@ -161,9 +166,9 @@ impl UpdateMonitor {
}
}
}
// Add a small delay before reconnecting to avoid tight reconnection loops
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
}
}

View File

@@ -42,11 +42,11 @@ impl DbusRegistry {
R: IntoIterator<Item = dbus_crossroads::IfaceToken<T>>,
{
let dbus_path = String::from(path);
let mut cr = self.crossroads.lock().unwrap();
let tokens: Vec<_> = register_fn(&mut cr).into_iter().collect();
cr.insert(dbus_path, &tokens, implementation);
// Start message handler if not already started
let mut handler_started = self.message_handler_started.lock().unwrap();
if !*handler_started {

View File

@@ -13,4 +13,4 @@ pub mod interface {
pub use crate::interface::NetBuzzertKordophoneRepositoryConversationsUpdated as ConversationsUpdated;
pub use crate::interface::NetBuzzertKordophoneRepositoryMessagesUpdated as MessagesUpdated;
}
}
}

View File

@@ -7,7 +7,8 @@ use tokio::sync::oneshot;
use crate::daemon::{
events::{Event, Reply},
settings::Settings, DaemonResult,
settings::Settings,
DaemonResult,
};
use crate::dbus::interface::NetBuzzertKordophoneRepository as DbusRepository;
@@ -136,52 +137,82 @@ impl DbusRepository for ServerImpl {
"sender".into(),
arg::Variant(Box::new(msg.sender.display_name())),
);
// Add attachments array
let attachments: Vec<arg::PropMap> = msg.attachments
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())));
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)));
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)));
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)));
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
})
.collect();
map.insert("attachments".into(), arg::Variant(Box::new(attachments)));
map
})
.collect()
@@ -216,20 +247,21 @@ impl DbusRepository for ServerImpl {
(
// - path: string
path.to_string_lossy().to_string(),
// - preview_path: string
preview_path.to_string_lossy().to_string(),
// - downloaded: boolean
downloaded,
// - preview_downloaded: boolean
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
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>
where
T: Send,

View File

@@ -60,14 +60,12 @@ async fn main() {
// Create and register server implementation
let server = ServerImpl::new(daemon.event_sender.clone());
dbus_registry.register_object(
interface::OBJECT_PATH,
server,
|cr| vec![
dbus_registry.register_object(interface::OBJECT_PATH, server, |cr| {
vec![
interface::register_net_buzzert_kordophone_repository(cr),
interface::register_net_buzzert_kordophone_settings(cr),
]
);
});
let mut signal_receiver = daemon.obtain_signal_receiver();
tokio::spawn(async move {