Private
Public Access
1
0

[core] kordophoned-client: add support for settings and missing gui features

This commit is contained in:
2026-05-27 22:02:28 -07:00
parent a3dfedcfd0
commit 0173be356e
6 changed files with 463 additions and 24 deletions

3
core/Cargo.lock generated
View File

@@ -1274,7 +1274,7 @@ dependencies = [
[[package]] [[package]]
name = "kordophoned" name = "kordophoned"
version = "1.3.0" version = "1.3.2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -1313,6 +1313,7 @@ dependencies = [
"block", "block",
"dbus", "dbus",
"dbus-codegen", "dbus-codegen",
"keyring",
"log", "log",
"xpc-connection", "xpc-connection",
"xpc-connection-sys", "xpc-connection-sys",

View File

@@ -10,6 +10,7 @@ log = "0.4.22"
# D-Bus dependencies only on Linux # D-Bus dependencies only on Linux
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
dbus = "0.9.7" dbus = "0.9.7"
keyring = { version = "3.6.3", features = ["sync-secret-service"] }
# D-Bus codegen only on Linux # D-Bus codegen only on Linux
[target.'cfg(target_os = "linux")'.build-dependencies] [target.'cfg(target_os = "linux")'.build-dependencies]

View File

@@ -1,5 +1,7 @@
mod platform; mod platform;
mod worker; mod worker;
pub use worker::{spawn_worker, ChatMessage, ConversationSummary, Event, Request}; pub use worker::{
spawn_worker, Attachment, AttachmentInfo, AttachmentMetadata, AttributionInfo, ChatMessage,
ConversationSummary, Event, Request, SettingsSnapshot,
};

View File

@@ -1,8 +1,11 @@
#![cfg(target_os = "linux")] #![cfg(target_os = "linux")]
use crate::worker::{ChatMessage, ConversationSummary, DaemonClient, Event}; use crate::worker::{
Attachment, AttachmentInfo, AttachmentMetadata, AttributionInfo, ChatMessage,
ConversationSummary, DaemonClient, Event, SettingsSnapshot,
};
use anyhow::Result; use anyhow::Result;
use dbus::arg::{PropMap, RefArg}; use dbus::arg::{cast, PropMap, RefArg};
use dbus::blocking::{Connection, Proxy}; use dbus::blocking::{Connection, Proxy};
use dbus::channel::Token; use dbus::channel::Token;
use std::sync::mpsc::Sender; use std::sync::mpsc::Sender;
@@ -17,6 +20,7 @@ mod dbus_interface {
include!(concat!(env!("OUT_DIR"), "/kordophone-client.rs")); include!(concat!(env!("OUT_DIR"), "/kordophone-client.rs"));
} }
use dbus_interface::NetBuzzertKordophoneRepository as KordophoneRepository; use dbus_interface::NetBuzzertKordophoneRepository as KordophoneRepository;
use dbus_interface::NetBuzzertKordophoneSettings as KordophoneSettings;
pub(crate) struct DBusClient { pub(crate) struct DBusClient {
conn: Connection, conn: Connection,
@@ -31,7 +35,7 @@ impl DBusClient {
}) })
} }
fn proxy(&self) -> Proxy<&Connection> { fn proxy(&self) -> Proxy<'_, &Connection> {
self.conn self.conn
.with_proxy(DBUS_NAME, DBUS_PATH, std::time::Duration::from_millis(5000)) .with_proxy(DBUS_NAME, DBUS_PATH, std::time::Duration::from_millis(5000))
} }
@@ -52,6 +56,10 @@ fn get_u32(map: &PropMap, key: &str) -> u32 {
get_i64(map, key).try_into().unwrap_or(0) get_i64(map, key).try_into().unwrap_or(0)
} }
fn get_bool(map: &PropMap, key: &str) -> bool {
map.get(key).and_then(|v| v.0.as_i64()).unwrap_or(0) != 0
}
fn get_vec_string(map: &PropMap, key: &str) -> Vec<String> { fn get_vec_string(map: &PropMap, key: &str) -> Vec<String> {
map.get(key) map.get(key)
.and_then(|v| v.0.as_iter()) .and_then(|v| v.0.as_iter())
@@ -62,6 +70,122 @@ fn get_vec_string(map: &PropMap, key: &str) -> Vec<String> {
.unwrap_or_default() .unwrap_or_default()
} }
fn clone_prop_map(map: &PropMap) -> PropMap {
map.iter()
.map(|(key, value)| (key.clone(), dbus::arg::Variant(value.0.box_clone())))
.collect()
}
fn get_prop_map(map: &PropMap, key: &str) -> Option<PropMap> {
let value = map.get(key)?;
if let Some(prop_map) = cast::<PropMap>(&value.0) {
return Some(clone_prop_map(prop_map));
}
let mut iter = value.0.as_iter()?;
let mut out = PropMap::new();
loop {
let Some(key) = iter.next().and_then(|arg| arg.as_str()) else {
break;
};
let Some(value) = iter.next() else {
break;
};
out.insert(key.to_string(), dbus::arg::Variant(value.box_clone()));
}
Some(out)
}
fn get_vec_prop_map(map: &PropMap, key: &str) -> Vec<PropMap> {
let Some(value) = map.get(key) else {
return Vec::new();
};
if let Some(items) = cast::<Vec<PropMap>>(&value.0) {
return items.iter().map(clone_prop_map).collect();
}
value
.0
.as_iter()
.map(|iter| {
iter.filter_map(|item| {
let mut item_iter = item.as_iter()?;
let mut out = PropMap::new();
loop {
let Some(key) = item_iter.next().and_then(|arg| arg.as_str()) else {
break;
};
let Some(value) = item_iter.next() else {
break;
};
out.insert(key.to_string(), dbus::arg::Variant(value.box_clone()));
}
Some(out)
})
.collect()
})
.unwrap_or_default()
}
fn attachment_from_prop_map(map: PropMap) -> Attachment {
let metadata = get_prop_map(&map, "metadata").map(|metadata_map| {
let attribution_info =
get_prop_map(&metadata_map, "attribution_info").map(|info_map| AttributionInfo {
width: info_map
.get("width")
.and_then(|v| v.0.as_i64())
.and_then(|v| v.try_into().ok()),
height: info_map
.get("height")
.and_then(|v| v.0.as_i64())
.and_then(|v| v.try_into().ok()),
});
AttachmentMetadata { attribution_info }
});
Attachment {
guid: get_string(&map, "guid"),
downloaded: get_bool(&map, "downloaded"),
preview_downloaded: get_bool(&map, "preview_downloaded"),
metadata,
}
}
fn password_for_username(username: &str) -> Result<String> {
if username.trim().is_empty() {
return Ok(String::new());
}
let entry = keyring::Entry::new("net.buzzert.kordophonecd", username)?;
match entry.get_password() {
Ok(password) => Ok(password),
Err(keyring::Error::NoEntry) => Ok(String::new()),
Err(e) => Err(e.into()),
}
}
fn set_password_for_username(username: &str, password: &str) -> Result<()> {
if username.trim().is_empty() {
return Ok(());
}
let entry = keyring::Entry::new("net.buzzert.kordophonecd", username)?;
if password.is_empty() {
match entry.delete_credential() {
Ok(()) | Err(keyring::Error::NoEntry) => Ok(()),
Err(e) => Err(e.into()),
}
} else {
entry.set_password(password).map_err(Into::into)
}
}
impl DaemonClient for DBusClient { impl DaemonClient for DBusClient {
fn get_conversations(&mut self, limit: i32, offset: i32) -> Result<Vec<ConversationSummary>> { fn get_conversations(&mut self, limit: i32, offset: i32) -> Result<Vec<ConversationSummary>> {
let mut items = KordophoneRepository::get_conversations(&self.proxy(), limit, offset)?; let mut items = KordophoneRepository::get_conversations(&self.proxy(), limit, offset)?;
@@ -83,6 +207,7 @@ impl DaemonClient for DBusClient {
id, id,
title, title,
preview: get_string(&conv, "last_message_preview").replace('\n', " "), preview: get_string(&conv, "last_message_preview").replace('\n', " "),
participants,
unread_count: get_u32(&conv, "unread_count"), unread_count: get_u32(&conv, "unread_count"),
date_unix: get_i64(&conv, "date"), date_unix: get_i64(&conv, "date"),
} }
@@ -107,20 +232,37 @@ impl DaemonClient for DBusClient {
Ok(messages Ok(messages
.into_iter() .into_iter()
.map(|msg| ChatMessage { .map(|msg| ChatMessage {
id: get_string(&msg, "id"),
sender: get_string(&msg, "sender"), sender: get_string(&msg, "sender"),
text: get_string(&msg, "text"), text: get_string(&msg, "text"),
date_unix: get_i64(&msg, "date"), date_unix: get_i64(&msg, "date"),
attachments: get_vec_prop_map(&msg, "attachments")
.into_iter()
.map(attachment_from_prop_map)
.collect(),
}) })
.collect()) .collect())
} }
fn send_message(&mut self, conversation_id: String, text: String) -> Result<Option<String>> { fn send_message(&mut self, conversation_id: String, text: String) -> Result<Option<String>> {
let attachment_guids: Vec<&str> = vec![]; self.send_message_with_attachments(conversation_id, text, Vec::new())
}
fn send_message_with_attachments(
&mut self,
conversation_id: String,
text: String,
attachment_guids: Vec<String>,
) -> Result<Option<String>> {
let attachment_guid_refs = attachment_guids
.iter()
.map(String::as_str)
.collect::<Vec<_>>();
let outgoing_id = KordophoneRepository::send_message( let outgoing_id = KordophoneRepository::send_message(
&self.proxy(), &self.proxy(),
&conversation_id, &conversation_id,
&text, &text,
attachment_guids, attachment_guid_refs,
)?; )?;
Ok(Some(outgoing_id)) Ok(Some(outgoing_id))
} }
@@ -135,6 +277,61 @@ impl DaemonClient for DBusClient {
.map_err(|e| anyhow::anyhow!("Failed to sync conversation: {e}")) .map_err(|e| anyhow::anyhow!("Failed to sync conversation: {e}"))
} }
fn sync_conversation_list(&mut self) -> Result<()> {
KordophoneRepository::sync_conversation_list(&self.proxy())
.map_err(|e| anyhow::anyhow!("Failed to sync conversation list: {e}"))
}
fn sync_all_conversations(&mut self) -> Result<()> {
KordophoneRepository::sync_all_conversations(&self.proxy())
.map_err(|e| anyhow::anyhow!("Failed to sync all conversations: {e}"))
}
fn download_attachment(&mut self, attachment_guid: String, preview: bool) -> Result<()> {
KordophoneRepository::download_attachment(&self.proxy(), &attachment_guid, preview)
.map_err(|e| anyhow::anyhow!("Failed to download attachment: {e}"))
}
fn get_attachment_info(&mut self, attachment_guid: String) -> Result<AttachmentInfo> {
let (path, preview_path, downloaded, preview_downloaded) =
KordophoneRepository::get_attachment_info(&self.proxy(), &attachment_guid)?;
Ok(AttachmentInfo {
path,
preview_path,
downloaded,
preview_downloaded,
})
}
fn upload_attachment(&mut self, path: String) -> Result<String> {
KordophoneRepository::upload_attachment(&self.proxy(), &path)
.map_err(|e| anyhow::anyhow!("Failed to upload attachment: {e}"))
}
fn get_settings(&mut self) -> Result<SettingsSnapshot> {
let server_url = KordophoneSettings::server_url(&self.proxy()).unwrap_or_default();
let username = KordophoneSettings::username(&self.proxy()).unwrap_or_default();
let password = password_for_username(&username)?;
Ok(SettingsSnapshot {
server_url,
username,
password,
})
}
fn save_settings(
&mut self,
server_url: String,
username: String,
password: String,
) -> Result<()> {
KordophoneSettings::set_server(&self.proxy(), &server_url, &username)
.map_err(|e| anyhow::anyhow!("Failed to save daemon settings: {e}"))?;
set_password_for_username(&username, &password)
}
fn install_signal_handlers(&mut self, event_tx: Sender<Event>) -> Result<()> { fn install_signal_handlers(&mut self, event_tx: Sender<Event>) -> Result<()> {
let conversations_tx = event_tx.clone(); let conversations_tx = event_tx.clone();
let t1 = self let t1 = self
@@ -164,7 +361,7 @@ impl DaemonClient for DBusClient {
) )
.map_err(|e| anyhow::anyhow!("Failed to match MessagesUpdated: {e}"))?; .map_err(|e| anyhow::anyhow!("Failed to match MessagesUpdated: {e}"))?;
let reconnected_tx = event_tx; let reconnected_tx = event_tx.clone();
let t3 = self let t3 = self
.proxy() .proxy()
.match_signal( .match_signal(
@@ -177,7 +374,54 @@ impl DaemonClient for DBusClient {
) )
.map_err(|e| anyhow::anyhow!("Failed to match UpdateStreamReconnected: {e}"))?; .map_err(|e| anyhow::anyhow!("Failed to match UpdateStreamReconnected: {e}"))?;
self.signal_tokens.extend([t1, t2, t3]); let download_tx = event_tx.clone();
let t4 = self
.proxy()
.match_signal(
move |s: dbus_interface::NetBuzzertKordophoneRepositoryAttachmentDownloadCompleted,
_: &Connection,
_: &dbus::message::Message| {
let _ = download_tx.send(Event::AttachmentDownloaded {
attachment_guid: s.attachment_id,
});
true
},
)
.map_err(|e| anyhow::anyhow!("Failed to match AttachmentDownloadCompleted: {e}"))?;
let failed_tx = event_tx.clone();
let t5 = self
.proxy()
.match_signal(
move |s: dbus_interface::NetBuzzertKordophoneRepositoryAttachmentDownloadFailed,
_: &Connection,
_: &dbus::message::Message| {
let _ = failed_tx.send(Event::AttachmentDownloadFailed {
attachment_guid: s.attachment_id,
error: s.error_message,
});
true
},
)
.map_err(|e| anyhow::anyhow!("Failed to match AttachmentDownloadFailed: {e}"))?;
let upload_tx = event_tx;
let t6 = self
.proxy()
.match_signal(
move |s: dbus_interface::NetBuzzertKordophoneRepositoryAttachmentUploadCompleted,
_: &Connection,
_: &dbus::message::Message| {
let _ = upload_tx.send(Event::AttachmentUploaded {
upload_guid: s.upload_guid,
attachment_guid: s.attachment_guid,
});
true
},
)
.map_err(|e| anyhow::anyhow!("Failed to match AttachmentUploadCompleted: {e}"))?;
self.signal_tokens.extend([t1, t2, t3, t4, t5, t6]);
Ok(()) Ok(())
} }
@@ -186,4 +430,3 @@ impl DaemonClient for DBusClient {
Ok(()) Ok(())
} }
} }

