Add 'core/' from commit 'b0dfc4146ca0da535a87f8509aec68817fb2ab14'
git-subtree-dir: core git-subtree-mainline:a07f3dcd23git-subtree-split:b0dfc4146c
This commit is contained in:
212
core/kpcli/src/daemon/dbus.rs
Normal file
212
core/kpcli/src/daemon/dbus.rs
Normal file
@@ -0,0 +1,212 @@
|
||||
//! Linux-only D-Bus implementation of the `DaemonInterface`.
|
||||
#![cfg(target_os = "linux")]
|
||||
|
||||
use super::{ConfigCommands, DaemonInterface};
|
||||
use crate::printers::{ConversationPrinter, MessagePrinter};
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use dbus::blocking::{Connection, Proxy};
|
||||
use prettytable::table;
|
||||
|
||||
const DBUS_NAME: &str = "net.buzzert.kordophonecd";
|
||||
const DBUS_PATH: &str = "/net/buzzert/kordophonecd/daemon";
|
||||
|
||||
#[allow(unused)]
|
||||
mod dbus_interface {
|
||||
#![allow(unused)]
|
||||
include!(concat!(env!("OUT_DIR"), "/kordophone-client.rs"));
|
||||
}
|
||||
|
||||
use dbus_interface::NetBuzzertKordophoneRepository as KordophoneRepository;
|
||||
use dbus_interface::NetBuzzertKordophoneSettings as KordophoneSettings;
|
||||
|
||||
pub struct DBusDaemonInterface {
|
||||
conn: Connection,
|
||||
}
|
||||
|
||||
impl DBusDaemonInterface {
|
||||
pub fn new() -> Result<Self> {
|
||||
Ok(Self {
|
||||
conn: Connection::new_session()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Proxy<&Connection> {
|
||||
self.conn
|
||||
.with_proxy(DBUS_NAME, DBUS_PATH, std::time::Duration::from_millis(5000))
|
||||
}
|
||||
|
||||
async fn print_settings(&mut self) -> Result<()> {
|
||||
let server_url = KordophoneSettings::server_url(&self.proxy()).unwrap_or_default();
|
||||
let username = KordophoneSettings::username(&self.proxy()).unwrap_or_default();
|
||||
|
||||
let table = table!([
|
||||
b->"Server URL", &server_url
|
||||
], [
|
||||
b->"Username", &username
|
||||
]);
|
||||
table.printstd();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_server_url(&mut self, url: String) -> Result<()> {
|
||||
KordophoneSettings::set_server_url(&self.proxy(), url)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to set server URL: {}", e))
|
||||
}
|
||||
|
||||
async fn set_username(&mut self, username: String) -> Result<()> {
|
||||
KordophoneSettings::set_username(&self.proxy(), username)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to set username: {}", e))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl DaemonInterface for DBusDaemonInterface {
|
||||
async fn print_version(&mut self) -> Result<()> {
|
||||
let version = KordophoneRepository::get_version(&self.proxy())?;
|
||||
println!("Server version: {}", version);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn print_conversations(&mut self) -> Result<()> {
|
||||
let conversations = KordophoneRepository::get_conversations(&self.proxy(), 100, 0)?;
|
||||
println!("Number of conversations: {}", conversations.len());
|
||||
for conversation in conversations {
|
||||
println!("{}", ConversationPrinter::new(&conversation.into()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn sync_conversations(&mut self, conversation_id: Option<String>) -> Result<()> {
|
||||
if let Some(conversation_id) = conversation_id {
|
||||
KordophoneRepository::sync_conversation(&self.proxy(), &conversation_id)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to sync conversation: {}", e))
|
||||
} else {
|
||||
KordophoneRepository::sync_all_conversations(&self.proxy())
|
||||
.map_err(|e| anyhow::anyhow!("Failed to sync conversations: {}", e))
|
||||
}
|
||||
}
|
||||
|
||||
async fn sync_conversations_list(&mut self) -> Result<()> {
|
||||
KordophoneRepository::sync_conversation_list(&self.proxy())
|
||||
.map_err(|e| anyhow::anyhow!("Failed to sync conversations: {}", e))
|
||||
}
|
||||
|
||||
async fn print_messages(
|
||||
&mut self,
|
||||
conversation_id: String,
|
||||
last_message_id: Option<String>,
|
||||
) -> Result<()> {
|
||||
let messages = KordophoneRepository::get_messages(
|
||||
&self.proxy(),
|
||||
&conversation_id,
|
||||
&last_message_id.unwrap_or_default(),
|
||||
)?;
|
||||
println!("Number of messages: {}", messages.len());
|
||||
for message in messages {
|
||||
println!("{}", MessagePrinter::new(&message.into()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn enqueue_outgoing_message(
|
||||
&mut self,
|
||||
conversation_id: String,
|
||||
text: String,
|
||||
) -> Result<()> {
|
||||
let attachment_guids: Vec<&str> = vec![];
|
||||
let outgoing_message_id = KordophoneRepository::send_message(
|
||||
&self.proxy(),
|
||||
&conversation_id,
|
||||
&text,
|
||||
attachment_guids,
|
||||
)?;
|
||||
println!("Outgoing message ID: {}", outgoing_message_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn wait_for_signals(&mut self) -> Result<()> {
|
||||
use dbus::Message;
|
||||
mod dbus_signals {
|
||||
pub use super::dbus_interface::NetBuzzertKordophoneRepositoryConversationsUpdated as ConversationsUpdated;
|
||||
}
|
||||
|
||||
let _id = self.proxy().match_signal(
|
||||
|_: dbus_signals::ConversationsUpdated, _: &Connection, _: &Message| {
|
||||
println!("Signal: Conversations updated");
|
||||
true
|
||||
},
|
||||
);
|
||||
|
||||
println!("Waiting for signals...");
|
||||
loop {
|
||||
self.conn.process(std::time::Duration::from_millis(1000))?;
|
||||
}
|
||||
}
|
||||
|
||||
async fn config(&mut self, cmd: ConfigCommands) -> Result<()> {
|
||||
match cmd {
|
||||
ConfigCommands::Print => self.print_settings().await,
|
||||
ConfigCommands::SetServerUrl { url } => self.set_server_url(url).await,
|
||||
ConfigCommands::SetUsername { username } => self.set_username(username).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn delete_all_conversations(&mut self) -> Result<()> {
|
||||
KordophoneRepository::delete_all_conversations(&self.proxy())
|
||||
.map_err(|e| anyhow::anyhow!("Failed to delete all conversations: {}", e))
|
||||
}
|
||||
|
||||
async fn download_attachment(&mut self, attachment_id: String) -> Result<()> {
|
||||
// Trigger download.
|
||||
KordophoneRepository::download_attachment(&self.proxy(), &attachment_id, false)?;
|
||||
|
||||
// Get attachment info.
|
||||
let attachment_info =
|
||||
KordophoneRepository::get_attachment_info(&self.proxy(), &attachment_id)?;
|
||||
let (path, _preview_path, downloaded, _preview_downloaded) = attachment_info;
|
||||
|
||||
if downloaded {
|
||||
println!("Attachment already downloaded: {}", path);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("Downloading attachment: {}", attachment_id);
|
||||
|
||||
// Attach to the signal that the attachment has been downloaded.
|
||||
let download_path = path.clone();
|
||||
let _id = self.proxy().match_signal(
|
||||
move |_: dbus_interface::NetBuzzertKordophoneRepositoryAttachmentDownloadCompleted,
|
||||
_: &Connection,
|
||||
_: &dbus::message::Message| {
|
||||
println!("Signal: Attachment downloaded: {}", download_path);
|
||||
std::process::exit(0);
|
||||
},
|
||||
);
|
||||
|
||||
let _id = self.proxy().match_signal(
|
||||
|h: dbus_interface::NetBuzzertKordophoneRepositoryAttachmentDownloadFailed,
|
||||
_: &Connection,
|
||||
_: &dbus::message::Message| {
|
||||
println!("Signal: Attachment download failed: {}", h.attachment_id);
|
||||
std::process::exit(1);
|
||||
},
|
||||
);
|
||||
|
||||
// Wait for the signal.
|
||||
loop {
|
||||
self.conn.process(std::time::Duration::from_millis(1000))?;
|
||||
}
|
||||
}
|
||||
|
||||
async fn upload_attachment(&mut self, path: String) -> Result<()> {
|
||||
let upload_guid = KordophoneRepository::upload_attachment(&self.proxy(), &path)?;
|
||||
println!("Upload GUID: {}", upload_guid);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn mark_conversation_as_read(&mut self, conversation_id: String) -> Result<()> {
|
||||
KordophoneRepository::mark_conversation_as_read(&self.proxy(), &conversation_id)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to mark conversation as read: {}", e))
|
||||
}
|
||||
}
|
||||
224
core/kpcli/src/daemon/mod.rs
Normal file
224
core/kpcli/src/daemon/mod.rs
Normal file
@@ -0,0 +1,224 @@
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use clap::Subcommand;
|
||||
|
||||
// Platform-specific modules
|
||||
#[cfg(target_os = "linux")]
|
||||
mod dbus;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod xpc;
|
||||
|
||||
#[cfg_attr(target_os = "macos", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_os = "macos"), async_trait)]
|
||||
pub trait DaemonInterface {
|
||||
async fn print_version(&mut self) -> Result<()>;
|
||||
async fn print_conversations(&mut self) -> Result<()>;
|
||||
async fn sync_conversations(&mut self, conversation_id: Option<String>) -> Result<()>;
|
||||
async fn sync_conversations_list(&mut self) -> Result<()>;
|
||||
async fn print_messages(
|
||||
&mut self,
|
||||
conversation_id: String,
|
||||
last_message_id: Option<String>,
|
||||
) -> Result<()>;
|
||||
async fn enqueue_outgoing_message(
|
||||
&mut self,
|
||||
conversation_id: String,
|
||||
text: String,
|
||||
) -> Result<()>;
|
||||
async fn wait_for_signals(&mut self) -> Result<()>;
|
||||
async fn config(&mut self, cmd: ConfigCommands) -> Result<()>;
|
||||
async fn delete_all_conversations(&mut self) -> Result<()>;
|
||||
async fn download_attachment(&mut self, attachment_id: String) -> Result<()>;
|
||||
async fn upload_attachment(&mut self, path: String) -> Result<()>;
|
||||
async fn mark_conversation_as_read(&mut self, conversation_id: String) -> Result<()>;
|
||||
}
|
||||
|
||||
struct StubDaemonInterface;
|
||||
impl StubDaemonInterface {
|
||||
fn new() -> Result<Self> {
|
||||
Ok(Self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_os = "macos", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_os = "macos"), async_trait)]
|
||||
impl DaemonInterface for StubDaemonInterface {
|
||||
async fn print_version(&mut self) -> Result<()> {
|
||||
Err(anyhow::anyhow!(
|
||||
"Daemon interface not implemented on this platform"
|
||||
))
|
||||
}
|
||||
async fn print_conversations(&mut self) -> Result<()> {
|
||||
Err(anyhow::anyhow!(
|
||||
"Daemon interface not implemented on this platform"
|
||||
))
|
||||
}
|
||||
async fn sync_conversations(&mut self, _conversation_id: Option<String>) -> Result<()> {
|
||||
Err(anyhow::anyhow!(
|
||||
"Daemon interface not implemented on this platform"
|
||||
))
|
||||
}
|
||||
async fn sync_conversations_list(&mut self) -> Result<()> {
|
||||
Err(anyhow::anyhow!(
|
||||
"Daemon interface not implemented on this platform"
|
||||
))
|
||||
}
|
||||
async fn print_messages(
|
||||
&mut self,
|
||||
_conversation_id: String,
|
||||
_last_message_id: Option<String>,
|
||||
) -> Result<()> {
|
||||
Err(anyhow::anyhow!(
|
||||
"Daemon interface not implemented on this platform"
|
||||
))
|
||||
}
|
||||
async fn enqueue_outgoing_message(
|
||||
&mut self,
|
||||
_conversation_id: String,
|
||||
_text: String,
|
||||
) -> Result<()> {
|
||||
Err(anyhow::anyhow!(
|
||||
"Daemon interface not implemented on this platform"
|
||||
))
|
||||
}
|
||||
async fn wait_for_signals(&mut self) -> Result<()> {
|
||||
Err(anyhow::anyhow!(
|
||||
"Daemon interface not implemented on this platform"
|
||||
))
|
||||
}
|
||||
async fn config(&mut self, _cmd: ConfigCommands) -> Result<()> {
|
||||
Err(anyhow::anyhow!(
|
||||
"Daemon interface not implemented on this platform"
|
||||
))
|
||||
}
|
||||
async fn delete_all_conversations(&mut self) -> Result<()> {
|
||||
Err(anyhow::anyhow!(
|
||||
"Daemon interface not implemented on this platform"
|
||||
))
|
||||
}
|
||||
async fn download_attachment(&mut self, _attachment_id: String) -> Result<()> {
|
||||
Err(anyhow::anyhow!(
|
||||
"Daemon interface not implemented on this platform"
|
||||
))
|
||||
}
|
||||
async fn upload_attachment(&mut self, _path: String) -> Result<()> {
|
||||
Err(anyhow::anyhow!(
|
||||
"Daemon interface not implemented on this platform"
|
||||
))
|
||||
}
|
||||
async fn mark_conversation_as_read(&mut self, _conversation_id: String) -> Result<()> {
|
||||
Err(anyhow::anyhow!(
|
||||
"Daemon interface not implemented on this platform"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_daemon_interface() -> Result<Box<dyn DaemonInterface>> {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
Ok(Box::new(dbus::DBusDaemonInterface::new()?))
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
Ok(Box::new(xpc::XpcDaemonInterface::new()?))
|
||||
}
|
||||
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
||||
{
|
||||
Ok(Box::new(StubDaemonInterface::new()?))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Commands {
|
||||
/// Gets all known conversations.
|
||||
Conversations,
|
||||
|
||||
/// Runs a full sync operation for a conversation and its messages.
|
||||
Sync { conversation_id: Option<String> },
|
||||
|
||||
/// Runs a sync operation for the conversation list.
|
||||
SyncList,
|
||||
|
||||
/// Prints the server Kordophone version.
|
||||
Version,
|
||||
|
||||
/// Configuration options
|
||||
Config {
|
||||
#[command(subcommand)]
|
||||
command: ConfigCommands,
|
||||
},
|
||||
|
||||
/// Waits for signals from the daemon.
|
||||
Signals,
|
||||
|
||||
/// Prints the messages for a conversation.
|
||||
Messages {
|
||||
conversation_id: String,
|
||||
last_message_id: Option<String>,
|
||||
},
|
||||
|
||||
/// Deletes all conversations.
|
||||
DeleteAllConversations,
|
||||
|
||||
/// Enqueues an outgoing message to be sent to a conversation.
|
||||
SendMessage {
|
||||
conversation_id: String,
|
||||
text: String,
|
||||
},
|
||||
|
||||
/// Downloads an attachment from the server to the attachment store. Returns the path to the attachment.
|
||||
DownloadAttachment { attachment_id: String },
|
||||
|
||||
/// Uploads an attachment to the server, returns upload guid.
|
||||
UploadAttachment { path: String },
|
||||
|
||||
/// Marks a conversation as read.
|
||||
MarkConversationAsRead { conversation_id: String },
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum ConfigCommands {
|
||||
/// Prints the current settings.
|
||||
Print,
|
||||
|
||||
/// Sets the server URL.
|
||||
SetServerUrl { url: String },
|
||||
|
||||
/// Sets the username.
|
||||
SetUsername { username: String },
|
||||
}
|
||||
|
||||
impl Commands {
|
||||
pub async fn run(cmd: Commands) -> Result<()> {
|
||||
let mut client = new_daemon_interface()?;
|
||||
match cmd {
|
||||
Commands::Version => client.print_version().await,
|
||||
Commands::Conversations => client.print_conversations().await,
|
||||
Commands::Sync { conversation_id } => client.sync_conversations(conversation_id).await,
|
||||
Commands::SyncList => client.sync_conversations_list().await,
|
||||
Commands::Config { command } => client.config(command).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::DeleteAllConversations => client.delete_all_conversations().await,
|
||||
Commands::SendMessage {
|
||||
conversation_id,
|
||||
text,
|
||||
} => client.enqueue_outgoing_message(conversation_id, text).await,
|
||||
Commands::UploadAttachment { path } => client.upload_attachment(path).await,
|
||||
Commands::DownloadAttachment { attachment_id } => {
|
||||
client.download_attachment(attachment_id).await
|
||||
}
|
||||
Commands::MarkConversationAsRead { conversation_id } => {
|
||||
client.mark_conversation_as_read(conversation_id).await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
604
core/kpcli/src/daemon/xpc.rs
Normal file
604
core/kpcli/src/daemon/xpc.rs
Normal file
@@ -0,0 +1,604 @@
|
||||
use super::{ConfigCommands, DaemonInterface};
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use futures_util::StreamExt;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::ops::Deref;
|
||||
use std::{pin::Pin, task::Poll};
|
||||
|
||||
use xpc_connection::Message;
|
||||
|
||||
use futures::{
|
||||
channel::mpsc::{unbounded as unbounded_channel, UnboundedReceiver, UnboundedSender},
|
||||
Stream,
|
||||
};
|
||||
|
||||
const SERVICE_NAME: &str = "net.buzzert.kordophonecd\0";
|
||||
|
||||
const GET_VERSION_METHOD: &str = "GetVersion";
|
||||
const GET_CONVERSATIONS_METHOD: &str = "GetConversations";
|
||||
|
||||
// We can't use XPCClient from xpc-connection because of some strange decisions with which flags
|
||||
// are passed to xpc_connection_create_mach_service.
|
||||
struct XPCClient {
|
||||
connection: xpc_connection_sys::xpc_connection_t,
|
||||
receiver: UnboundedReceiver<Message>,
|
||||
sender: UnboundedSender<Message>,
|
||||
event_handler_is_running: bool,
|
||||
}
|
||||
|
||||
impl XPCClient {
|
||||
pub fn connect(name: impl AsRef<CStr>) -> Self {
|
||||
use block::ConcreteBlock;
|
||||
use xpc_connection::xpc_object_to_message;
|
||||
use xpc_connection_sys::xpc_connection_resume;
|
||||
use xpc_connection_sys::xpc_connection_set_event_handler;
|
||||
|
||||
let name = name.as_ref();
|
||||
let connection = unsafe {
|
||||
xpc_connection_sys::xpc_connection_create_mach_service(
|
||||
name.as_ptr(),
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
)
|
||||
};
|
||||
|
||||
let (sender, receiver) = unbounded_channel();
|
||||
let sender_clone = sender.clone();
|
||||
|
||||
let block = ConcreteBlock::new(move |event| {
|
||||
let message = xpc_object_to_message(event);
|
||||
sender_clone.unbounded_send(message).ok()
|
||||
});
|
||||
|
||||
let block = block.copy();
|
||||
|
||||
unsafe {
|
||||
xpc_connection_set_event_handler(connection, block.deref() as *const _ as *mut _);
|
||||
xpc_connection_resume(connection);
|
||||
}
|
||||
|
||||
Self {
|
||||
connection,
|
||||
receiver,
|
||||
sender,
|
||||
event_handler_is_running: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_message(&self, message: Message) {
|
||||
use xpc_connection::message_to_xpc_object;
|
||||
use xpc_connection_sys::xpc_connection_send_message;
|
||||
use xpc_connection_sys::xpc_release;
|
||||
|
||||
let xpc_object = message_to_xpc_object(message);
|
||||
unsafe {
|
||||
xpc_connection_send_message(self.connection, xpc_object);
|
||||
xpc_release(xpc_object);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_message_with_reply(&self, message: Message) -> Message {
|
||||
use xpc_connection::message_to_xpc_object;
|
||||
use xpc_connection::xpc_object_to_message;
|
||||
use xpc_connection_sys::{xpc_connection_send_message_with_reply_sync, xpc_release};
|
||||
|
||||
unsafe {
|
||||
let xobj = message_to_xpc_object(message);
|
||||
let reply = xpc_connection_send_message_with_reply_sync(self.connection, xobj);
|
||||
xpc_release(xobj);
|
||||
let msg = xpc_object_to_message(reply);
|
||||
if !reply.is_null() {
|
||||
xpc_release(reply);
|
||||
}
|
||||
msg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for XPCClient {
|
||||
fn drop(&mut self) {
|
||||
use xpc_connection_sys::xpc_object_t;
|
||||
use xpc_connection_sys::xpc_release;
|
||||
|
||||
unsafe { xpc_release(self.connection as xpc_object_t) };
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for XPCClient {
|
||||
type Item = Message;
|
||||
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> Poll<Option<Self::Item>> {
|
||||
match Stream::poll_next(Pin::new(&mut self.receiver), cx) {
|
||||
Poll::Ready(Some(Message::Error(xpc_connection::MessageError::ConnectionInvalid))) => {
|
||||
self.event_handler_is_running = false;
|
||||
Poll::Ready(None)
|
||||
}
|
||||
v => v,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for XPCClient {}
|
||||
|
||||
/// XPC-based implementation of DaemonInterface that sends method calls to the daemon over libxpc.
|
||||
pub struct XpcDaemonInterface;
|
||||
|
||||
impl XpcDaemonInterface {
|
||||
/// Create a new XpcDaemonInterface. No state is held.
|
||||
pub fn new() -> Result<Self> {
|
||||
Ok(Self)
|
||||
}
|
||||
|
||||
fn build_service_name() -> Result<CString> {
|
||||
let service_name = SERVICE_NAME.trim_end_matches('\0');
|
||||
Ok(CString::new(service_name)?)
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
method: &str,
|
||||
args: Option<HashMap<CString, Message>>,
|
||||
) -> HashMap<CString, Message> {
|
||||
let mut request = HashMap::new();
|
||||
request.insert(
|
||||
CString::new("method").unwrap(),
|
||||
Message::String(CString::new(method).unwrap()),
|
||||
);
|
||||
if let Some(arguments) = args {
|
||||
request.insert(
|
||||
CString::new("arguments").unwrap(),
|
||||
Message::Dictionary(arguments),
|
||||
);
|
||||
}
|
||||
request
|
||||
}
|
||||
|
||||
async fn call_method(
|
||||
&self,
|
||||
client: &mut XPCClient,
|
||||
method: &str,
|
||||
args: Option<HashMap<CString, Message>>,
|
||||
) -> anyhow::Result<HashMap<CString, Message>> {
|
||||
let request = Self::build_request(method, args);
|
||||
let reply = client.send_message_with_reply(Message::Dictionary(request));
|
||||
match reply {
|
||||
Message::Dictionary(map) => Ok(map),
|
||||
other => Err(anyhow::anyhow!("Unexpected XPC reply: {:?}", other)),
|
||||
}
|
||||
}
|
||||
|
||||
fn key(k: &str) -> CString {
|
||||
CString::new(k).unwrap()
|
||||
}
|
||||
|
||||
fn get_string<'a>(map: &'a HashMap<CString, Message>, key: &str) -> Option<&'a CStr> {
|
||||
map.get(&Self::key(key)).and_then(|v| match v {
|
||||
Message::String(s) => Some(s.as_c_str()),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_i64_from_str(map: &HashMap<CString, Message>, key: &str) -> Option<i64> {
|
||||
Self::get_string(map, key).and_then(|s| s.to_string_lossy().parse().ok())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl DaemonInterface for XpcDaemonInterface {
|
||||
async fn print_version(&mut self) -> Result<()> {
|
||||
// Build service name and connect
|
||||
let mach_port_name = Self::build_service_name()?;
|
||||
let mut client = XPCClient::connect(&mach_port_name);
|
||||
|
||||
// Call generic method and parse reply
|
||||
let map = self
|
||||
.call_method(&mut client, GET_VERSION_METHOD, None)
|
||||
.await?;
|
||||
if let Some(ver) = Self::get_string(&map, "version") {
|
||||
println!("Server version: {}", ver.to_string_lossy());
|
||||
Ok(())
|
||||
} else if let Some(ty) = Self::get_string(&map, "type") {
|
||||
println!("XPC replied with type: {}", ty.to_string_lossy());
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow::anyhow!(
|
||||
"Unexpected XPC reply payload for GetVersion"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// Remaining methods unimplemented on macOS
|
||||
async fn print_conversations(&mut self) -> Result<()> {
|
||||
// Connect
|
||||
let mach_port_name = Self::build_service_name()?;
|
||||
let mut client = XPCClient::connect(&mach_port_name);
|
||||
|
||||
// Build arguments: limit=100, offset=0 (string-encoded for portability)
|
||||
let mut args = HashMap::new();
|
||||
args.insert(
|
||||
CString::new("limit").unwrap(),
|
||||
Message::String(CString::new("100").unwrap()),
|
||||
);
|
||||
args.insert(
|
||||
CString::new("offset").unwrap(),
|
||||
Message::String(CString::new("0").unwrap()),
|
||||
);
|
||||
|
||||
// Call
|
||||
let reply = self
|
||||
.call_method(&mut client, GET_CONVERSATIONS_METHOD, Some(args))
|
||||
.await?;
|
||||
|
||||
// Expect an array under "conversations"
|
||||
match reply.get(&Self::key("conversations")) {
|
||||
Some(Message::Array(items)) => {
|
||||
println!("Number of conversations: {}", items.len());
|
||||
|
||||
for item in items {
|
||||
if let Message::Dictionary(map) = item {
|
||||
// Convert to PrintableConversation
|
||||
let guid = Self::get_string(map, "guid")
|
||||
.map(|s| s.to_string_lossy().into_owned())
|
||||
.unwrap_or_default();
|
||||
let display_name = Self::get_string(map, "display_name")
|
||||
.map(|s| s.to_string_lossy().into_owned());
|
||||
let last_preview = Self::get_string(map, "last_message_preview")
|
||||
.map(|s| s.to_string_lossy().into_owned());
|
||||
|
||||
let unread_count =
|
||||
Self::get_i64_from_str(map, "unread_count").unwrap_or(0) as i32;
|
||||
let date_ts: i64 = Self::get_i64_from_str(map, "date").unwrap_or(0);
|
||||
|
||||
let participants: Vec<String> = match map.get(&Self::key("participants")) {
|
||||
Some(Message::Array(arr)) => arr
|
||||
.iter()
|
||||
.filter_map(|m| match m {
|
||||
Message::String(s) => Some(s.to_string_lossy().into_owned()),
|
||||
_ => None,
|
||||
})
|
||||
.collect(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
|
||||
// Build PrintableConversation directly
|
||||
let conv = crate::printers::PrintableConversation {
|
||||
guid,
|
||||
display_name,
|
||||
last_message_preview: last_preview,
|
||||
unread_count,
|
||||
date: time::OffsetDateTime::from_unix_timestamp(date_ts)
|
||||
.unwrap_or_else(|_| time::OffsetDateTime::UNIX_EPOCH),
|
||||
participants,
|
||||
};
|
||||
|
||||
println!("{}", crate::printers::ConversationPrinter::new(&conv));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Some(other) => Err(anyhow::anyhow!(
|
||||
"Unexpected conversations payload: {:?}",
|
||||
other
|
||||
)),
|
||||
None => Err(anyhow::anyhow!("Missing conversations in reply")),
|
||||
}
|
||||
}
|
||||
async fn sync_conversations(&mut self, _conversation_id: Option<String>) -> Result<()> {
|
||||
let mach_port_name = Self::build_service_name()?;
|
||||
let mut client = XPCClient::connect(&mach_port_name);
|
||||
|
||||
if let Some(id) = _conversation_id {
|
||||
let mut args = HashMap::new();
|
||||
args.insert(
|
||||
Self::key("conversation_id"),
|
||||
Message::String(CString::new(id).unwrap()),
|
||||
);
|
||||
let _ = self
|
||||
.call_method(&mut client, "SyncConversation", Some(args))
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let _ = self
|
||||
.call_method(&mut client, "SyncAllConversations", None)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
async fn sync_conversations_list(&mut self) -> Result<()> {
|
||||
let mach_port_name = Self::build_service_name()?;
|
||||
let mut client = XPCClient::connect(&mach_port_name);
|
||||
let _ = self
|
||||
.call_method(&mut client, "SyncConversationList", None)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
async fn print_messages(
|
||||
&mut self,
|
||||
_conversation_id: String,
|
||||
_last_message_id: Option<String>,
|
||||
) -> Result<()> {
|
||||
let mach_port_name = Self::build_service_name()?;
|
||||
let mut client = XPCClient::connect(&mach_port_name);
|
||||
|
||||
let mut args = HashMap::new();
|
||||
args.insert(
|
||||
Self::key("conversation_id"),
|
||||
Message::String(CString::new(_conversation_id).unwrap()),
|
||||
);
|
||||
if let Some(last) = _last_message_id {
|
||||
args.insert(
|
||||
Self::key("last_message_id"),
|
||||
Message::String(CString::new(last).unwrap()),
|
||||
);
|
||||
}
|
||||
|
||||
let reply = self
|
||||
.call_method(&mut client, "GetMessages", Some(args))
|
||||
.await?;
|
||||
match reply.get(&Self::key("messages")) {
|
||||
Some(Message::Array(items)) => {
|
||||
println!("Number of messages: {}", items.len());
|
||||
for item in items {
|
||||
if let Message::Dictionary(map) = item {
|
||||
let guid = Self::get_string(map, "id")
|
||||
.map(|s| s.to_string_lossy().into_owned())
|
||||
.unwrap_or_default();
|
||||
let sender = Self::get_string(map, "sender")
|
||||
.map(|s| s.to_string_lossy().into_owned())
|
||||
.unwrap_or_default();
|
||||
let text = Self::get_string(map, "text")
|
||||
.map(|s| s.to_string_lossy().into_owned())
|
||||
.unwrap_or_default();
|
||||
let date_ts = Self::get_i64_from_str(map, "date").unwrap_or(0);
|
||||
let msg = crate::printers::PrintableMessage {
|
||||
guid,
|
||||
date: time::OffsetDateTime::from_unix_timestamp(date_ts)
|
||||
.unwrap_or_else(|_| time::OffsetDateTime::UNIX_EPOCH),
|
||||
sender,
|
||||
text,
|
||||
file_transfer_guids: vec![],
|
||||
attachment_metadata: None,
|
||||
};
|
||||
println!("{}", crate::printers::MessagePrinter::new(&msg));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(anyhow::anyhow!("Unexpected messages payload")),
|
||||
}
|
||||
}
|
||||
async fn enqueue_outgoing_message(
|
||||
&mut self,
|
||||
_conversation_id: String,
|
||||
_text: String,
|
||||
) -> Result<()> {
|
||||
let mach_port_name = Self::build_service_name()?;
|
||||
let mut client = XPCClient::connect(&mach_port_name);
|
||||
let mut args = HashMap::new();
|
||||
args.insert(
|
||||
Self::key("conversation_id"),
|
||||
Message::String(CString::new(_conversation_id).unwrap()),
|
||||
);
|
||||
args.insert(
|
||||
Self::key("text"),
|
||||
Message::String(CString::new(_text).unwrap()),
|
||||
);
|
||||
let reply = self
|
||||
.call_method(&mut client, "SendMessage", Some(args))
|
||||
.await?;
|
||||
if let Some(uuid) = Self::get_string(&reply, "uuid") {
|
||||
println!("Outgoing message ID: {}", uuid.to_string_lossy());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
async fn wait_for_signals(&mut self) -> Result<()> {
|
||||
let mach_port_name = Self::build_service_name()?;
|
||||
let mut client = XPCClient::connect(&mach_port_name);
|
||||
|
||||
// Subscribe to begin receiving signals on this connection
|
||||
eprintln!("[kpcli] Sending SubscribeSignals");
|
||||
client.send_message(Message::Dictionary(Self::build_request(
|
||||
"SubscribeSignals",
|
||||
None,
|
||||
)));
|
||||
|
||||
println!("Waiting for XPC signals...");
|
||||
while let Some(msg) = client.next().await {
|
||||
match msg {
|
||||
Message::Dictionary(map) => {
|
||||
eprintln!("[kpcli] Received signal dictionary");
|
||||
let name_key = Self::key("name");
|
||||
let args_key = Self::key("arguments");
|
||||
let name = match map.get(&name_key) {
|
||||
Some(Message::String(s)) => s.to_string_lossy().into_owned(),
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
match name.as_str() {
|
||||
"ConversationsUpdated" => {
|
||||
println!("Signal: Conversations updated");
|
||||
}
|
||||
"MessagesUpdated" => {
|
||||
if let Some(Message::Dictionary(args)) = map.get(&args_key) {
|
||||
if let Some(Message::String(cid)) =
|
||||
args.get(&Self::key("conversation_id"))
|
||||
{
|
||||
println!(
|
||||
"Signal: Messages updated for conversation {}",
|
||||
cid.to_string_lossy()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
"UpdateStreamReconnected" => {
|
||||
println!("Signal: Update stream reconnected");
|
||||
}
|
||||
"AttachmentDownloadCompleted" => {
|
||||
if let Some(Message::Dictionary(args)) = map.get(&args_key) {
|
||||
if let Some(Message::String(aid)) =
|
||||
args.get(&Self::key("attachment_id"))
|
||||
{
|
||||
println!(
|
||||
"Signal: Attachment downloaded: {}",
|
||||
aid.to_string_lossy()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
"AttachmentDownloadFailed" => {
|
||||
if let Some(Message::Dictionary(args)) = map.get(&args_key) {
|
||||
if let Some(Message::String(aid)) =
|
||||
args.get(&Self::key("attachment_id"))
|
||||
{
|
||||
eprintln!(
|
||||
"Signal: Attachment download failed: {}",
|
||||
aid.to_string_lossy()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
"AttachmentUploadCompleted" => {
|
||||
if let Some(Message::Dictionary(args)) = map.get(&args_key) {
|
||||
let upload = args
|
||||
.get(&Self::key("upload_guid"))
|
||||
.and_then(|v| match v {
|
||||
Message::String(s) => {
|
||||
Some(s.to_string_lossy().into_owned())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let attachment = args
|
||||
.get(&Self::key("attachment_guid"))
|
||||
.and_then(|v| match v {
|
||||
Message::String(s) => {
|
||||
Some(s.to_string_lossy().into_owned())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
println!(
|
||||
"Signal: Attachment uploaded: upload={}, attachment={}",
|
||||
upload, attachment
|
||||
);
|
||||
}
|
||||
}
|
||||
"ConfigChanged" => {
|
||||
println!("Signal: Config changed");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Message::Error(xpc_connection::MessageError::ConnectionInvalid) => {
|
||||
eprintln!("[kpcli] XPC connection invalid");
|
||||
break;
|
||||
}
|
||||
other => {
|
||||
eprintln!("[kpcli] Unexpected XPC message: {:?}", other);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
async fn config(&mut self, _cmd: ConfigCommands) -> Result<()> {
|
||||
let mach_port_name = Self::build_service_name()?;
|
||||
let mut client = XPCClient::connect(&mach_port_name);
|
||||
match _cmd {
|
||||
ConfigCommands::Print => {
|
||||
let reply = self
|
||||
.call_method(&mut client, "GetAllSettings", None)
|
||||
.await?;
|
||||
let server_url = Self::get_string(&reply, "server_url")
|
||||
.map(|s| s.to_string_lossy().into_owned())
|
||||
.unwrap_or_default();
|
||||
let username = Self::get_string(&reply, "username")
|
||||
.map(|s| s.to_string_lossy().into_owned())
|
||||
.unwrap_or_default();
|
||||
let table =
|
||||
prettytable::table!([b->"Server URL", &server_url], [b->"Username", &username]);
|
||||
table.printstd();
|
||||
Ok(())
|
||||
}
|
||||
ConfigCommands::SetServerUrl { url } => {
|
||||
let mut args = HashMap::new();
|
||||
args.insert(
|
||||
Self::key("server_url"),
|
||||
Message::String(CString::new(url).unwrap()),
|
||||
);
|
||||
let _ = self
|
||||
.call_method(&mut client, "UpdateSettings", Some(args))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
ConfigCommands::SetUsername { username } => {
|
||||
let mut args = HashMap::new();
|
||||
args.insert(
|
||||
Self::key("username"),
|
||||
Message::String(CString::new(username).unwrap()),
|
||||
);
|
||||
let _ = self
|
||||
.call_method(&mut client, "UpdateSettings", Some(args))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
async fn delete_all_conversations(&mut self) -> Result<()> {
|
||||
let mach_port_name = Self::build_service_name()?;
|
||||
let mut client = XPCClient::connect(&mach_port_name);
|
||||
let _ = self
|
||||
.call_method(&mut client, "DeleteAllConversations", None)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
async fn download_attachment(&mut self, _attachment_id: String) -> Result<()> {
|
||||
let mach_port_name = Self::build_service_name()?;
|
||||
let mut client = XPCClient::connect(&mach_port_name);
|
||||
let mut args = HashMap::new();
|
||||
args.insert(
|
||||
Self::key("attachment_id"),
|
||||
Message::String(CString::new(_attachment_id).unwrap()),
|
||||
);
|
||||
args.insert(
|
||||
Self::key("preview"),
|
||||
Message::String(CString::new("false").unwrap()),
|
||||
);
|
||||
let _ = self
|
||||
.call_method(&mut client, "DownloadAttachment", Some(args))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
async fn upload_attachment(&mut self, _path: String) -> Result<()> {
|
||||
let mach_port_name = Self::build_service_name()?;
|
||||
let mut client = XPCClient::connect(&mach_port_name);
|
||||
let mut args = HashMap::new();
|
||||
args.insert(
|
||||
Self::key("path"),
|
||||
Message::String(CString::new(_path).unwrap()),
|
||||
);
|
||||
let reply = self
|
||||
.call_method(&mut client, "UploadAttachment", Some(args))
|
||||
.await?;
|
||||
if let Some(guid) = Self::get_string(&reply, "upload_guid") {
|
||||
println!("Upload GUID: {}", guid.to_string_lossy());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
async fn mark_conversation_as_read(&mut self, _conversation_id: String) -> Result<()> {
|
||||
let mach_port_name = Self::build_service_name()?;
|
||||
let mut client = XPCClient::connect(&mach_port_name);
|
||||
let mut args = HashMap::new();
|
||||
args.insert(
|
||||
Self::key("conversation_id"),
|
||||
Message::String(CString::new(_conversation_id).unwrap()),
|
||||
);
|
||||
let _ = self
|
||||
.call_method(&mut client, "MarkConversationAsRead", Some(args))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user