Private
Public Access
1
0

first attempt: notification code is in dbus::agent

This commit is contained in:
2025-11-01 21:39:53 -07:00
parent e650cffde7
commit 717138b371
15 changed files with 1222 additions and 120 deletions

840
core/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -307,8 +307,8 @@ impl<'a> Repository<'a> {
} }
pub fn delete_all_messages(&mut self) -> Result<()> { pub fn delete_all_messages(&mut self) -> Result<()> {
use crate::schema::messages::dsl as messages_dsl;
use crate::schema::message_aliases::dsl as aliases_dsl; use crate::schema::message_aliases::dsl as aliases_dsl;
use crate::schema::messages::dsl as messages_dsl;
diesel::delete(messages_dsl::messages).execute(self.connection)?; diesel::delete(messages_dsl::messages).execute(self.connection)?;
diesel::delete(aliases_dsl::message_aliases).execute(self.connection)?; diesel::delete(aliases_dsl::message_aliases).execute(self.connection)?;

View File

@@ -394,8 +394,7 @@ impl<K: AuthenticationStore + Send + Sync> APIInterface for HTTPAPIClient<K> {
None => "updates".to_string(), None => "updates".to_string(),
}; };
let uri = self let uri = self.uri_for_endpoint(&endpoint, Some(self.websocket_scheme()))?;
.uri_for_endpoint(&endpoint, Some(self.websocket_scheme()))?;
loop { loop {
log::debug!("Connecting to websocket: {:?}", uri); log::debug!("Connecting to websocket: {:?}", uri);
@@ -430,14 +429,16 @@ impl<K: AuthenticationStore + Send + Sync> APIInterface for HTTPAPIClient<K> {
match connect_async(request).await.map_err(Error::from) { match connect_async(request).await.map_err(Error::from) {
Ok((socket, response)) => { Ok((socket, response)) => {
log::debug!("Websocket connected: {:?}", response.status()); log::debug!("Websocket connected: {:?}", response.status());
break Ok(WebsocketEventSocket::new(socket)) break Ok(WebsocketEventSocket::new(socket));
} }
Err(e) => match &e { Err(e) => match &e {
Error::ClientError(ce) => match ce.as_str() { Error::ClientError(ce) => match ce.as_str() {
"HTTP error: 401 Unauthorized" | "Unauthorized" => { "HTTP error: 401 Unauthorized" | "Unauthorized" => {
// Try to authenticate // Try to authenticate
if let Some(credentials) = &self.auth_store.get_credentials().await { if let Some(credentials) = &self.auth_store.get_credentials().await {
log::warn!("Websocket connection failed, attempting to authenticate"); log::warn!(
"Websocket connection failed, attempting to authenticate"
);
let new_token = self.authenticate(credentials.clone()).await?; let new_token = self.authenticate(credentials.clone()).await?;
self.auth_store.set_token(new_token.to_string()).await; self.auth_store.set_token(new_token.to_string()).await;
@@ -469,16 +470,16 @@ impl<K: AuthenticationStore + Send + Sync> HTTPAPIClient<K> {
let https = HttpsConnector::new(); let https = HttpsConnector::new();
let client = Client::builder().build::<_, Body>(https); let client = Client::builder().build::<_, Body>(https);
HTTPAPIClient { base_url, auth_store, client } HTTPAPIClient {
base_url,
auth_store,
client,
}
} }
fn uri_for_endpoint(&self, endpoint: &str, scheme: Option<&str>) -> Result<Uri, Error> { fn uri_for_endpoint(&self, endpoint: &str, scheme: Option<&str>) -> Result<Uri, Error> {
let mut parts = self.base_url.clone().into_parts(); let mut parts = self.base_url.clone().into_parts();
let root_path: PathBuf = parts let root_path: PathBuf = parts.path_and_query.ok_or(Error::URLError)?.path().into();
.path_and_query
.ok_or(Error::URLError)?
.path()
.into();
let path = root_path.join(endpoint); let path = root_path.join(endpoint);
let path_str = path.to_str().ok_or(Error::URLError)?; let path_str = path.to_str().ok_or(Error::URLError)?;

View File

@@ -23,6 +23,7 @@ tokio-condvar = "0.3.0"
uuid = "1.16.0" uuid = "1.16.0"
once_cell = "1.19.0" once_cell = "1.19.0"
mime_guess = "2.0" mime_guess = "2.0"
notify = { package = "notify-rust", version = "4.10.0" }
# D-Bus dependencies only on Linux # D-Bus dependencies only on Linux
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]

View File

@@ -103,6 +103,13 @@
"/> "/>
</method> </method>
<method name="TestNotification">
<arg type="s" name="summary" direction="in"/>
<arg type="s" name="body" direction="in"/>
<annotation name="org.freedesktop.DBus.DocString"
value="Displays a test desktop notification with the provided summary and body."/>
</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

@@ -41,6 +41,12 @@ pub enum Event {
/// - offset: The offset into the conversation list to start returning conversations from. /// - offset: The offset into the conversation list to start returning conversations from.
GetAllConversations(i32, i32, Reply<Vec<Conversation>>), GetAllConversations(i32, i32, Reply<Vec<Conversation>>),
/// Returns a conversation by its ID.
GetConversation(String, Reply<Option<Conversation>>),
/// Returns the most recent message for a conversation.
GetLastMessage(String, Reply<Option<Message>>),
/// Returns all known settings from the database. /// Returns all known settings from the database.
GetAllSettings(Reply<Settings>), GetAllSettings(Reply<Settings>),

View File

@@ -271,6 +271,16 @@ impl Daemon {
reply.send(conversations).unwrap(); reply.send(conversations).unwrap();
} }
Event::GetConversation(conversation_id, reply) => {
let conversation = self.get_conversation(conversation_id).await;
reply.send(conversation).unwrap();
}
Event::GetLastMessage(conversation_id, reply) => {
let message = self.get_last_message(conversation_id).await;
reply.send(message).unwrap();
}
Event::GetAllSettings(reply) => { Event::GetAllSettings(reply) => {
let settings = self.get_settings().await.unwrap_or_else(|e| { let settings = self.get_settings().await.unwrap_or_else(|e| {
log::error!(target: target::SETTINGS, "Failed to get settings: {:#?}", e); log::error!(target: target::SETTINGS, "Failed to get settings: {:#?}", e);
@@ -433,6 +443,14 @@ impl Daemon {
self.signal_receiver.take().unwrap() self.signal_receiver.take().unwrap()
} }
async fn get_conversation(&mut self, conversation_id: String) -> Option<Conversation> {
self.database
.lock()
.await
.with_repository(|r| r.get_conversation_by_guid(&conversation_id).unwrap())
.await
}
async fn get_conversations_limit_offset( async fn get_conversations_limit_offset(
&mut self, &mut self,
limit: i32, limit: i32,
@@ -445,6 +463,18 @@ impl Daemon {
.await .await
} }
async fn get_last_message(&mut self, conversation_id: String) -> Option<Message> {
self.database
.lock()
.await
.with_repository(|r| {
r.get_last_message_for_conversation(&conversation_id)
.unwrap()
.map(|message| message.into())
})
.await
}
async fn get_messages( async fn get_messages(
&mut self, &mut self,
conversation_id: String, conversation_id: String,
@@ -471,9 +501,8 @@ impl Daemon {
.await; .await;
// Convert DB messages to daemon model, substituting local_id when an alias exists. // Convert DB messages to daemon model, substituting local_id when an alias exists.
let mut result: Vec<Message> = Vec::with_capacity( let mut result: Vec<Message> =
db_messages.len() + outgoing_messages.len(), Vec::with_capacity(db_messages.len() + outgoing_messages.len());
);
for m in db_messages.into_iter() { for m in db_messages.into_iter() {
let server_id = m.id.clone(); let server_id = m.id.clone();
let mut dm: Message = m.into(); let mut dm: Message = m.into();

View File

@@ -25,8 +25,7 @@ impl Attachment {
// Prefer common, user-friendly extensions over obscure ones // Prefer common, user-friendly extensions over obscure ones
match normalized { match normalized {
"image/jpeg" | "image/pjpeg" => Some("jpg"), "image/jpeg" | "image/pjpeg" => Some("jpg"),
_ => mime_guess::get_mime_extensions_str(normalized) _ => mime_guess::get_mime_extensions_str(normalized).and_then(|list| {
.and_then(|list| {
// If jpg is one of the candidates, prefer it // If jpg is one of the candidates, prefer it
if list.iter().any(|e| *e == "jpg") { if list.iter().any(|e| *e == "jpg") {
Some("jpg") Some("jpg")

View File

@@ -1,6 +1,7 @@
use dbus::arg; use dbus::arg;
use dbus_tree::MethodErr; use dbus_tree::MethodErr;
use std::sync::Arc; use notify::Notification;
use std::sync::{Arc, Mutex as StdMutex};
use std::{future::Future, thread}; use std::{future::Future, thread};
use tokio::sync::{mpsc, oneshot, Mutex}; use tokio::sync::{mpsc, oneshot, Mutex};
@@ -9,10 +10,11 @@ use kordophoned::daemon::{
events::{Event, Reply}, events::{Event, Reply},
settings::Settings, settings::Settings,
signals::Signal, signals::Signal,
DaemonResult, DaemonResult, Message,
}; };
use kordophone_db::models::participant::Participant; use kordophone_db::models::participant::Participant;
use kordophone_db::models::Conversation;
use crate::dbus::endpoint::DbusRegistry; use crate::dbus::endpoint::DbusRegistry;
use crate::dbus::interface; use crate::dbus::interface;
@@ -23,7 +25,7 @@ use dbus_tokio::connection;
pub struct DBusAgent { pub struct DBusAgent {
event_sink: mpsc::Sender<Event>, event_sink: mpsc::Sender<Event>,
signal_receiver: Arc<Mutex<Option<mpsc::Receiver<Signal>>>>, signal_receiver: Arc<Mutex<Option<mpsc::Receiver<Signal>>>>,
contact_resolver: ContactResolver<DefaultContactResolverBackend>, contact_resolver: Arc<StdMutex<ContactResolver<DefaultContactResolverBackend>>>,
} }
impl DBusAgent { impl DBusAgent {
@@ -31,7 +33,9 @@ impl DBusAgent {
Self { Self {
event_sink, event_sink,
signal_receiver: Arc::new(Mutex::new(Some(signal_receiver))), signal_receiver: Arc::new(Mutex::new(Some(signal_receiver))),
contact_resolver: ContactResolver::new(DefaultContactResolverBackend::default()), contact_resolver: Arc::new(StdMutex::new(ContactResolver::new(
DefaultContactResolverBackend::default(),
))),
} }
} }
@@ -68,6 +72,7 @@ impl DBusAgent {
{ {
let registry = dbus_registry.clone(); let registry = dbus_registry.clone();
let receiver_arc = self.signal_receiver.clone(); let receiver_arc = self.signal_receiver.clone();
let agent_clone = self.clone();
tokio::spawn(async move { tokio::spawn(async move {
let mut receiver = receiver_arc let mut receiver = receiver_arc
.lock() .lock()
@@ -94,6 +99,7 @@ impl DBusAgent {
"Sending signal: MessagesUpdated for conversation {}", "Sending signal: MessagesUpdated for conversation {}",
conversation_id conversation_id
); );
let conversation_id_for_notification = conversation_id.clone();
registry registry
.send_signal( .send_signal(
interface::OBJECT_PATH, interface::OBJECT_PATH,
@@ -103,6 +109,10 @@ impl DBusAgent {
log::error!("Failed to send signal"); log::error!("Failed to send signal");
0 0
}); });
agent_clone
.maybe_notify_on_messages_updated(&conversation_id_for_notification)
.await;
} }
Signal::AttachmentDownloaded(attachment_id) => { Signal::AttachmentDownloaded(attachment_id) => {
log::debug!( log::debug!(
@@ -181,7 +191,7 @@ impl DBusAgent {
.map_err(|e| MethodErr::failed(&format!("Daemon error: {}", e))) .map_err(|e| MethodErr::failed(&format!("Daemon error: {}", e)))
} }
fn resolve_participant_display_name(&mut self, participant: &Participant) -> String { fn resolve_participant_display_name(&self, participant: &Participant) -> String {
match participant { match participant {
// Me (we should use a special string here...) // Me (we should use a special string here...)
Participant::Me => "(Me)".to_string(), Participant::Me => "(Me)".to_string(),
@@ -191,10 +201,15 @@ impl DBusAgent {
handle, handle,
contact_id: Some(contact_id), contact_id: Some(contact_id),
.. ..
} => self } => {
.contact_resolver if let Ok(mut resolver) = self.contact_resolver.lock() {
resolver
.get_contact_display_name(contact_id) .get_contact_display_name(contact_id)
.unwrap_or_else(|| handle.clone()), .unwrap_or_else(|| handle.clone())
} else {
handle.clone()
}
}
// Remote participant without a resolved contact_id // Remote participant without a resolved contact_id
Participant::Remote { handle, .. } => handle.clone(), Participant::Remote { handle, .. } => handle.clone(),
@@ -202,6 +217,113 @@ impl DBusAgent {
} }
} }
impl DBusAgent {
fn conversation_display_name(&self, conversation: &Conversation) -> String {
if let Some(display_name) = &conversation.display_name {
if !display_name.trim().is_empty() {
return display_name.clone();
}
}
let names: Vec<String> = conversation
.participants
.iter()
.filter(|participant| !matches!(participant, Participant::Me))
.map(|participant| self.resolve_participant_display_name(participant))
.collect();
if names.is_empty() {
"Kordophone".to_string()
} else {
names.join(", ")
}
}
async fn prepare_incoming_message_notification(
&self,
conversation_id: &str,
) -> DaemonResult<Option<(String, String)>> {
let conversation = match self
.send_event(|reply| Event::GetConversation(conversation_id.to_string(), reply))
.await?
{
Some(conv) => conv,
None => return Ok(None),
};
if conversation.unread_count == 0 {
return Ok(None);
}
let last_message: Option<Message> = self
.send_event(|reply| Event::GetLastMessage(conversation_id.to_string(), reply))
.await?;
let last_message = match last_message {
Some(message) => message,
None => return Ok(None),
};
let sender_participant: Participant = Participant::from(last_message.sender.clone());
if matches!(sender_participant, Participant::Me) {
return Ok(None);
}
let summary = self.conversation_display_name(&conversation);
let sender_display_name = self.resolve_participant_display_name(&sender_participant);
let mut message_text = last_message.text.replace('\u{FFFC}', "");
if message_text.trim().is_empty() {
if !last_message.attachments.is_empty() {
message_text = "Sent an attachment".to_string();
} else {
message_text = "Sent a message".to_string();
}
}
let body = if sender_display_name.is_empty() {
message_text
} else {
format!("{}: {}", sender_display_name, message_text)
};
Ok(Some((summary, body)))
}
fn show_notification(&self, summary: &str, body: &str) -> Result<(), notify::error::Error> {
Notification::new()
.appname("Kordophone")
.summary(summary)
.body(body)
.show()
.map(|_| ())
}
async fn maybe_notify_on_messages_updated(&self, conversation_id: &str) {
match self
.prepare_incoming_message_notification(conversation_id)
.await
{
Ok(Some((summary, body))) => {
if let Err(error) = self.show_notification(&summary, &body) {
log::warn!(
"Failed to display notification for conversation {}: {}",
conversation_id,
error
);
}
}
Ok(None) => {}
Err(error) => {
log::warn!(
"Unable to prepare notification for conversation {}: {}",
conversation_id,
error
);
}
}
}
}
// //
// D-Bus repository interface implementation // D-Bus repository interface implementation
// //
@@ -398,6 +520,11 @@ impl DbusRepository for DBusAgent {
.map(|uuid| uuid.to_string()) .map(|uuid| uuid.to_string())
} }
fn test_notification(&mut self, summary: String, body: String) -> Result<(), MethodErr> {
self.show_notification(&summary, &body)
.map_err(|e| MethodErr::failed(&format!("Failed to display notification: {}", e)))
}
fn get_attachment_info( fn get_attachment_info(
&mut self, &mut self,
attachment_id: String, attachment_id: String,

View File

@@ -15,10 +15,16 @@ pub struct DispatchResult {
impl DispatchResult { impl DispatchResult {
pub fn new(message: Message) -> Self { pub fn new(message: Message) -> Self {
Self { message, cleanup: None } Self {
message,
cleanup: None,
}
} }
pub fn with_cleanup<T: Any + Send + 'static>(message: Message, cleanup: T) -> Self { pub fn with_cleanup<T: Any + Send + 'static>(message: Message, cleanup: T) -> Self {
Self { message, cleanup: Some(Box::new(cleanup)) } Self {
message,
cleanup: Some(Box::new(cleanup)),
}
} }
} }

View File

@@ -105,7 +105,12 @@ pub async fn dispatch(
.and_then(|m| dict_get_str(m, "conversation_id")) .and_then(|m| dict_get_str(m, "conversation_id"))
{ {
Some(id) => id, Some(id) => id,
None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing conversation_id")), None => {
return DispatchResult::new(make_error_reply(
"InvalidRequest",
"Missing conversation_id",
))
}
}; };
match agent match agent
.send_event(|r| Event::SyncConversation(conversation_id, r)) .send_event(|r| Event::SyncConversation(conversation_id, r))
@@ -122,7 +127,12 @@ pub async fn dispatch(
.and_then(|m| dict_get_str(m, "conversation_id")) .and_then(|m| dict_get_str(m, "conversation_id"))
{ {
Some(id) => id, Some(id) => id,
None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing conversation_id")), None => {
return DispatchResult::new(make_error_reply(
"InvalidRequest",
"Missing conversation_id",
))
}
}; };
match agent match agent
.send_event(|r| Event::MarkConversationAsRead(conversation_id, r)) .send_event(|r| Event::MarkConversationAsRead(conversation_id, r))
@@ -137,11 +147,21 @@ pub async fn dispatch(
"GetMessages" => { "GetMessages" => {
let args = match get_dictionary_field(root, "arguments") { let args = match get_dictionary_field(root, "arguments") {
Some(a) => a, Some(a) => a,
None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing arguments")), None => {
return DispatchResult::new(make_error_reply(
"InvalidRequest",
"Missing arguments",
))
}
}; };
let conversation_id = match dict_get_str(args, "conversation_id") { let conversation_id = match dict_get_str(args, "conversation_id") {
Some(id) => id, Some(id) => id,
None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing conversation_id")), None => {
return DispatchResult::new(make_error_reply(
"InvalidRequest",
"Missing conversation_id",
))
}
}; };
let last_message_id = dict_get_str(args, "last_message_id"); let last_message_id = dict_get_str(args, "last_message_id");
match agent match agent
@@ -158,11 +178,8 @@ pub async fn dispatch(
dict_put_str(&mut m, "sender", &msg.sender.display_name()); dict_put_str(&mut m, "sender", &msg.sender.display_name());
// Include attachment GUIDs for the client to resolve/download // Include attachment GUIDs for the client to resolve/download
let attachment_guids: Vec<String> = msg let attachment_guids: Vec<String> =
.attachments msg.attachments.iter().map(|a| a.guid.clone()).collect();
.iter()
.map(|a| a.guid.clone())
.collect();
m.insert(cstr("attachment_guids"), array_from_strs(attachment_guids)); m.insert(cstr("attachment_guids"), array_from_strs(attachment_guids));
// Full attachments array with metadata (mirrors DBus fields) // Full attachments array with metadata (mirrors DBus fields)
@@ -193,12 +210,23 @@ pub async fn dispatch(
if let Some(attribution_info) = &metadata.attribution_info { if let Some(attribution_info) = &metadata.attribution_info {
let mut attribution_map: XpcMap = HashMap::new(); let mut attribution_map: XpcMap = HashMap::new();
if let Some(width) = attribution_info.width { if let Some(width) = attribution_info.width {
dict_put_i64_as_str(&mut attribution_map, "width", width as i64); dict_put_i64_as_str(
&mut attribution_map,
"width",
width as i64,
);
} }
if let Some(height) = attribution_info.height { if let Some(height) = attribution_info.height {
dict_put_i64_as_str(&mut attribution_map, "height", height as i64); dict_put_i64_as_str(
&mut attribution_map,
"height",
height as i64,
);
} }
metadata_map.insert(cstr("attribution_info"), Message::Dictionary(attribution_map)); metadata_map.insert(
cstr("attribution_info"),
Message::Dictionary(attribution_map),
);
} }
if !metadata_map.is_empty() { if !metadata_map.is_empty() {
a.insert(cstr("metadata"), Message::Dictionary(metadata_map)); a.insert(cstr("metadata"), Message::Dictionary(metadata_map));
@@ -230,11 +258,21 @@ pub async fn dispatch(
"SendMessage" => { "SendMessage" => {
let args = match get_dictionary_field(root, "arguments") { let args = match get_dictionary_field(root, "arguments") {
Some(a) => a, Some(a) => a,
None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing arguments")), None => {
return DispatchResult::new(make_error_reply(
"InvalidRequest",
"Missing arguments",
))
}
}; };
let conversation_id = match dict_get_str(args, "conversation_id") { let conversation_id = match dict_get_str(args, "conversation_id") {
Some(v) => v, Some(v) => v,
None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing conversation_id")), None => {
return DispatchResult::new(make_error_reply(
"InvalidRequest",
"Missing conversation_id",
))
}
}; };
let text = dict_get_str(args, "text").unwrap_or_default(); let text = dict_get_str(args, "text").unwrap_or_default();
let attachment_guids: Vec<String> = match args.get(&cstr("attachment_guids")) { let attachment_guids: Vec<String> = match args.get(&cstr("attachment_guids")) {
@@ -265,11 +303,21 @@ pub async fn dispatch(
"GetAttachmentInfo" => { "GetAttachmentInfo" => {
let args = match get_dictionary_field(root, "arguments") { let args = match get_dictionary_field(root, "arguments") {
Some(a) => a, Some(a) => a,
None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing arguments")), None => {
return DispatchResult::new(make_error_reply(
"InvalidRequest",
"Missing arguments",
))
}
}; };
let attachment_id = match dict_get_str(args, "attachment_id") { let attachment_id = match dict_get_str(args, "attachment_id") {
Some(v) => v, Some(v) => v,
None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing attachment_id")), None => {
return DispatchResult::new(make_error_reply(
"InvalidRequest",
"Missing attachment_id",
))
}
}; };
match agent match agent
.send_event(|r| Event::GetAttachment(attachment_id, r)) .send_event(|r| Event::GetAttachment(attachment_id, r))
@@ -308,11 +356,21 @@ pub async fn dispatch(
"OpenAttachmentFd" => { "OpenAttachmentFd" => {
let args = match get_dictionary_field(root, "arguments") { let args = match get_dictionary_field(root, "arguments") {
Some(a) => a, Some(a) => a,
None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing arguments")), None => {
return DispatchResult::new(make_error_reply(
"InvalidRequest",
"Missing arguments",
))
}
}; };
let attachment_id = match dict_get_str(args, "attachment_id") { let attachment_id = match dict_get_str(args, "attachment_id") {
Some(v) => v, Some(v) => v,
None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing attachment_id")), None => {
return DispatchResult::new(make_error_reply(
"InvalidRequest",
"Missing attachment_id",
))
}
}; };
let preview = dict_get_str(args, "preview") let preview = dict_get_str(args, "preview")
.map(|s| s == "true") .map(|s| s == "true")
@@ -335,9 +393,14 @@ pub async fn dispatch(
dict_put_str(&mut reply, "type", "OpenAttachmentFdResponse"); dict_put_str(&mut reply, "type", "OpenAttachmentFdResponse");
reply.insert(cstr("fd"), Message::Fd(fd)); reply.insert(cstr("fd"), Message::Fd(fd));
DispatchResult { message: Message::Dictionary(reply), cleanup: Some(Box::new(file)) } DispatchResult {
message: Message::Dictionary(reply),
cleanup: Some(Box::new(file)),
}
}
Err(e) => {
DispatchResult::new(make_error_reply("OpenFailed", &format!("{}", e)))
} }
Err(e) => DispatchResult::new(make_error_reply("OpenFailed", &format!("{}", e))),
} }
} }
Err(e) => DispatchResult::new(make_error_reply("DaemonError", &format!("{}", e))), Err(e) => DispatchResult::new(make_error_reply("DaemonError", &format!("{}", e))),
@@ -348,11 +411,21 @@ pub async fn dispatch(
"DownloadAttachment" => { "DownloadAttachment" => {
let args = match get_dictionary_field(root, "arguments") { let args = match get_dictionary_field(root, "arguments") {
Some(a) => a, Some(a) => a,
None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing arguments")), None => {
return DispatchResult::new(make_error_reply(
"InvalidRequest",
"Missing arguments",
))
}
}; };
let attachment_id = match dict_get_str(args, "attachment_id") { let attachment_id = match dict_get_str(args, "attachment_id") {
Some(v) => v, Some(v) => v,
None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing attachment_id")), None => {
return DispatchResult::new(make_error_reply(
"InvalidRequest",
"Missing attachment_id",
))
}
}; };
let preview = dict_get_str(args, "preview") let preview = dict_get_str(args, "preview")
.map(|s| s == "true") .map(|s| s == "true")
@@ -371,11 +444,18 @@ pub async fn dispatch(
use std::path::PathBuf; use std::path::PathBuf;
let args = match get_dictionary_field(root, "arguments") { let args = match get_dictionary_field(root, "arguments") {
Some(a) => a, Some(a) => a,
None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing arguments")), None => {
return DispatchResult::new(make_error_reply(
"InvalidRequest",
"Missing arguments",
))
}
}; };
let path = match dict_get_str(args, "path") { let path = match dict_get_str(args, "path") {
Some(v) => v, Some(v) => v,
None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing path")), None => {
return DispatchResult::new(make_error_reply("InvalidRequest", "Missing path"))
}
}; };
match agent match agent
.send_event(|r| Event::UploadAttachment(PathBuf::from(path), r)) .send_event(|r| Event::UploadAttachment(PathBuf::from(path), r))
@@ -413,7 +493,12 @@ pub async fn dispatch(
"UpdateSettings" => { "UpdateSettings" => {
let args = match get_dictionary_field(root, "arguments") { let args = match get_dictionary_field(root, "arguments") {
Some(a) => a, Some(a) => a,
None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing arguments")), None => {
return DispatchResult::new(make_error_reply(
"InvalidRequest",
"Missing arguments",
))
}
}; };
let server_url = dict_get_str(args, "server_url"); let server_url = dict_get_str(args, "server_url");
let username = dict_get_str(args, "username"); let username = dict_get_str(args, "username");

View File

@@ -146,8 +146,7 @@ impl ClientCli {
loop { loop {
match stream.next().await.unwrap() { match stream.next().await.unwrap() {
Ok(update) => { Ok(update) => match update {
match update {
SocketUpdate::Update(updates) => { SocketUpdate::Update(updates) => {
for update in updates { for update in updates {
println!("Got update: {:?}", update); println!("Got update: {:?}", update);
@@ -156,7 +155,6 @@ impl ClientCli {
SocketUpdate::Pong => { SocketUpdate::Pong => {
println!("Pong"); println!("Pong");
} }
}
}, },
Err(e) => { Err(e) => {

View File

@@ -209,4 +209,9 @@ impl DaemonInterface for DBusDaemonInterface {
KordophoneRepository::mark_conversation_as_read(&self.proxy(), &conversation_id) KordophoneRepository::mark_conversation_as_read(&self.proxy(), &conversation_id)
.map_err(|e| anyhow::anyhow!("Failed to mark conversation as read: {}", e)) .map_err(|e| anyhow::anyhow!("Failed to mark conversation as read: {}", e))
} }
async fn test_notification(&mut self, summary: String, body: String) -> Result<()> {
KordophoneRepository::test_notification(&self.proxy(), &summary, &body)
.map_err(|e| anyhow::anyhow!("Failed to trigger test notification: {}", e))
}
} }

View File

@@ -32,6 +32,7 @@ pub trait DaemonInterface {
async fn download_attachment(&mut self, attachment_id: String) -> Result<()>; async fn download_attachment(&mut self, attachment_id: String) -> Result<()>;
async fn upload_attachment(&mut self, path: String) -> Result<()>; async fn upload_attachment(&mut self, path: String) -> Result<()>;
async fn mark_conversation_as_read(&mut self, conversation_id: String) -> Result<()>; async fn mark_conversation_as_read(&mut self, conversation_id: String) -> Result<()>;
async fn test_notification(&mut self, summary: String, body: String) -> Result<()>;
} }
struct StubDaemonInterface; struct StubDaemonInterface;
@@ -112,6 +113,11 @@ impl DaemonInterface for StubDaemonInterface {
"Daemon interface not implemented on this platform" "Daemon interface not implemented on this platform"
)) ))
} }
async fn test_notification(&mut self, _summary: String, _body: String) -> Result<()> {
Err(anyhow::anyhow!(
"Daemon interface not implemented on this platform"
))
}
} }
pub fn new_daemon_interface() -> Result<Box<dyn DaemonInterface>> { pub fn new_daemon_interface() -> Result<Box<dyn DaemonInterface>> {
@@ -175,6 +181,9 @@ pub enum Commands {
/// Marks a conversation as read. /// Marks a conversation as read.
MarkConversationAsRead { conversation_id: String }, MarkConversationAsRead { conversation_id: String },
/// Displays a test notification using the daemon.
TestNotification { summary: String, body: String },
} }
#[derive(Subcommand)] #[derive(Subcommand)]
@@ -219,6 +228,9 @@ impl Commands {
Commands::MarkConversationAsRead { conversation_id } => { Commands::MarkConversationAsRead { conversation_id } => {
client.mark_conversation_as_read(conversation_id).await client.mark_conversation_as_read(conversation_id).await
} }
Commands::TestNotification { summary, body } => {
client.test_notification(summary, body).await
}
} }
} }
} }

View File

@@ -1,13 +1,13 @@
use std::env; use std::env;
use std::process; use std::process;
use kordophone::{
api::{HTTPAPIClient, InMemoryAuthenticationStore, EventSocket},
model::{ConversationID, event::EventData},
APIInterface,
};
use kordophone::api::http_client::Credentials;
use kordophone::api::AuthenticationStore; use kordophone::api::AuthenticationStore;
use kordophone::api::http_client::Credentials;
use kordophone::{
APIInterface,
api::{EventSocket, HTTPAPIClient, InMemoryAuthenticationStore},
model::{ConversationID, event::EventData},
};
use futures_util::StreamExt; use futures_util::StreamExt;
use hyper::Uri; use hyper::Uri;
@@ -18,7 +18,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = env::args().collect(); let args: Vec<String> = env::args().collect();
if args.len() < 2 { if args.len() < 2 {
eprintln!("Usage: {} <conversation_id1> [conversation_id2] [conversation_id3] ...", args[0]); eprintln!(
"Usage: {} <conversation_id1> [conversation_id2] [conversation_id3] ...",
args[0]
);
eprintln!("Environment variables required:"); eprintln!("Environment variables required:");
eprintln!(" KORDOPHONE_API_URL - Server URL"); eprintln!(" KORDOPHONE_API_URL - Server URL");
eprintln!(" KORDOPHONE_USERNAME - Username for authentication"); eprintln!(" KORDOPHONE_USERNAME - Username for authentication");
@@ -40,12 +43,14 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let credentials = Credentials { username, password }; let credentials = Credentials { username, password };
// Collect all conversation IDs from command line arguments // Collect all conversation IDs from command line arguments
let target_conversation_ids: Vec<ConversationID> = args[1..].iter() let target_conversation_ids: Vec<ConversationID> =
.map(|id| id.clone()) args[1..].iter().map(|id| id.clone()).collect();
.collect();
println!("Monitoring {} conversation(s) for updates: {:?}", println!(
target_conversation_ids.len(), target_conversation_ids); "Monitoring {} conversation(s) for updates: {:?}",
target_conversation_ids.len(),
target_conversation_ids
);
let auth_store = InMemoryAuthenticationStore::new(Some(credentials.clone())); let auth_store = InMemoryAuthenticationStore::new(Some(credentials.clone()));
let mut client = HTTPAPIClient::new(server_url, auth_store); let mut client = HTTPAPIClient::new(server_url, auth_store);
@@ -62,26 +67,33 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
match event_result { match event_result {
Ok(socket_event) => { Ok(socket_event) => {
match socket_event { match socket_event {
kordophone::api::event_socket::SocketEvent::Update(event) => { kordophone::api::event_socket::SocketEvent::Update(event) => match event.data {
match event.data {
EventData::MessageReceived(conversation, _message) => { EventData::MessageReceived(conversation, _message) => {
if target_conversation_ids.contains(&conversation.guid) { if target_conversation_ids.contains(&conversation.guid) {
println!("Message update detected for conversation {}, marking as read...", conversation.guid); println!(
"Message update detected for conversation {}, marking as read...",
conversation.guid
);
match client.mark_conversation_as_read(&conversation.guid).await { match client.mark_conversation_as_read(&conversation.guid).await {
Ok(_) => println!("Successfully marked conversation {} as read", conversation.guid), Ok(_) => println!(
Err(e) => eprintln!("Failed to mark conversation {} as read: {:?}", conversation.guid, e), "Successfully marked conversation {} as read",
conversation.guid
),
Err(e) => eprintln!(
"Failed to mark conversation {} as read: {:?}",
conversation.guid, e
),
}
} }
} }
},
_ => {} _ => {}
}
}, },
kordophone::api::event_socket::SocketEvent::Pong => { kordophone::api::event_socket::SocketEvent::Pong => {
// Ignore pong messages // Ignore pong messages
} }
} }
}, }
Err(e) => { Err(e) => {
eprintln!("Error receiving event: {:?}", e); eprintln!("Error receiving event: {:?}", e);
break; break;