View File

@@ -142,6 +142,7 @@ impl DaemonClient for XpcClient {
id, id,
title, title,
preview: preview.replace('\n', " "), preview: preview.replace('\n', " "),
participants,
unread_count, unread_count,
date_unix, date_unix,
}); });
@@ -180,9 +181,11 @@ impl DaemonClient for XpcClient {
for item in items { for item in items {
let Message::Dictionary(msg) = item else { continue }; let Message::Dictionary(msg) = item else { continue };
messages.push(ChatMessage { messages.push(ChatMessage {
id: Self::get_string(msg, "id").unwrap_or_default(),
sender: Self::get_string(msg, "sender").unwrap_or_default(), sender: Self::get_string(msg, "sender").unwrap_or_default(),
text: Self::get_string(msg, "text").unwrap_or_default(), text: Self::get_string(msg, "text").unwrap_or_default(),
date_unix: Self::get_i64_from_str(msg, "date"), date_unix: Self::get_i64_from_str(msg, "date"),
attachments: Vec::new(),
}); });
} }
Ok(messages) Ok(messages)
@@ -230,4 +233,3 @@ impl DaemonClient for XpcClient {
Ok(()) Ok(())
} }
} }

View File

@@ -8,23 +8,98 @@ pub struct ConversationSummary {
pub id: String, pub id: String,
pub title: String, pub title: String,
pub preview: String, pub preview: String,
pub participants: Vec<String>,
pub unread_count: u32, pub unread_count: u32,
pub date_unix: i64, pub date_unix: i64,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ChatMessage { pub struct ChatMessage {
pub id: String,
pub sender: String, pub sender: String,
pub text: String, pub text: String,
pub date_unix: i64, pub date_unix: i64,
pub attachments: Vec<Attachment>,
}
impl ChatMessage {
pub fn from_me(&self) -> bool {
self.sender == "(Me)"
}
}
#[derive(Clone, Debug, Default)]
pub struct AttributionInfo {
pub width: Option<u32>,
pub height: Option<u32>,
}
#[derive(Clone, Debug, Default)]
pub struct AttachmentMetadata {
pub attribution_info: Option<AttributionInfo>,
}
#[derive(Clone, Debug, Default)]
pub struct Attachment {
pub guid: String,
pub downloaded: bool,
pub preview_downloaded: bool,
pub metadata: Option<AttachmentMetadata>,
}
#[derive(Clone, Debug, Default)]
pub struct AttachmentInfo {
pub path: String,
pub preview_path: String,
pub downloaded: bool,
pub preview_downloaded: bool,
}
#[derive(Clone, Debug, Default)]
pub struct SettingsSnapshot {
pub server_url: String,
pub username: String,
pub password: String,
} }
pub enum Request { pub enum Request {
RefreshConversations, RefreshConversations,
RefreshMessages { conversation_id: String }, RefreshMessages {
SendMessage { conversation_id: String, text: String }, conversation_id: String,
MarkRead { conversation_id: String }, },
SyncConversation { conversation_id: String }, SendMessage {
conversation_id: String,
text: String,
},
SendMessageWithAttachments {
conversation_id: String,
text: String,
attachment_guids: Vec<String>,
},
MarkRead {
conversation_id: String,
},
SyncConversation {
conversation_id: String,
},
SyncConversationList,
SyncAllConversations,
DownloadAttachment {
attachment_guid: String,
preview: bool,
},
GetAttachmentInfo {
attachment_guid: String,
},
UploadAttachment {
path: String,
},
LoadSettings,
SaveSettings {
server_url: String,
username: String,
password: String,
},
} }
pub enum Event { pub enum Event {
@@ -38,9 +113,40 @@ pub enum Event {
outgoing_id: Option<String>, outgoing_id: Option<String>,
}, },
MarkedRead, MarkedRead,
ConversationSyncTriggered { conversation_id: String }, ConversationSyncTriggered {
conversation_id: String,
},
ConversationListSyncTriggered,
AllConversationsSyncTriggered,
AttachmentDownloadQueued {
attachment_guid: String,
preview: bool,
},
AttachmentDownloaded {
attachment_guid: String,
},
AttachmentDownloadFailed {
attachment_guid: String,
error: String,
},
AttachmentUploaded {
upload_guid: String,
attachment_guid: String,
},
AttachmentUploadQueued {
upload_guid: String,
path: String,
},
AttachmentInfo {
attachment_guid: String,
info: AttachmentInfo,
},
SettingsLoaded(SettingsSnapshot),
SettingsSaved,
ConversationsUpdated, ConversationsUpdated,
MessagesUpdated { conversation_id: String }, MessagesUpdated {
conversation_id: String,
},
UpdateStreamReconnected, UpdateStreamReconnected,
Error(String), Error(String),
} }
@@ -59,16 +165,18 @@ pub fn spawn_worker(
}; };
if let Err(e) = client.install_signal_handlers(event_tx.clone()) { if let Err(e) = client.install_signal_handlers(event_tx.clone()) {
let _ = event_tx.send(Event::Error(format!("Failed to install daemon signals: {e}"))); let _ = event_tx.send(Event::Error(format!(
"Failed to install daemon signals: {e}"
)));
} }
loop { loop {
match request_rx.recv_timeout(Duration::from_millis(100)) { match request_rx.recv_timeout(Duration::from_millis(100)) {
Ok(req) => { Ok(req) => {
let res = match req { let res = match req {
Request::RefreshConversations => client Request::RefreshConversations => {
.get_conversations(200, 0) client.get_conversations(200, 0).map(Event::Conversations)
.map(Event::Conversations), }
Request::RefreshMessages { conversation_id } => client Request::RefreshMessages { conversation_id } => client
.get_messages(conversation_id.clone(), None) .get_messages(conversation_id.clone(), None)
.map(|messages| Event::Messages { .map(|messages| Event::Messages {
@@ -78,8 +186,24 @@ pub fn spawn_worker(
Request::SendMessage { Request::SendMessage {
conversation_id, conversation_id,
text, text,
} => {
client
.send_message(conversation_id.clone(), text)
.map(|outgoing_id| Event::MessageSent {
conversation_id,
outgoing_id,
})
}
Request::SendMessageWithAttachments {
conversation_id,
text,
attachment_guids,
} => client } => client
.send_message(conversation_id.clone(), text) .send_message_with_attachments(
conversation_id.clone(),
text,
attachment_guids,
)
.map(|outgoing_id| Event::MessageSent { .map(|outgoing_id| Event::MessageSent {
conversation_id, conversation_id,
outgoing_id, outgoing_id,
@@ -90,6 +214,38 @@ pub fn spawn_worker(
Request::SyncConversation { conversation_id } => client Request::SyncConversation { conversation_id } => client
.sync_conversation(conversation_id.clone()) .sync_conversation(conversation_id.clone())
.map(|_| Event::ConversationSyncTriggered { conversation_id }), .map(|_| Event::ConversationSyncTriggered { conversation_id }),
Request::SyncConversationList => client
.sync_conversation_list()
.map(|_| Event::ConversationListSyncTriggered),
Request::SyncAllConversations => client
.sync_all_conversations()
.map(|_| Event::AllConversationsSyncTriggered),
Request::DownloadAttachment {
attachment_guid,
preview,
} => client
.download_attachment(attachment_guid.clone(), preview)
.map(|_| Event::AttachmentDownloadQueued {
attachment_guid,
preview,
}),
Request::GetAttachmentInfo { attachment_guid } => client
.get_attachment_info(attachment_guid.clone())
.map(|info| Event::AttachmentInfo {
attachment_guid,
info,
}),
Request::UploadAttachment { path } => client
.upload_attachment(path.clone())
.map(|upload_guid| Event::AttachmentUploadQueued { upload_guid, path }),
Request::LoadSettings => client.get_settings().map(Event::SettingsLoaded),
Request::SaveSettings {
server_url,
username,
password,
} => client
.save_settings(server_url, username, password)
.map(|_| Event::SettingsSaved),
}; };
match res { match res {
@@ -120,8 +276,43 @@ pub(crate) trait DaemonClient {
last_message_id: Option<String>, last_message_id: Option<String>,
) -> Result<Vec<ChatMessage>>; ) -> Result<Vec<ChatMessage>>;
fn send_message(&mut self, conversation_id: String, text: String) -> Result<Option<String>>; fn send_message(&mut self, conversation_id: String, text: String) -> Result<Option<String>>;
fn send_message_with_attachments(
&mut self,
conversation_id: String,
text: String,
attachment_guids: Vec<String>,
) -> Result<Option<String>> {
let _ = attachment_guids;
self.send_message(conversation_id, text)
}
fn mark_conversation_as_read(&mut self, conversation_id: String) -> Result<()>; fn mark_conversation_as_read(&mut self, conversation_id: String) -> Result<()>;
fn sync_conversation(&mut self, conversation_id: String) -> Result<()>; fn sync_conversation(&mut self, conversation_id: String) -> Result<()>;
fn sync_conversation_list(&mut self) -> Result<()> {
Ok(())
}
fn sync_all_conversations(&mut self) -> Result<()> {
Ok(())
}
fn download_attachment(&mut self, _attachment_guid: String, _preview: bool) -> Result<()> {
anyhow::bail!("Attachment downloads are not supported on this platform")
}
fn get_attachment_info(&mut self, _attachment_guid: String) -> Result<AttachmentInfo> {
anyhow::bail!("Attachment info is not supported on this platform")
}
fn upload_attachment(&mut self, _path: String) -> Result<String> {
anyhow::bail!("Attachment uploads are not supported on this platform")
}
fn get_settings(&mut self) -> Result<SettingsSnapshot> {
anyhow::bail!("Settings are not supported on this platform")
}
fn save_settings(
&mut self,
_server_url: String,
_username: String,
_password: String,
) -> Result<()> {
anyhow::bail!("Settings are not supported on this platform")
}
fn install_signal_handlers(&mut self, _event_tx: mpsc::Sender<Event>) -> Result<()> { fn install_signal_handlers(&mut self, _event_tx: mpsc::Sender<Event>) -> Result<()> {
Ok(()) Ok(())
} }
@@ -130,4 +321,3 @@ pub(crate) trait DaemonClient {
Ok(()) Ok(())
} }
} }