Private
Public Access
1
0

daemon: implements post office

This commit is contained in:
2025-05-02 14:22:43 -07:00
parent 07b55f8615
commit 2519bc05ad
13 changed files with 234 additions and 48 deletions

61
Cargo.lock generated
View File

@@ -1036,6 +1036,8 @@ dependencies = [
"log", "log",
"thiserror 2.0.12", "thiserror 2.0.12",
"tokio", "tokio",
"tokio-condvar",
"uuid",
] ]
[[package]] [[package]]
@@ -1386,35 +1388,14 @@ version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
]
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.9.1" version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
dependencies = [ dependencies = [
"rand_chacha 0.9.0", "rand_chacha",
"rand_core 0.9.3", "rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core 0.6.4",
] ]
[[package]] [[package]]
@@ -1424,16 +1405,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [ dependencies = [
"ppv-lite86", "ppv-lite86",
"rand_core 0.9.3", "rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.14",
] ]
[[package]] [[package]]
@@ -1821,6 +1793,15 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "tokio-condvar"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8530e402d24f6a65019baa57593f1769557c670302f493cdf8fa3dfbe4d85ac"
dependencies = [
"tokio",
]
[[package]] [[package]]
name = "tokio-macros" name = "tokio-macros"
version = "2.5.0" version = "2.5.0"
@@ -1944,7 +1925,7 @@ dependencies = [
"http 1.3.1", "http 1.3.1",
"httparse", "httparse",
"log", "log",
"rand 0.9.1", "rand",
"sha1", "sha1",
"thiserror 2.0.12", "thiserror 2.0.12",
"utf-8", "utf-8",
@@ -1988,20 +1969,20 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.11.0" version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
dependencies = [ dependencies = [
"getrandom 0.2.14", "getrandom 0.3.2",
"rand 0.8.5", "rand",
"uuid-macro-internal", "uuid-macro-internal",
] ]
[[package]] [[package]]
name = "uuid-macro-internal" name = "uuid-macro-internal"
version = "1.11.0" version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b91f57fe13a38d0ce9e28a03463d8d3c2468ed03d75375110ec71d93b449a08" checksum = "72dcd78c4f979627a754f5522cea6e6a25e55139056535fe6e69c506cd64a862"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

@@ -227,7 +227,7 @@ impl<K: AuthenticationStore + Send + Sync> APIInterface for HTTPAPIClient<K> {
async fn send_message( async fn send_message(
&mut self, &mut self,
outgoing_message: OutgoingMessage, outgoing_message: &OutgoingMessage,
) -> Result<Message, Self::Error> { ) -> Result<Message, Self::Error> {
let message: Message = self.request_with_body( let message: Message = self.request_with_body(
"sendMessage", "sendMessage",

View File

@@ -15,10 +15,11 @@ pub mod event_socket;
pub use event_socket::EventSocket; pub use event_socket::EventSocket;
use self::http_client::Credentials; use self::http_client::Credentials;
use std::fmt::Debug;
#[async_trait] #[async_trait]
pub trait APIInterface { pub trait APIInterface {
type Error; type Error: Debug;
// (GET) /version // (GET) /version
async fn get_version(&mut self) -> Result<String, Self::Error>; async fn get_version(&mut self) -> Result<String, Self::Error>;
@@ -38,7 +39,7 @@ pub trait APIInterface {
// (POST) /sendMessage // (POST) /sendMessage
async fn send_message( async fn send_message(
&mut self, &mut self,
outgoing_message: OutgoingMessage, outgoing_message: &OutgoingMessage,
) -> Result<Message, Self::Error>; ) -> Result<Message, Self::Error>;
// (POST) /authenticate // (POST) /authenticate

View File

@@ -1,8 +1,12 @@
use serde::Serialize; use serde::Serialize;
use super::conversation::ConversationID; use super::conversation::ConversationID;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct OutgoingMessage { pub struct OutgoingMessage {
#[serde(skip)]
pub guid: Uuid,
#[serde(rename = "body")] #[serde(rename = "body")]
pub text: String, pub text: String,
@@ -21,6 +25,7 @@ impl OutgoingMessage {
#[derive(Default)] #[derive(Default)]
pub struct OutgoingMessageBuilder { pub struct OutgoingMessageBuilder {
guid: Option<Uuid>,
text: Option<String>, text: Option<String>,
conversation_id: Option<ConversationID>, conversation_id: Option<ConversationID>,
file_transfer_guids: Option<Vec<String>>, file_transfer_guids: Option<Vec<String>>,
@@ -31,6 +36,11 @@ impl OutgoingMessageBuilder {
Self::default() Self::default()
} }
pub fn guid(mut self, guid: Uuid) -> Self {
self.guid = Some(guid);
self
}
pub fn text(mut self, text: String) -> Self { pub fn text(mut self, text: String) -> Self {
self.text = Some(text); self.text = Some(text);
self self
@@ -48,6 +58,7 @@ impl OutgoingMessageBuilder {
pub fn build(self) -> OutgoingMessage { pub fn build(self) -> OutgoingMessage {
OutgoingMessage { OutgoingMessage {
guid: self.guid.unwrap_or_else(|| Uuid::new_v4()),
text: self.text.unwrap(), text: self.text.unwrap(),
conversation_id: self.conversation_id.unwrap(), conversation_id: self.conversation_id.unwrap(),
file_transfer_guids: self.file_transfer_guids.unwrap_or_default(), file_transfer_guids: self.file_transfer_guids.unwrap_or_default(),

View File

@@ -93,15 +93,15 @@ impl APIInterface for TestClient {
async fn send_message( async fn send_message(
&mut self, &mut self,
outgoing_message: OutgoingMessage, outgoing_message: &OutgoingMessage,
) -> Result<Message, Self::Error> { ) -> Result<Message, Self::Error> {
let message = Message::builder() let message = Message::builder()
.guid(Uuid::new_v4().to_string()) .guid(Uuid::new_v4().to_string())
.text(outgoing_message.text) .text(outgoing_message.text.clone())
.date(OffsetDateTime::now_utc()) .date(OffsetDateTime::now_utc())
.build(); .build();
self.messages.entry(outgoing_message.conversation_id).or_insert(vec![]).push(message.clone()); self.messages.entry(outgoing_message.conversation_id.clone()).or_insert(vec![]).push(message.clone());
Ok(message) Ok(message)
} }

View File

@@ -18,6 +18,8 @@ kordophone-db = { path = "../kordophone-db" }
log = "0.4.25" log = "0.4.25"
thiserror = "2.0.12" thiserror = "2.0.12"
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
tokio-condvar = "0.3.0"
uuid = "1.16.0"
[build-dependencies] [build-dependencies]
dbus-codegen = "0.10.0" dbus-codegen = "0.10.0"

View File

@@ -58,6 +58,15 @@
<arg type="aa{sv}" direction="out" name="messages"/> <arg type="aa{sv}" direction="out" name="messages"/>
</method> </method>
<method name="SendMessage">
<arg type="s" name="conversation_id" direction="in"/>
<arg type="s" name="text" direction="in"/>
<arg type="s" name="outgoing_message_id" direction="out"/>
<annotation name="org.freedesktop.DBus.DocString"
value="Sends a message to the server. Returns the outgoing message ID."/>
</method>
<signal name="MessagesUpdated"> <signal name="MessagesUpdated">
<arg type="s" name="conversation_id" direction="in"/> <arg type="s" name="conversation_id" direction="in"/>
<annotation name="org.freedesktop.DBus.DocString" <annotation name="org.freedesktop.DBus.DocString"

View File

@@ -1,4 +1,6 @@
use tokio::sync::oneshot; use tokio::sync::oneshot;
use uuid::Uuid;
use kordophone_db::models::{Conversation, Message}; use kordophone_db::models::{Conversation, Message};
use crate::daemon::settings::Settings; use crate::daemon::settings::Settings;
@@ -33,6 +35,13 @@ pub enum Event {
/// - last_message_id: (optional) The ID of the last message to get. If None, all messages are returned. /// - last_message_id: (optional) The ID of the last message to get. If None, all messages are returned.
GetMessages(String, Option<String>, Reply<Vec<Message>>), GetMessages(String, Option<String>, Reply<Vec<Message>>),
/// Enqueues a message to be sent to the server.
/// Parameters:
/// - conversation_id: The ID of the conversation to send the message to.
/// - text: The text of the message to send.
/// - reply: The outgoing message ID (not the server-assigned message ID).
SendMessage(String, String, Reply<Uuid>),
/// Delete all conversations from the database. /// Delete all conversations from the database.
DeleteAllConversations(Reply<()>), DeleteAllConversations(Reply<()>),
} }

View File

@@ -16,6 +16,7 @@ use thiserror::Error;
use tokio::sync::mpsc::{Sender, Receiver}; use tokio::sync::mpsc::{Sender, Receiver};
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use uuid::Uuid;
use kordophone_db::{ use kordophone_db::{
database::{Database, DatabaseAccess}, database::{Database, DatabaseAccess},
@@ -24,6 +25,7 @@ use kordophone_db::{
use kordophone::api::APIInterface; use kordophone::api::APIInterface;
use kordophone::api::http_client::HTTPAPIClient; use kordophone::api::http_client::HTTPAPIClient;
use kordophone::model::outgoing_message::OutgoingMessage;
mod update_monitor; mod update_monitor;
use update_monitor::UpdateMonitor; use update_monitor::UpdateMonitor;
@@ -31,6 +33,10 @@ use update_monitor::UpdateMonitor;
mod auth_store; mod auth_store;
use auth_store::DatabaseAuthenticationStore; use auth_store::DatabaseAuthenticationStore;
mod post_office;
use post_office::PostOffice;
use post_office::Event as PostOfficeEvent;
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum DaemonError { pub enum DaemonError {
#[error("Client Not Configured")] #[error("Client Not Configured")]
@@ -52,6 +58,9 @@ pub struct Daemon {
signal_receiver: Option<Receiver<Signal>>, signal_receiver: Option<Receiver<Signal>>,
signal_sender: Sender<Signal>, signal_sender: Sender<Signal>,
post_office_sink: Sender<PostOfficeEvent>,
post_office_source: Option<Receiver<PostOfficeEvent>>,
version: String, version: String,
database: Arc<Mutex<Database>>, database: Arc<Mutex<Database>>,
runtime: tokio::runtime::Runtime, runtime: tokio::runtime::Runtime,
@@ -69,6 +78,7 @@ impl Daemon {
// Create event channels // Create event channels
let (event_sender, event_receiver) = tokio::sync::mpsc::channel(100); let (event_sender, event_receiver) = tokio::sync::mpsc::channel(100);
let (signal_sender, signal_receiver) = tokio::sync::mpsc::channel(100); let (signal_sender, signal_receiver) = tokio::sync::mpsc::channel(100);
let (post_office_sink, post_office_source) = tokio::sync::mpsc::channel(100);
// Create background task runtime // Create background task runtime
let runtime = tokio::runtime::Builder::new_multi_thread() let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all() .enable_all()
@@ -84,6 +94,8 @@ impl Daemon {
event_sender, event_sender,
signal_receiver: Some(signal_receiver), signal_receiver: Some(signal_receiver),
signal_sender, signal_sender,
post_office_sink,
post_office_source: Some(post_office_source),
runtime runtime
}) })
} }
@@ -92,12 +104,23 @@ impl Daemon {
log::info!("Starting daemon version {}", self.version); log::info!("Starting daemon version {}", self.version);
log::debug!("Debug logging enabled."); log::debug!("Debug logging enabled.");
// Update monitor
let mut update_monitor = UpdateMonitor::new(self.database.clone(), self.event_sender.clone()); let mut update_monitor = UpdateMonitor::new(self.database.clone(), self.event_sender.clone());
tokio::spawn(async move { tokio::spawn(async move {
update_monitor.run().await; // should run indefinitely update_monitor.run().await; // should run indefinitely
}); });
// Post office
{
let mut database = self.database.clone();
let event_sender = self.event_sender.clone();
let post_office_source = self.post_office_source.take().unwrap();
tokio::spawn(async move {
let mut post_office = PostOffice::new(post_office_source, event_sender, async move || Self::get_client_impl(&mut database).await );
post_office.run().await;
});
}
while let Some(event) = self.event_receiver.recv().await { while let Some(event) = self.event_receiver.recv().await {
log::debug!(target: target::EVENT, "Received event: {:?}", event); log::debug!(target: target::EVENT, "Received event: {:?}", event);
self.handle_event(event).await; self.handle_event(event).await;
@@ -188,6 +211,11 @@ impl Daemon {
reply.send(()).unwrap(); reply.send(()).unwrap();
}, },
Event::SendMessage(conversation_id, text, reply) => {
let uuid = self.enqueue_outgoing_message(text, conversation_id).await;
reply.send(uuid).unwrap();
},
} }
} }
@@ -204,6 +232,18 @@ impl Daemon {
self.database.lock().await.with_repository(|r| r.get_messages_for_conversation(&conversation_id).unwrap()).await self.database.lock().await.with_repository(|r| r.get_messages_for_conversation(&conversation_id).unwrap()).await
} }
async fn enqueue_outgoing_message(&mut self, text: String, conversation_id: String) -> Uuid {
let outgoing_message = OutgoingMessage::builder()
.text(text)
.conversation_id(conversation_id)
.build();
let guid = outgoing_message.guid.clone();
self.post_office_sink.send(PostOfficeEvent::EnqueueOutgoingMessage(outgoing_message)).await.unwrap();
guid
}
async fn sync_conversation_list(database: &mut Arc<Mutex<Database>>, signal_sender: &Sender<Signal>) -> Result<()> { async fn sync_conversation_list(database: &mut Arc<Mutex<Database>>, signal_sender: &Sender<Signal>) -> Result<()> {
log::info!(target: target::SYNC, "Starting list conversation sync"); log::info!(target: target::SYNC, "Starting list conversation sync");

View File

@@ -0,0 +1,115 @@
use std::collections::VecDeque;
use std::time::Duration;
use tokio::sync::mpsc::{Sender, Receiver};
use tokio::sync::{Mutex, MutexGuard};
use tokio_condvar::Condvar;
use crate::daemon::events::Event as DaemonEvent;
use kordophone::model::outgoing_message::OutgoingMessage;
use kordophone::api::APIInterface;
use anyhow::Result;
mod target {
pub static POST_OFFICE: &str = "post_office";
}
#[derive(Debug)]
pub enum Event {
EnqueueOutgoingMessage(OutgoingMessage),
}
pub struct PostOffice<C: APIInterface, F: AsyncFnMut() -> Result<C>> {
event_source: Receiver<Event>,
event_sink: Sender<DaemonEvent>,
make_client: F,
message_queue: Mutex<VecDeque<OutgoingMessage>>,
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 {
event_source,
event_sink,
make_client,
message_queue: Mutex::new(VecDeque::new()),
message_available: Condvar::new(),
}
}
pub async fn queue_message(&mut self, message: &OutgoingMessage) {
self.message_queue.lock().await.push_back(message.clone());
self.message_available.notify_one();
}
pub async fn run(&mut self) {
log::info!(target: target::POST_OFFICE, "Starting post office");
loop {
let mut retry_messages = Vec::new();
log::debug!(target: target::POST_OFFICE, "Waiting for event");
tokio::select! {
// Incoming events
Some(event) = self.event_source.recv() => {
match event {
Event::EnqueueOutgoingMessage(message) => {
log::debug!(target: target::POST_OFFICE, "Received enqueue outgoing message event");
self.message_queue.lock().await.push_back(message);
self.message_available.notify_one();
}
}
}
// Message queue
mut lock = self.message_available.wait(self.message_queue.lock().await) => {
log::debug!(target: target::POST_OFFICE, "Message available in queue");
retry_messages = Self::try_send_message_impl(&mut lock, &mut self.make_client).await;
}
}
if !retry_messages.is_empty() {
log::debug!(target: target::POST_OFFICE, "Queueing {} messages for retry", retry_messages.len());
for message in retry_messages {
self.queue_message(&message).await;
}
}
}
}
async fn try_send_message_impl(message_queue: &mut MutexGuard<'_, VecDeque<OutgoingMessage>>, make_client: &mut F) -> Vec<OutgoingMessage> {
log::debug!(target: target::POST_OFFICE, "Trying to send enqueued messages");
let mut retry_messages = Vec::new();
while let Some(message) = message_queue.pop_front() {
match (make_client)().await {
Ok(mut client) => {
log::debug!(target: target::POST_OFFICE, "Obtained client, sending message.");
match client.send_message(&message).await {
Ok(message) => {
log::info!(target: target::POST_OFFICE, "Message sent successfully: {}", message.guid);
// TODO: Notify the daemon via the event sink.
}
Err(e) => {
log::error!(target: target::POST_OFFICE, "Error sending message: {:?}", e);
log::warn!(target: target::POST_OFFICE, "Retrying in 5 seconds");
tokio::time::sleep(Duration::from_secs(5)).await;
retry_messages.push(message);
}
}
}
Err(e) => {
log::error!(target: target::POST_OFFICE, "Error creating client: {:?}", e);
log::warn!(target: target::POST_OFFICE, "Retrying in 5 seconds");
tokio::time::sleep(Duration::from_secs(5)).await;
}
}
}
retry_messages
}
}

View File

@@ -102,6 +102,11 @@ impl DbusRepository for ServerImpl {
fn delete_all_conversations(&mut self) -> Result<(), dbus::MethodErr> { fn delete_all_conversations(&mut self) -> Result<(), dbus::MethodErr> {
self.send_event_sync(Event::DeleteAllConversations) self.send_event_sync(Event::DeleteAllConversations)
} }
fn send_message(&mut self, conversation_id: String, text: String) -> Result<String, dbus::MethodErr> {
self.send_event_sync(|r| Event::SendMessage(conversation_id, text, r))
.map(|uuid| uuid.to_string())
}
} }
impl DbusSettings for ServerImpl { impl DbusSettings for ServerImpl {

View File

@@ -138,7 +138,7 @@ impl ClientCli {
.text(message) .text(message)
.build(); .build();
let message = self.api.send_message(outgoing_message).await?; let message = self.api.send_message(&outgoing_message).await?;
println!("Message sent: {}", message.guid); println!("Message sent: {}", message.guid);
Ok(()) Ok(())
} }

View File

@@ -48,6 +48,12 @@ pub enum Commands {
/// Deletes all conversations. /// Deletes all conversations.
DeleteAllConversations, DeleteAllConversations,
/// Enqueues an outgoing message to be sent to a conversation.
SendMessage {
conversation_id: String,
text: String,
},
} }
#[derive(Subcommand)] #[derive(Subcommand)]
@@ -83,6 +89,7 @@ impl Commands {
Commands::Signals => client.wait_for_signals().await, Commands::Signals => client.wait_for_signals().await,
Commands::Messages { conversation_id, last_message_id } => client.print_messages(conversation_id, last_message_id).await, Commands::Messages { conversation_id, last_message_id } => client.print_messages(conversation_id, last_message_id).await,
Commands::DeleteAllConversations => client.delete_all_conversations().await, Commands::DeleteAllConversations => client.delete_all_conversations().await,
Commands::SendMessage { conversation_id, text } => client.enqueue_outgoing_message(conversation_id, text).await,
} }
} }
} }
@@ -145,6 +152,12 @@ impl DaemonCli {
Ok(()) Ok(())
} }
pub async fn enqueue_outgoing_message(&mut self, conversation_id: String, text: String) -> Result<()> {
let outgoing_message_id = KordophoneRepository::send_message(&self.proxy(), &conversation_id, &text)?;
println!("Outgoing message ID: {}", outgoing_message_id);
Ok(())
}
pub async fn wait_for_signals(&mut self) -> Result<()> { pub async fn wait_for_signals(&mut self) -> Result<()> {
use dbus::Message; use dbus::Message;
mod dbus_signals { mod dbus_signals {