Merge branch 'wip/macos-xpc'
* wip/macos-xpc: (23 commits) auth: try switching to platform agnostic auth store daemon: fix crash when misconfigured xpc: better file descriptor handling xpc: adds OpenAttachmentFd xpc: full attachment data sync policy: only ignore empty bodies if there are no attachments xpc: include attachment guids cargo fmt xpc: Some cleanup xpc: refactor -- separate rpc impl and xpc glue xpc: refactor, less chatty logging xpc: Use reply port when replying to RPC messages cargo fmt xpc: implement signals xpc: implement rest of methods in kpcli except signals. cargo fmt xpc: Better type unpacking xpc: implement GetConversations xpc: kpcli: clean up client interface xpc: generic interface for dispatching methods ...
This commit is contained in:
252
Cargo.lock
generated
252
Cargo.lock
generated
@@ -169,6 +169,29 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.58.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f8523b410d7187a43085e7e064416ea32ded16bd0a4e6fc025e21616d01258f"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"clap 2.34.0",
|
||||
"env_logger 0.8.4",
|
||||
"lazy_static",
|
||||
"lazycell",
|
||||
"log",
|
||||
"peeking_take_while",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"rustc-hash",
|
||||
"shlex",
|
||||
"which",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
@@ -177,9 +200,15 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.5.0"
|
||||
version = "2.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
|
||||
checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d"
|
||||
|
||||
[[package]]
|
||||
name = "block"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
@@ -208,6 +237,15 @@ version = "1.0.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b"
|
||||
|
||||
[[package]]
|
||||
name = "cexpr"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
@@ -229,6 +267,17 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clang-sys"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"libc",
|
||||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "2.34.0"
|
||||
@@ -300,6 +349,16 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.6"
|
||||
@@ -612,6 +671,19 @@ dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"humantime",
|
||||
"log",
|
||||
"regex",
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.11.8"
|
||||
@@ -668,6 +740,21 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.31"
|
||||
@@ -675,6 +762,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -683,6 +771,23 @@ version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.31"
|
||||
@@ -712,10 +817,13 @@ version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
@@ -760,6 +868,12 @@ version = "0.28.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.3.26"
|
||||
@@ -857,6 +971,12 @@ version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "humantime"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.28"
|
||||
@@ -991,12 +1111,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "keyring"
|
||||
version = "3.6.2"
|
||||
version = "3.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1961983669d57bdfe6c0f3ef8e4c229b5ef751afcc7d87e4271d2f71f6ccfa8b"
|
||||
checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c"
|
||||
dependencies = [
|
||||
"dbus-secret-service",
|
||||
"log",
|
||||
"security-framework 2.10.0",
|
||||
"security-framework 3.3.0",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1008,7 +1131,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"chrono",
|
||||
"ctor",
|
||||
"env_logger",
|
||||
"env_logger 0.11.8",
|
||||
"futures-util",
|
||||
"hyper",
|
||||
"hyper-tls",
|
||||
@@ -1050,6 +1173,7 @@ version = "1.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"block",
|
||||
"chrono",
|
||||
"dbus",
|
||||
"dbus-codegen",
|
||||
@@ -1057,18 +1181,22 @@ dependencies = [
|
||||
"dbus-tokio",
|
||||
"dbus-tree",
|
||||
"directories",
|
||||
"env_logger",
|
||||
"env_logger 0.11.8",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"keyring",
|
||||
"kordophone",
|
||||
"kordophone-db",
|
||||
"log",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tokio-condvar",
|
||||
"uuid",
|
||||
"xpc-connection",
|
||||
"xpc-connection-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1077,12 +1205,14 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"block",
|
||||
"clap 4.5.20",
|
||||
"dbus",
|
||||
"dbus-codegen",
|
||||
"dbus-tree",
|
||||
"dotenv",
|
||||
"env_logger",
|
||||
"env_logger 0.11.8",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"kordophone",
|
||||
"kordophone-db",
|
||||
@@ -1092,6 +1222,8 @@ dependencies = [
|
||||
"serde_json",
|
||||
"time",
|
||||
"tokio",
|
||||
"xpc-connection",
|
||||
"xpc-connection-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1100,6 +1232,12 @@ version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "lazycell"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.172"
|
||||
@@ -1115,13 +1253,23 @@ dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.8.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"bitflags 2.9.3",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -1218,11 +1366,21 @@ dependencies = [
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
"security-framework 2.10.0",
|
||||
"security-framework-sys",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "5.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08959a387a676302eebf4ddbcbc611da04285579f76f88ee0506c63b1a61dd4b"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num"
|
||||
version = "0.4.3"
|
||||
@@ -1323,7 +1481,7 @@ version = "0.10.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"bitflags 2.9.3",
|
||||
"cfg-if",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
@@ -1390,6 +1548,12 @@ dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "peeking_take_while"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.14"
|
||||
@@ -1610,13 +1774,19 @@ version = "0.1.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"bitflags 2.9.3",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
@@ -1657,7 +1827,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation",
|
||||
"core-foundation 0.9.4",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "3.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c"
|
||||
dependencies = [
|
||||
"bitflags 2.9.3",
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
@@ -1665,9 +1848,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.10.0"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef"
|
||||
checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
@@ -1733,6 +1916,12 @@ dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.1"
|
||||
@@ -2219,6 +2408,15 @@ version = "0.2.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d"
|
||||
|
||||
[[package]]
|
||||
name = "which"
|
||||
version = "3.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
@@ -2413,7 +2611,7 @@ version = "0.39.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"bitflags 2.9.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2421,3 +2619,27 @@ name = "xml-rs"
|
||||
version = "0.8.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4"
|
||||
|
||||
[[package]]
|
||||
name = "xpc-connection"
|
||||
version = "0.2.3"
|
||||
source = "git+https://github.com/dfrankland/xpc-connection-rs.git?rev=cd4fb3d#cd4fb3d05edb4292ccb9566ae27cdeb874222d2a"
|
||||
dependencies = [
|
||||
"block",
|
||||
"futures",
|
||||
"xpc-connection-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xpc-connection-sys"
|
||||
version = "0.1.1"
|
||||
source = "git+https://github.com/dfrankland/xpc-connection-rs.git?rev=cd4fb3d#cd4fb3d05edb4292ccb9566ae27cdeb874222d2a"
|
||||
dependencies = [
|
||||
"bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
||||
|
||||
@@ -50,12 +50,12 @@ impl Conversation {
|
||||
|
||||
impl PartialEq for Conversation {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.guid == other.guid &&
|
||||
self.unread_count == other.unread_count &&
|
||||
self.display_name == other.display_name &&
|
||||
self.last_message_preview == other.last_message_preview &&
|
||||
self.date == other.date &&
|
||||
self.participants == other.participants
|
||||
self.guid == other.guid
|
||||
&& self.unread_count == other.unread_count
|
||||
&& self.display_name == other.display_name
|
||||
&& self.last_message_preview == other.last_message_preview
|
||||
&& self.date == other.date
|
||||
&& self.participants == other.participants
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,10 @@ impl From<kordophone::model::Conversation> for Conversation {
|
||||
participants: value
|
||||
.participant_display_names
|
||||
.into_iter()
|
||||
.map(|p| Participant::Remote { handle: p, contact_id: None }) // todo: this is wrong
|
||||
.map(|p| Participant::Remote {
|
||||
handle: p,
|
||||
contact_id: None,
|
||||
}) // todo: this is wrong
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,10 @@ impl From<Record> for Message {
|
||||
.and_then(|json| serde_json::from_str(&json).ok());
|
||||
|
||||
let message_sender = match record.sender_participant_handle {
|
||||
Some(handle) => Participant::Remote { handle, contact_id: None },
|
||||
Some(handle) => Participant::Remote {
|
||||
handle,
|
||||
contact_id: None,
|
||||
},
|
||||
None => Participant::Me,
|
||||
};
|
||||
Self {
|
||||
|
||||
@@ -22,16 +22,18 @@ pub struct InsertableRecord {
|
||||
impl From<Participant> for InsertableRecord {
|
||||
fn from(participant: Participant) -> Self {
|
||||
match participant {
|
||||
Participant::Me => InsertableRecord {
|
||||
handle: "me".to_string(),
|
||||
is_me: true,
|
||||
contact_id: None,
|
||||
},
|
||||
Participant::Remote { handle, contact_id, .. } => InsertableRecord {
|
||||
handle,
|
||||
is_me: false,
|
||||
contact_id,
|
||||
},
|
||||
Participant::Me => InsertableRecord {
|
||||
handle: "me".to_string(),
|
||||
is_me: true,
|
||||
contact_id: None,
|
||||
},
|
||||
Participant::Remote {
|
||||
handle, contact_id, ..
|
||||
} => InsertableRecord {
|
||||
handle,
|
||||
is_me: false,
|
||||
contact_id,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,16 +64,18 @@ impl From<Record> for Participant {
|
||||
impl From<Participant> for Record {
|
||||
fn from(participant: Participant) -> Self {
|
||||
match participant {
|
||||
Participant::Me => Record {
|
||||
handle: "me".to_string(),
|
||||
is_me: true,
|
||||
contact_id: None,
|
||||
},
|
||||
Participant::Remote { handle, contact_id, .. } => Record {
|
||||
handle,
|
||||
is_me: false,
|
||||
contact_id,
|
||||
},
|
||||
Participant::Me => Record {
|
||||
handle: "me".to_string(),
|
||||
is_me: true,
|
||||
contact_id: None,
|
||||
},
|
||||
Participant::Remote {
|
||||
handle, contact_id, ..
|
||||
} => Record {
|
||||
handle,
|
||||
is_me: false,
|
||||
contact_id,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,9 +28,13 @@ impl From<kordophone::model::Message> for Message {
|
||||
contact_id: None,
|
||||
|
||||
// Weird server quirk: some sender handles are encoded with control characters.
|
||||
handle: sender.chars()
|
||||
.filter(|c| !c.is_control() && !matches!(c,
|
||||
'\u{202A}' | // LRE
|
||||
handle: sender
|
||||
.chars()
|
||||
.filter(|c| {
|
||||
!c.is_control()
|
||||
&& !matches!(
|
||||
c,
|
||||
'\u{202A}' | // LRE
|
||||
'\u{202B}' | // RLE
|
||||
'\u{202C}' | // PDF
|
||||
'\u{202D}' | // LRO
|
||||
@@ -38,8 +42,9 @@ impl From<kordophone::model::Message> for Message {
|
||||
'\u{2066}' | // LRI
|
||||
'\u{2067}' | // RLI
|
||||
'\u{2068}' | // FSI
|
||||
'\u{2069}' // PDI
|
||||
))
|
||||
'\u{2069}' // PDI
|
||||
)
|
||||
})
|
||||
.collect::<String>(),
|
||||
},
|
||||
|
||||
|
||||
@@ -377,7 +377,11 @@ impl<'a> Repository<'a> {
|
||||
fn get_or_create_participant(&mut self, participant: &Participant) -> Option<String> {
|
||||
match participant {
|
||||
Participant::Me => None,
|
||||
Participant::Remote { handle: p_handle, contact_id: c_id, .. } => {
|
||||
Participant::Remote {
|
||||
handle: p_handle,
|
||||
contact_id: c_id,
|
||||
..
|
||||
} => {
|
||||
use crate::schema::participants::dsl::*;
|
||||
|
||||
let existing_participant = participants
|
||||
|
||||
@@ -271,10 +271,14 @@ impl<K: AuthenticationStore + Send + Sync> APIInterface for HTTPAPIClient<K> {
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
async fn mark_conversation_as_read(&mut self, conversation_id: &ConversationID) -> Result<(), Self::Error> {
|
||||
async fn mark_conversation_as_read(
|
||||
&mut self,
|
||||
conversation_id: &ConversationID,
|
||||
) -> Result<(), Self::Error> {
|
||||
// SERVER JANK: This should be POST, but it's GET for some reason.
|
||||
let endpoint = format!("markConversation?guid={}", conversation_id);
|
||||
self.response_with_body_retry(&endpoint, Method::GET, Body::empty, true).await?;
|
||||
self.response_with_body_retry(&endpoint, Method::GET, Body::empty, true)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,10 @@ pub trait APIInterface {
|
||||
async fn authenticate(&mut self, credentials: Credentials) -> Result<JwtToken, Self::Error>;
|
||||
|
||||
// (GET) /markConversation
|
||||
async fn mark_conversation_as_read(&mut self, conversation_id: &ConversationID) -> Result<(), Self::Error>;
|
||||
async fn mark_conversation_as_read(
|
||||
&mut self,
|
||||
conversation_id: &ConversationID,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
// (WS) /updates
|
||||
async fn open_event_socket(
|
||||
|
||||
@@ -149,7 +149,10 @@ impl APIInterface for TestClient {
|
||||
Ok(String::from("test"))
|
||||
}
|
||||
|
||||
async fn mark_conversation_as_read(&mut self, conversation_id: &ConversationID) -> Result<(), Self::Error> {
|
||||
async fn mark_conversation_as_read(
|
||||
&mut self,
|
||||
conversation_id: &ConversationID,
|
||||
) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ chrono = "0.4.38"
|
||||
directories = "6.0.0"
|
||||
env_logger = "0.11.6"
|
||||
futures-util = "0.3.31"
|
||||
keyring = { version = "3.6.2", features = ["sync-secret-service"] }
|
||||
keyring = { version = "3.6.3", features = ["apple-native", "sync-secret-service"] }
|
||||
kordophone = { path = "../kordophone" }
|
||||
kordophone-db = { path = "../kordophone-db" }
|
||||
log = "0.4.25"
|
||||
@@ -23,18 +23,25 @@ tokio-condvar = "0.3.0"
|
||||
uuid = "1.16.0"
|
||||
once_cell = "1.19.0"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
# D-Bus dependencies only on Linux
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
dbus = { version = "0.9.7", features = ["futures"] }
|
||||
dbus-crossroads = "0.5.2"
|
||||
dbus-tokio = "0.7.6"
|
||||
dbus-tree = "0.9.2"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.build-dependencies]
|
||||
# D-Bus codegen only on Linux
|
||||
[target.'cfg(target_os = "linux")'.build-dependencies]
|
||||
dbus-codegen = "0.10.0"
|
||||
dbus-crossroads = "0.5.1"
|
||||
|
||||
# XPC (libxpc) interface for macOS IPC
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
block = "0.1.6"
|
||||
futures = "0.3.31"
|
||||
xpc-connection = { git = "https://github.com/dfrankland/xpc-connection-rs.git", rev = "cd4fb3d", package = "xpc-connection" }
|
||||
xpc-connection-sys = { git = "https://github.com/dfrankland/xpc-connection-rs.git", rev = "cd4fb3d", package = "xpc-connection-sys" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
[package.metadata.generate-rpm]
|
||||
assets = [
|
||||
@@ -42,3 +49,4 @@ assets = [
|
||||
{ source = "../target/release/kpcli", dest = "/usr/bin/kpcli", mode = "755" },
|
||||
{ source = "include/net.buzzert.kordophonecd.service", dest = "/usr/share/dbus-1/services/net.buzzert.kordophonecd.service", mode = "644" },
|
||||
]
|
||||
|
||||
|
||||
@@ -13,3 +13,25 @@ cargo build --release
|
||||
strip -s target/release/kordophoned
|
||||
cargo generate-rpm
|
||||
```
|
||||
|
||||
## Running on macOS
|
||||
|
||||
Before any client can talk to the kordophone daemon on macOS, the XPC service needs to be manually registered with launchd.
|
||||
|
||||
- Register using `launchctl load net.buzzert.kordophonecd.plist`
|
||||
|
||||
Plans are to embed this into the app executable, which would then not need to be manually registered (only via the following Swift code):
|
||||
|
||||
```swift
|
||||
try? SMAppService.agent(plistName: "net.buzzert.kordophonecd.plist").register()
|
||||
```
|
||||
|
||||
and the following in Info.plist:
|
||||
|
||||
```xml
|
||||
<key>Label</key><string>net.buzzert.kordophonecd</string>
|
||||
<key>BundleProgram</key><string>Contents/MacOS/kordophoned</string>
|
||||
<key>MachServices</key><dict><key>net.buzzert.kordophonecd</key><true/></dict>
|
||||
<key>KeepAlive</key><true/>
|
||||
```
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ fn main() {
|
||||
|
||||
let xml = std::fs::read_to_string(KORDOPHONE_XML).expect("Error reading server dbus interface");
|
||||
|
||||
let output = dbus_codegen::generate(&xml, &opts)
|
||||
.expect("Error generating server dbus interface");
|
||||
let output =
|
||||
dbus_codegen::generate(&xml, &opts).expect("Error generating server dbus interface");
|
||||
|
||||
std::fs::write(out_path, output).expect("Error writing server dbus code");
|
||||
|
||||
|
||||
35
kordophoned/include/net.buzzert.kordophonecd.plist
Normal file
35
kordophoned/include/net.buzzert.kordophonecd.plist
Normal file
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>net.buzzert.kordophonecd</string>
|
||||
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/Users/buzzert/src/kordophone/kordophone-rs/target/debug/kordophoned</string>
|
||||
</array>
|
||||
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>RUST_LOG</key>
|
||||
<string>info</string>
|
||||
</dict>
|
||||
|
||||
<key>MachServices</key>
|
||||
<dict>
|
||||
<key>net.buzzert.kordophonecd</key>
|
||||
<true/>
|
||||
</dict>
|
||||
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
|
||||
<key>StandardOutPath</key>
|
||||
<string>/tmp/kordophoned.out.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/tmp/kordophoned.err.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -21,10 +21,7 @@ impl DatabaseAuthenticationStore {
|
||||
|
||||
#[async_trait]
|
||||
impl AuthenticationStore for DatabaseAuthenticationStore {
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn get_credentials(&mut self) -> Option<Credentials> {
|
||||
use keyring::secret_service::SsCredential;
|
||||
|
||||
self.database
|
||||
.lock()
|
||||
.await
|
||||
@@ -38,15 +35,14 @@ impl AuthenticationStore for DatabaseAuthenticationStore {
|
||||
|
||||
match username {
|
||||
Some(username) => {
|
||||
let credential = SsCredential::new_with_target(
|
||||
None,
|
||||
"net.buzzert.kordophonecd",
|
||||
&username,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let password: Result<String> =
|
||||
Entry::new_with_credential(Box::new(credential)).get_password();
|
||||
let credential_res = Entry::new("net.buzzert.kordophonecd", &username);
|
||||
let password: Result<String> = match credential_res {
|
||||
Ok(credential) => credential.get_password(),
|
||||
Err(e) => {
|
||||
log::error!("error creating keyring credential: {}", e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
match password {
|
||||
Ok(password) => Some(Credentials { username, password }),
|
||||
@@ -62,11 +58,6 @@ impl AuthenticationStore for DatabaseAuthenticationStore {
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
async fn get_credentials(&mut self) -> Option<Credentials> {
|
||||
None
|
||||
}
|
||||
|
||||
async fn get_token(&mut self) -> Option<String> {
|
||||
self.database
|
||||
.lock()
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use super::ContactResolverBackend;
|
||||
use dbus::blocking::Connection;
|
||||
use dbus::arg::{RefArg, Variant};
|
||||
use dbus::blocking::Connection;
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EDSContactResolverBackend;
|
||||
@@ -40,8 +40,13 @@ static ADDRESS_BOOK_HANDLE: OnceCell<Mutex<AddressBookHandle>> = OnceCell::new()
|
||||
|
||||
/// Check whether a given well-known name currently has an owner on the bus.
|
||||
fn name_has_owner(conn: &Connection, name: &str) -> bool {
|
||||
let proxy = conn.with_proxy("org.freedesktop.DBus", "/org/freedesktop/DBus", Duration::from_secs(2));
|
||||
let result: Result<(bool,), _> = proxy.method_call("org.freedesktop.DBus", "NameHasOwner", (name.to_string(),));
|
||||
let proxy = conn.with_proxy(
|
||||
"org.freedesktop.DBus",
|
||||
"/org/freedesktop/DBus",
|
||||
Duration::from_secs(2),
|
||||
);
|
||||
let result: Result<(bool,), _> =
|
||||
proxy.method_call("org.freedesktop.DBus", "NameHasOwner", (name.to_string(),));
|
||||
result.map(|(b,)| b).unwrap_or(false)
|
||||
}
|
||||
|
||||
@@ -99,10 +104,7 @@ fn ensure_address_book_uid(conn: &Connection) -> anyhow::Result<String> {
|
||||
|
||||
// The GetManagedObjects reply is the usual ObjectManager map.
|
||||
let (managed_objects,): (
|
||||
HashMap<
|
||||
dbus::Path<'static>,
|
||||
HashMap<String, HashMap<String, Variant<Box<dyn RefArg>>>>,
|
||||
>,
|
||||
HashMap<dbus::Path<'static>, HashMap<String, HashMap<String, Variant<Box<dyn RefArg>>>>>,
|
||||
) = source_manager_proxy.method_call(
|
||||
"org.freedesktop.DBus.ObjectManager",
|
||||
"GetManagedObjects",
|
||||
@@ -153,10 +155,7 @@ fn data_contains_address_book_backend(data: &str) -> bool {
|
||||
/// Open the Evolution address book referenced by `source_uid` and return the
|
||||
/// pair `(object_path, bus_name)` that identifies the newly created D-Bus
|
||||
/// proxy.
|
||||
fn open_address_book(
|
||||
conn: &Connection,
|
||||
source_uid: &str,
|
||||
) -> anyhow::Result<(String, String)> {
|
||||
fn open_address_book(conn: &Connection, source_uid: &str) -> anyhow::Result<(String, String)> {
|
||||
let factory_proxy = conn.with_proxy(
|
||||
"org.gnome.evolution.dataserver.AddressBook10",
|
||||
"/org/gnome/evolution/dataserver/AddressBookFactory",
|
||||
@@ -177,11 +176,8 @@ fn open_address_book(
|
||||
/// calls the `Open` method once per process. We ignore any error here
|
||||
/// because the backend might already be open.
|
||||
fn ensure_address_book_open(proxy: &dbus::blocking::Proxy<&Connection>) {
|
||||
let _: Result<(), _> = proxy.method_call(
|
||||
"org.gnome.evolution.dataserver.AddressBook",
|
||||
"Open",
|
||||
(),
|
||||
);
|
||||
let _: Result<(), _> =
|
||||
proxy.method_call("org.gnome.evolution.dataserver.AddressBook", "Open", ());
|
||||
}
|
||||
|
||||
impl ContactResolverBackend for EDSContactResolverBackend {
|
||||
|
||||
@@ -15,7 +15,9 @@ pub struct EDSContactResolverBackend;
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
impl Default for EDSContactResolverBackend {
|
||||
fn default() -> Self { EDSContactResolverBackend }
|
||||
fn default() -> Self {
|
||||
EDSContactResolverBackend
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
@@ -56,7 +58,11 @@ where
|
||||
T: Default,
|
||||
{
|
||||
pub fn new(backend: T) -> Self {
|
||||
Self { backend, display_name_cache: HashMap::new(), contact_id_cache: HashMap::new() }
|
||||
Self {
|
||||
backend,
|
||||
display_name_cache: HashMap::new(),
|
||||
contact_id_cache: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_contact_id(&mut self, address: &str) -> Option<AnyContactID> {
|
||||
@@ -66,7 +72,8 @@ where
|
||||
|
||||
let id = self.backend.resolve_contact_id(address).map(|id| id.into());
|
||||
if let Some(ref id) = id {
|
||||
self.contact_id_cache.insert(address.to_string(), id.clone());
|
||||
self.contact_id_cache
|
||||
.insert(address.to_string(), id.clone());
|
||||
}
|
||||
|
||||
id
|
||||
@@ -80,7 +87,8 @@ where
|
||||
let backend_contact_id: T::ContactID = T::ContactID::from((*contact_id).clone());
|
||||
let display_name = self.backend.get_contact_display_name(&backend_contact_id);
|
||||
if let Some(ref display_name) = display_name {
|
||||
self.display_name_cache.insert(contact_id.to_string(), display_name.clone());
|
||||
self.display_name_cache
|
||||
.insert(contact_id.to_string(), display_name.clone());
|
||||
}
|
||||
|
||||
display_name
|
||||
|
||||
@@ -522,12 +522,18 @@ impl Daemon {
|
||||
.await?
|
||||
{
|
||||
for p in &saved.participants {
|
||||
if let DbParticipant::Remote { handle, contact_id: None } = p {
|
||||
if let DbParticipant::Remote {
|
||||
handle,
|
||||
contact_id: None,
|
||||
} = p
|
||||
{
|
||||
log::trace!(target: target::SYNC, "Resolving contact id for participant: {}", handle);
|
||||
if let Some(contact) = contact_resolver.resolve_contact_id(handle) {
|
||||
log::trace!(target: target::SYNC, "Resolved contact id for participant: {}", contact);
|
||||
let _ = database
|
||||
.with_repository(|r| r.update_participant_contact(&handle, &contact))
|
||||
.with_repository(|r| {
|
||||
r.update_participant_contact(&handle, &contact)
|
||||
})
|
||||
.await;
|
||||
} else {
|
||||
log::trace!(target: target::SYNC, "No contact id found for participant: {}", handle);
|
||||
@@ -620,7 +626,10 @@ impl Daemon {
|
||||
// the typing indicator or stuff like that. In the future, we need to move to ChatItems instead of Messages.
|
||||
let insertable_messages: Vec<kordophone::model::Message> = messages
|
||||
.into_iter()
|
||||
.filter(|m| !m.text.is_empty() && !m.text.trim().is_empty())
|
||||
.filter(|m| {
|
||||
(!m.text.is_empty() && !m.text.trim().is_empty())
|
||||
|| !m.file_transfer_guids.is_empty()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let db_messages: Vec<kordophone_db::models::Message> = insertable_messages
|
||||
@@ -663,11 +672,11 @@ impl Daemon {
|
||||
signal_sender: &Sender<Signal>,
|
||||
) -> Result<()> {
|
||||
log::debug!(target: target::DAEMON, "Updating conversation metadata: {}", conversation.guid);
|
||||
let updated = database.with_repository(|r| r.merge_conversation_metadata(conversation)).await?;
|
||||
let updated = database
|
||||
.with_repository(|r| r.merge_conversation_metadata(conversation))
|
||||
.await?;
|
||||
if updated {
|
||||
signal_sender
|
||||
.send(Signal::ConversationsUpdated)
|
||||
.await?;
|
||||
signal_sender.send(Signal::ConversationsUpdated).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -692,7 +701,13 @@ impl Daemon {
|
||||
.ok_or(DaemonError::ClientNotConfigured)?;
|
||||
|
||||
let client = HTTPAPIClient::new(
|
||||
server_url.parse().unwrap(),
|
||||
match server_url.parse() {
|
||||
Ok(url) => url,
|
||||
Err(_) => {
|
||||
log::error!(target: target::DAEMON, "Invalid server URL: {}", server_url);
|
||||
return Err(DaemonError::ClientNotConfigured.into());
|
||||
}
|
||||
},
|
||||
DatabaseAuthenticationStore::new(database.clone()),
|
||||
);
|
||||
|
||||
|
||||
@@ -177,7 +177,10 @@ impl From<Participant> for DbParticipant {
|
||||
fn from(participant: Participant) -> Self {
|
||||
match participant {
|
||||
Participant::Me => DbParticipant::Me,
|
||||
Participant::Remote { handle, contact_id } => DbParticipant::Remote { handle, contact_id: contact_id.clone() },
|
||||
Participant::Remote { handle, contact_id } => DbParticipant::Remote {
|
||||
handle,
|
||||
contact_id: contact_id.clone(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,8 @@ impl UpdateMonitor {
|
||||
log::info!(target: target::UPDATES, "Conversation changed: {:?}", conversation);
|
||||
|
||||
// Explicitly update the unread count, we assume this is fresh from the notification.
|
||||
let db_conversation: kordophone_db::models::Conversation = conversation.clone().into();
|
||||
let db_conversation: kordophone_db::models::Conversation =
|
||||
conversation.clone().into();
|
||||
self.send_event(|r| Event::UpdateConversationMetadata(db_conversation, r))
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use dbus::arg;
|
||||
use dbus_tree::MethodErr;
|
||||
use std::{future::Future, thread};
|
||||
use std::sync::Arc;
|
||||
use std::{future::Future, thread};
|
||||
use tokio::sync::{mpsc, oneshot, Mutex};
|
||||
|
||||
use crate::daemon::{
|
||||
use kordophoned::daemon::{
|
||||
contact_resolver::{ContactResolver, DefaultContactResolverBackend},
|
||||
events::{Event, Reply},
|
||||
settings::Settings,
|
||||
signals::Signal,
|
||||
DaemonResult,
|
||||
contact_resolver::{ContactResolver, DefaultContactResolverBackend},
|
||||
};
|
||||
|
||||
use kordophone_db::models::participant::Participant;
|
||||
@@ -37,7 +37,8 @@ impl DBusAgent {
|
||||
|
||||
pub async fn run(self) {
|
||||
// Establish a session bus connection.
|
||||
let (resource, connection) = connection::new_session_sync().expect("Failed to connect to session bus");
|
||||
let (resource, connection) =
|
||||
connection::new_session_sync().expect("Failed to connect to session bus");
|
||||
|
||||
// Ensure the D-Bus resource is polled.
|
||||
tokio::spawn(async move {
|
||||
@@ -79,7 +80,10 @@ impl DBusAgent {
|
||||
Signal::ConversationsUpdated => {
|
||||
log::debug!("Sending signal: ConversationsUpdated");
|
||||
registry
|
||||
.send_signal(interface::OBJECT_PATH, DbusSignals::ConversationsUpdated {})
|
||||
.send_signal(
|
||||
interface::OBJECT_PATH,
|
||||
DbusSignals::ConversationsUpdated {},
|
||||
)
|
||||
.unwrap_or_else(|_| {
|
||||
log::error!("Failed to send signal");
|
||||
0
|
||||
@@ -118,7 +122,8 @@ impl DBusAgent {
|
||||
Signal::AttachmentUploaded(upload_guid, attachment_guid) => {
|
||||
log::debug!(
|
||||
"Sending signal: AttachmentUploaded for upload {}, attachment {}",
|
||||
upload_guid, attachment_guid
|
||||
upload_guid,
|
||||
attachment_guid
|
||||
);
|
||||
registry
|
||||
.send_signal(
|
||||
@@ -154,7 +159,10 @@ impl DBusAgent {
|
||||
std::future::pending::<()>().await;
|
||||
}
|
||||
|
||||
pub async fn send_event<T>(&self, make_event: impl FnOnce(Reply<T>) -> Event) -> DaemonResult<T> {
|
||||
pub async fn send_event<T>(
|
||||
&self,
|
||||
make_event: impl FnOnce(Reply<T>) -> Event,
|
||||
) -> DaemonResult<T> {
|
||||
let (reply_tx, reply_rx) = oneshot::channel();
|
||||
self.event_sink
|
||||
.send(make_event(reply_tx))
|
||||
@@ -179,14 +187,17 @@ impl DBusAgent {
|
||||
Participant::Me => "(Me)".to_string(),
|
||||
|
||||
// Remote participant with a resolved contact_id
|
||||
Participant::Remote { handle, contact_id: Some(contact_id), .. } => {
|
||||
self.contact_resolver.get_contact_display_name(contact_id).unwrap_or_else(|| handle.clone())
|
||||
}
|
||||
Participant::Remote {
|
||||
handle,
|
||||
contact_id: Some(contact_id),
|
||||
..
|
||||
} => self
|
||||
.contact_resolver
|
||||
.get_contact_display_name(contact_id)
|
||||
.unwrap_or_else(|| handle.clone()),
|
||||
|
||||
// Remote participant without a resolved contact_id
|
||||
Participant::Remote { handle, .. } => {
|
||||
handle.clone()
|
||||
}
|
||||
Participant::Remote { handle, .. } => handle.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -203,9 +214,12 @@ impl DbusRepository for DBusAgent {
|
||||
self.send_event_sync(Event::GetVersion)
|
||||
}
|
||||
|
||||
fn get_conversations(&mut self, limit: i32, offset: i32) -> Result<Vec<arg::PropMap>, MethodErr> {
|
||||
self
|
||||
.send_event_sync(|r| Event::GetAllConversations(limit, offset, r))
|
||||
fn get_conversations(
|
||||
&mut self,
|
||||
limit: i32,
|
||||
offset: i32,
|
||||
) -> Result<Vec<arg::PropMap>, MethodErr> {
|
||||
self.send_event_sync(|r| Event::GetAllConversations(limit, offset, r))
|
||||
.map(|conversations| {
|
||||
conversations
|
||||
.into_iter()
|
||||
@@ -243,7 +257,6 @@ impl DbusRepository for DBusAgent {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
fn sync_conversation_list(&mut self) -> Result<(), MethodErr> {
|
||||
self.send_event_sync(Event::SyncConversationList)
|
||||
}
|
||||
@@ -260,15 +273,18 @@ impl DbusRepository for DBusAgent {
|
||||
self.send_event_sync(|r| Event::MarkConversationAsRead(conversation_id, r))
|
||||
}
|
||||
|
||||
fn get_messages(&mut self, conversation_id: String, last_message_id: String) -> Result<Vec<arg::PropMap>, MethodErr> {
|
||||
fn get_messages(
|
||||
&mut self,
|
||||
conversation_id: String,
|
||||
last_message_id: String,
|
||||
) -> Result<Vec<arg::PropMap>, MethodErr> {
|
||||
let last_message_id_opt = if last_message_id.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(last_message_id)
|
||||
};
|
||||
|
||||
self
|
||||
.send_event_sync(|r| Event::GetMessages(conversation_id, last_message_id_opt, r))
|
||||
self.send_event_sync(|r| Event::GetMessages(conversation_id, last_message_id_opt, r))
|
||||
.map(|messages| {
|
||||
messages
|
||||
.into_iter()
|
||||
@@ -286,7 +302,9 @@ impl DbusRepository for DBusAgent {
|
||||
);
|
||||
map.insert(
|
||||
"sender".into(),
|
||||
arg::Variant(Box::new(self.resolve_participant_display_name(&msg.sender.into()))),
|
||||
arg::Variant(Box::new(
|
||||
self.resolve_participant_display_name(&msg.sender.into()),
|
||||
)),
|
||||
);
|
||||
|
||||
// Attachments array
|
||||
@@ -312,7 +330,9 @@ impl DbusRepository for DBusAgent {
|
||||
);
|
||||
attachment_map.insert(
|
||||
"preview_path".into(),
|
||||
arg::Variant(Box::new(preview_path.to_string_lossy().to_string())),
|
||||
arg::Variant(Box::new(
|
||||
preview_path.to_string_lossy().to_string(),
|
||||
)),
|
||||
);
|
||||
attachment_map.insert(
|
||||
"downloaded".into(),
|
||||
@@ -374,27 +394,34 @@ impl DbusRepository for DBusAgent {
|
||||
text: String,
|
||||
attachment_guids: Vec<String>,
|
||||
) -> Result<String, MethodErr> {
|
||||
self
|
||||
.send_event_sync(|r| Event::SendMessage(conversation_id, text, attachment_guids, r))
|
||||
self.send_event_sync(|r| Event::SendMessage(conversation_id, text, attachment_guids, r))
|
||||
.map(|uuid| uuid.to_string())
|
||||
}
|
||||
|
||||
fn get_attachment_info(&mut self, attachment_id: String) -> Result<(String, String, bool, bool), MethodErr> {
|
||||
self.send_event_sync(|r| Event::GetAttachment(attachment_id, r)).map(|attachment| {
|
||||
let path = attachment.get_path_for_preview(false);
|
||||
let downloaded = attachment.is_downloaded(false);
|
||||
let preview_path = attachment.get_path_for_preview(true);
|
||||
let preview_downloaded = attachment.is_downloaded(true);
|
||||
(
|
||||
path.to_string_lossy().to_string(),
|
||||
preview_path.to_string_lossy().to_string(),
|
||||
downloaded,
|
||||
preview_downloaded,
|
||||
)
|
||||
})
|
||||
fn get_attachment_info(
|
||||
&mut self,
|
||||
attachment_id: String,
|
||||
) -> Result<(String, String, bool, bool), MethodErr> {
|
||||
self.send_event_sync(|r| Event::GetAttachment(attachment_id, r))
|
||||
.map(|attachment| {
|
||||
let path = attachment.get_path_for_preview(false);
|
||||
let downloaded = attachment.is_downloaded(false);
|
||||
let preview_path = attachment.get_path_for_preview(true);
|
||||
let preview_downloaded = attachment.is_downloaded(true);
|
||||
(
|
||||
path.to_string_lossy().to_string(),
|
||||
preview_path.to_string_lossy().to_string(),
|
||||
downloaded,
|
||||
preview_downloaded,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn download_attachment(&mut self, attachment_id: String, preview: bool) -> Result<(), MethodErr> {
|
||||
fn download_attachment(
|
||||
&mut self,
|
||||
attachment_id: String,
|
||||
preview: bool,
|
||||
) -> Result<(), MethodErr> {
|
||||
self.send_event_sync(|r| Event::DownloadAttachment(attachment_id, preview, r))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
pub mod endpoint;
|
||||
pub mod agent;
|
||||
pub mod endpoint;
|
||||
|
||||
pub mod interface {
|
||||
#![allow(unused)]
|
||||
|
||||
1
kordophoned/src/lib.rs
Normal file
1
kordophoned/src/lib.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod daemon;
|
||||
@@ -1,12 +1,13 @@
|
||||
mod daemon;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod dbus;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod xpc;
|
||||
|
||||
use log::LevelFilter;
|
||||
use std::future;
|
||||
|
||||
use daemon::Daemon;
|
||||
use kordophoned::daemon::Daemon;
|
||||
|
||||
fn initialize_logging() {
|
||||
// Weird: is this the best way to do this?
|
||||
@@ -33,7 +34,17 @@ async fn start_ipc_agent(daemon: &mut Daemon) {
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
async fn start_ipc_agent(daemon: &mut Daemon) {
|
||||
// TODO: Implement macOS IPC agent.
|
||||
// Start the macOS XPC agent (events in, signals out) on a dedicated thread.
|
||||
let agent =
|
||||
xpc::agent::XpcAgent::new(daemon.event_sender.clone(), daemon.obtain_signal_receiver());
|
||||
std::thread::spawn(move || {
|
||||
// Use a single-threaded Tokio runtime for the XPC agent.
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("Unable to create tokio runtime for XPC agent");
|
||||
rt.block_on(agent.run());
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
||||
|
||||
186
kordophoned/src/xpc/agent.rs
Normal file
186
kordophoned/src/xpc/agent.rs
Normal file
@@ -0,0 +1,186 @@
|
||||
use crate::xpc::interface::SERVICE_NAME;
|
||||
use kordophoned::daemon::{events::Event, signals::Signal, DaemonResult};
|
||||
use std::ffi::CString;
|
||||
use std::os::raw::c_char;
|
||||
use std::ptr;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{mpsc, oneshot, Mutex};
|
||||
use xpc_connection::{message_to_xpc_object, xpc_object_to_message, Message, MessageError};
|
||||
use xpc_connection_sys as xpc_sys;
|
||||
|
||||
pub(super) static LOG_TARGET: &str = "xpc";
|
||||
|
||||
/// Wrapper for raw XPC connection pointer to declare cross-thread usage.
|
||||
/// Safety: libxpc connections are reference-counted and may be used to send from other threads.
|
||||
#[derive(Copy, Clone)]
|
||||
pub(crate) struct XpcConn(pub xpc_sys::xpc_connection_t);
|
||||
unsafe impl Send for XpcConn {}
|
||||
unsafe impl Sync for XpcConn {}
|
||||
|
||||
type Subscribers = Arc<std::sync::Mutex<Vec<XpcConn>>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct XpcAgent {
|
||||
event_sink: mpsc::Sender<Event>,
|
||||
signal_receiver: Arc<Mutex<Option<mpsc::Receiver<Signal>>>>,
|
||||
}
|
||||
|
||||
impl XpcAgent {
|
||||
pub fn new(event_sink: mpsc::Sender<Event>, signal_receiver: mpsc::Receiver<Signal>) -> Self {
|
||||
Self {
|
||||
event_sink,
|
||||
signal_receiver: Arc::new(Mutex::new(Some(signal_receiver))),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(self) {
|
||||
use block::ConcreteBlock;
|
||||
use std::ops::Deref;
|
||||
|
||||
// Construct the Mach service name without a trailing NUL for CString.
|
||||
let service_name = SERVICE_NAME.trim_end_matches('\0');
|
||||
let mach_port_name = match CString::new(service_name) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
log::error!(target: LOG_TARGET, "Invalid XPC service name: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
log::info!(
|
||||
target: LOG_TARGET,
|
||||
"Waiting for XPC connections on {}",
|
||||
service_name
|
||||
);
|
||||
|
||||
let rt = match tokio::runtime::Runtime::new() {
|
||||
Ok(rt) => Arc::new(rt),
|
||||
Err(e) => {
|
||||
log::error!(target: LOG_TARGET, "Failed to create Tokio runtime: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let connections: Subscribers = Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||
{
|
||||
let receiver_arc = self.signal_receiver.clone();
|
||||
let conns = connections.clone();
|
||||
rt.spawn(async move {
|
||||
let mut receiver = receiver_arc
|
||||
.lock()
|
||||
.await
|
||||
.take()
|
||||
.expect("Signal receiver already taken");
|
||||
while let Some(signal) = receiver.recv().await {
|
||||
log::trace!(target: LOG_TARGET, "Broadcasting signal: {:?}", signal);
|
||||
let msg = super::util::signal_to_message(signal);
|
||||
let xobj = message_to_xpc_object(msg);
|
||||
let list = conns.lock().unwrap();
|
||||
log::trace!(target: LOG_TARGET, "Active XPC clients: {}", list.len());
|
||||
for c in list.iter() {
|
||||
log::trace!(target: LOG_TARGET, "Sending signal to client");
|
||||
unsafe { xpc_sys::xpc_connection_send_message(c.0, xobj) };
|
||||
}
|
||||
unsafe { xpc_sys::xpc_release(xobj) };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let service = unsafe {
|
||||
xpc_sys::xpc_connection_create_mach_service(
|
||||
mach_port_name.as_ptr(),
|
||||
ptr::null_mut(),
|
||||
xpc_sys::XPC_CONNECTION_MACH_SERVICE_LISTENER as u64,
|
||||
)
|
||||
};
|
||||
|
||||
let agent = self.clone();
|
||||
let rt_accept = rt.clone();
|
||||
let conns_accept = connections.clone();
|
||||
let service_handler = ConcreteBlock::new(move |event: xpc_sys::xpc_object_t| {
|
||||
unsafe {
|
||||
let client = event as xpc_sys::xpc_connection_t;
|
||||
log::trace!(target: LOG_TARGET, "New XPC connection accepted");
|
||||
|
||||
let agent_conn = agent.clone();
|
||||
let rt_conn = rt_accept.clone();
|
||||
let conns_for_handler = conns_accept.clone();
|
||||
let conn_handler = ConcreteBlock::new(move |msg: xpc_sys::xpc_object_t| {
|
||||
match xpc_object_to_message(msg) {
|
||||
Message::Dictionary(map) => {
|
||||
let method = super::util::dict_get_str(&map, "method").or_else(|| super::util::dict_get_str(&map, "type")).unwrap_or_else(|| "<unknown>".to_string());
|
||||
log::trace!(target: LOG_TARGET, "XPC request received: {}", method);
|
||||
let response = rt_conn.block_on(super::rpc::dispatch(&agent_conn, &conns_for_handler, client, &map));
|
||||
let reply = xpc_sys::xpc_dictionary_create_reply(msg);
|
||||
if !reply.is_null() {
|
||||
let payload = message_to_xpc_object(response);
|
||||
let apply_block = ConcreteBlock::new(move |key: *const c_char, value: xpc_sys::xpc_object_t| {
|
||||
xpc_sys::xpc_dictionary_set_value(reply, key, value);
|
||||
})
|
||||
.copy();
|
||||
|
||||
xpc_sys::xpc_dictionary_apply(payload, apply_block.deref() as *const _ as *mut _);
|
||||
|
||||
xpc_sys::xpc_connection_send_message(client, reply);
|
||||
xpc_sys::xpc_release(payload);
|
||||
xpc_sys::xpc_release(reply);
|
||||
log::trace!(target: LOG_TARGET, "XPC reply sent for method: {}", method);
|
||||
} else {
|
||||
log::warn!(target: LOG_TARGET, "No reply port for method: {}", method);
|
||||
}
|
||||
}
|
||||
Message::Error(e) => {
|
||||
match e {
|
||||
MessageError::ConnectionInvalid => {
|
||||
let mut list = conns_for_handler.lock().unwrap();
|
||||
let before = list.len();
|
||||
list.retain(|c| c.0 != client);
|
||||
let after = list.len();
|
||||
if after < before {
|
||||
log::trace!(target: LOG_TARGET, "Removed closed XPC client from subscribers ({} -> {})", before, after);
|
||||
} else {
|
||||
log::debug!(target: LOG_TARGET, "XPC connection closed (no subscription)");
|
||||
}
|
||||
}
|
||||
other => {
|
||||
log::warn!(target: LOG_TARGET, "XPC error event: {:?}", other);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
.copy();
|
||||
|
||||
xpc_sys::xpc_connection_set_event_handler(
|
||||
client,
|
||||
conn_handler.deref() as *const _ as *mut _,
|
||||
);
|
||||
xpc_sys::xpc_connection_resume(client);
|
||||
}
|
||||
})
|
||||
.copy();
|
||||
|
||||
unsafe {
|
||||
xpc_sys::xpc_connection_set_event_handler(
|
||||
service,
|
||||
service_handler.deref() as *const _ as *mut _,
|
||||
);
|
||||
xpc_sys::xpc_connection_resume(service);
|
||||
}
|
||||
|
||||
futures_util::future::pending::<()>().await;
|
||||
}
|
||||
|
||||
pub async fn send_event<T>(
|
||||
&self,
|
||||
make_event: impl FnOnce(kordophoned::daemon::events::Reply<T>) -> Event,
|
||||
) -> DaemonResult<T> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.event_sink
|
||||
.send(make_event(tx))
|
||||
.await
|
||||
.map_err(|_| "Failed to send event")?;
|
||||
rx.await.map_err(|_| "Failed to receive reply".into())
|
||||
}
|
||||
}
|
||||
5
kordophoned/src/xpc/interface.rs
Normal file
5
kordophoned/src/xpc/interface.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
#![cfg(target_os = "macos")]
|
||||
//! XPC interface definitions for macOS IPC
|
||||
|
||||
/// Mach service name for the XPC interface (must include trailing NUL).
|
||||
pub const SERVICE_NAME: &str = "net.buzzert.kordophonecd\0";
|
||||
4
kordophoned/src/xpc/mod.rs
Normal file
4
kordophoned/src/xpc/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod agent;
|
||||
pub mod interface;
|
||||
pub mod rpc;
|
||||
pub mod util;
|
||||
452
kordophoned/src/xpc/rpc.rs
Normal file
452
kordophoned/src/xpc/rpc.rs
Normal file
@@ -0,0 +1,452 @@
|
||||
use super::agent::{XpcAgent, XpcConn, LOG_TARGET};
|
||||
use kordophoned::daemon::events::Event;
|
||||
use kordophoned::daemon::settings::Settings;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::CString;
|
||||
use xpc_connection::Message;
|
||||
use xpc_connection_sys as xpc_sys;
|
||||
|
||||
use super::util::*;
|
||||
|
||||
pub async fn dispatch(
|
||||
agent: &XpcAgent,
|
||||
subscribers: &std::sync::Mutex<Vec<XpcConn>>,
|
||||
current_client: xpc_sys::xpc_connection_t,
|
||||
root: &HashMap<CString, Message>,
|
||||
) -> Message {
|
||||
let request_id = dict_get_str(root, "request_id");
|
||||
|
||||
let method = match dict_get_str(root, "method").or_else(|| dict_get_str(root, "type")) {
|
||||
Some(m) => m,
|
||||
None => {
|
||||
return attach_request_id(
|
||||
make_error_reply("InvalidRequest", "Missing method/type"),
|
||||
request_id,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let _arguments = get_dictionary_field(root, "arguments");
|
||||
|
||||
let mut response = match method.as_str() {
|
||||
// GetVersion
|
||||
"GetVersion" => match agent.send_event(Event::GetVersion).await {
|
||||
Ok(version) => {
|
||||
let mut reply: XpcMap = HashMap::new();
|
||||
dict_put_str(&mut reply, "type", "GetVersionResponse");
|
||||
dict_put_str(&mut reply, "version", &version);
|
||||
Message::Dictionary(reply)
|
||||
}
|
||||
Err(e) => make_error_reply("DaemonError", &format!("{}", e)),
|
||||
},
|
||||
|
||||
// GetConversations
|
||||
"GetConversations" => {
|
||||
let mut limit: i32 = 100;
|
||||
let mut offset: i32 = 0;
|
||||
if let Some(args) = get_dictionary_field(root, "arguments") {
|
||||
if let Some(v) = dict_get_i64_from_str(args, "limit") {
|
||||
limit = v as i32;
|
||||
}
|
||||
if let Some(v) = dict_get_i64_from_str(args, "offset") {
|
||||
offset = v as i32;
|
||||
}
|
||||
}
|
||||
match agent
|
||||
.send_event(|r| Event::GetAllConversations(limit, offset, r))
|
||||
.await
|
||||
{
|
||||
Ok(conversations) => {
|
||||
let mut items: Vec<Message> = Vec::with_capacity(conversations.len());
|
||||
for conv in conversations {
|
||||
let mut m: XpcMap = HashMap::new();
|
||||
dict_put_str(&mut m, "guid", &conv.guid);
|
||||
dict_put_str(
|
||||
&mut m,
|
||||
"display_name",
|
||||
&conv.display_name.unwrap_or_default(),
|
||||
);
|
||||
dict_put_i64_as_str(&mut m, "unread_count", conv.unread_count as i64);
|
||||
dict_put_str(
|
||||
&mut m,
|
||||
"last_message_preview",
|
||||
&conv.last_message_preview.unwrap_or_default(),
|
||||
);
|
||||
let participant_names: Vec<String> = conv
|
||||
.participants
|
||||
.into_iter()
|
||||
.map(|p| p.display_name())
|
||||
.collect();
|
||||
m.insert(cstr("participants"), array_from_strs(participant_names));
|
||||
dict_put_i64_as_str(&mut m, "date", conv.date.and_utc().timestamp());
|
||||
items.push(Message::Dictionary(m));
|
||||
}
|
||||
let mut reply: XpcMap = HashMap::new();
|
||||
dict_put_str(&mut reply, "type", "GetConversationsResponse");
|
||||
reply.insert(cstr("conversations"), Message::Array(items));
|
||||
Message::Dictionary(reply)
|
||||
}
|
||||
Err(e) => make_error_reply("DaemonError", &format!("{}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
// Sync ops
|
||||
"SyncConversationList" => match agent.send_event(Event::SyncConversationList).await {
|
||||
Ok(()) => make_ok_reply(),
|
||||
Err(e) => make_error_reply("DaemonError", &format!("{}", e)),
|
||||
},
|
||||
"SyncAllConversations" => match agent.send_event(Event::SyncAllConversations).await {
|
||||
Ok(()) => make_ok_reply(),
|
||||
Err(e) => make_error_reply("DaemonError", &format!("{}", e)),
|
||||
},
|
||||
"SyncConversation" => {
|
||||
let conversation_id = match get_dictionary_field(root, "arguments")
|
||||
.and_then(|m| dict_get_str(m, "conversation_id"))
|
||||
{
|
||||
Some(id) => id,
|
||||
None => return make_error_reply("InvalidRequest", "Missing conversation_id"),
|
||||
};
|
||||
match agent
|
||||
.send_event(|r| Event::SyncConversation(conversation_id, r))
|
||||
.await
|
||||
{
|
||||
Ok(()) => make_ok_reply(),
|
||||
Err(e) => make_error_reply("DaemonError", &format!("{}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
// Mark as read
|
||||
"MarkConversationAsRead" => {
|
||||
let conversation_id = match get_dictionary_field(root, "arguments")
|
||||
.and_then(|m| dict_get_str(m, "conversation_id"))
|
||||
{
|
||||
Some(id) => id,
|
||||
None => return make_error_reply("InvalidRequest", "Missing conversation_id"),
|
||||
};
|
||||
match agent
|
||||
.send_event(|r| Event::MarkConversationAsRead(conversation_id, r))
|
||||
.await
|
||||
{
|
||||
Ok(()) => make_ok_reply(),
|
||||
Err(e) => make_error_reply("DaemonError", &format!("{}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
// GetMessages
|
||||
"GetMessages" => {
|
||||
let args = match get_dictionary_field(root, "arguments") {
|
||||
Some(a) => a,
|
||||
None => return make_error_reply("InvalidRequest", "Missing arguments"),
|
||||
};
|
||||
let conversation_id = match dict_get_str(args, "conversation_id") {
|
||||
Some(id) => id,
|
||||
None => return make_error_reply("InvalidRequest", "Missing conversation_id"),
|
||||
};
|
||||
let last_message_id = dict_get_str(args, "last_message_id");
|
||||
match agent
|
||||
.send_event(|r| Event::GetMessages(conversation_id, last_message_id, r))
|
||||
.await
|
||||
{
|
||||
Ok(messages) => {
|
||||
let mut items: Vec<Message> = Vec::with_capacity(messages.len());
|
||||
for msg in messages {
|
||||
let mut m: XpcMap = HashMap::new();
|
||||
dict_put_str(&mut m, "id", &msg.id);
|
||||
dict_put_str(&mut m, "text", &msg.text.replace('\u{FFFC}', ""));
|
||||
dict_put_i64_as_str(&mut m, "date", msg.date.and_utc().timestamp());
|
||||
dict_put_str(&mut m, "sender", &msg.sender.display_name());
|
||||
|
||||
// Include attachment GUIDs for the client to resolve/download
|
||||
let attachment_guids: Vec<String> = msg
|
||||
.attachments
|
||||
.iter()
|
||||
.map(|a| a.guid.clone())
|
||||
.collect();
|
||||
m.insert(cstr("attachment_guids"), array_from_strs(attachment_guids));
|
||||
|
||||
// Full attachments array with metadata (mirrors DBus fields)
|
||||
let mut attachments_items: Vec<Message> = Vec::new();
|
||||
for attachment in msg.attachments.iter() {
|
||||
let mut a: XpcMap = HashMap::new();
|
||||
// Basic identifiers
|
||||
dict_put_str(&mut a, "guid", &attachment.guid);
|
||||
|
||||
// Paths and download status
|
||||
let path = attachment.get_path_for_preview(false);
|
||||
let preview_path = attachment.get_path_for_preview(true);
|
||||
let downloaded = attachment.is_downloaded(false);
|
||||
let preview_downloaded = attachment.is_downloaded(true);
|
||||
|
||||
dict_put_str(&mut a, "path", &path.to_string_lossy());
|
||||
dict_put_str(&mut a, "preview_path", &preview_path.to_string_lossy());
|
||||
dict_put_str(&mut a, "downloaded", &downloaded.to_string());
|
||||
dict_put_str(
|
||||
&mut a,
|
||||
"preview_downloaded",
|
||||
&preview_downloaded.to_string(),
|
||||
);
|
||||
|
||||
// Metadata (optional)
|
||||
if let Some(metadata) = &attachment.metadata {
|
||||
let mut metadata_map: XpcMap = HashMap::new();
|
||||
if let Some(attribution_info) = &metadata.attribution_info {
|
||||
let mut attribution_map: XpcMap = HashMap::new();
|
||||
if let Some(width) = attribution_info.width {
|
||||
dict_put_i64_as_str(&mut attribution_map, "width", width as i64);
|
||||
}
|
||||
if let Some(height) = attribution_info.height {
|
||||
dict_put_i64_as_str(&mut attribution_map, "height", height as i64);
|
||||
}
|
||||
metadata_map.insert(cstr("attribution_info"), Message::Dictionary(attribution_map));
|
||||
}
|
||||
if !metadata_map.is_empty() {
|
||||
a.insert(cstr("metadata"), Message::Dictionary(metadata_map));
|
||||
}
|
||||
}
|
||||
|
||||
attachments_items.push(Message::Dictionary(a));
|
||||
}
|
||||
m.insert(cstr("attachments"), Message::Array(attachments_items));
|
||||
|
||||
items.push(Message::Dictionary(m));
|
||||
}
|
||||
let mut reply: XpcMap = HashMap::new();
|
||||
dict_put_str(&mut reply, "type", "GetMessagesResponse");
|
||||
reply.insert(cstr("messages"), Message::Array(items));
|
||||
Message::Dictionary(reply)
|
||||
}
|
||||
Err(e) => make_error_reply("DaemonError", &format!("{}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
// Delete all
|
||||
"DeleteAllConversations" => match agent.send_event(Event::DeleteAllConversations).await {
|
||||
Ok(()) => make_ok_reply(),
|
||||
Err(e) => make_error_reply("DaemonError", &format!("{}", e)),
|
||||
},
|
||||
|
||||
// SendMessage
|
||||
"SendMessage" => {
|
||||
let args = match get_dictionary_field(root, "arguments") {
|
||||
Some(a) => a,
|
||||
None => return make_error_reply("InvalidRequest", "Missing arguments"),
|
||||
};
|
||||
let conversation_id = match dict_get_str(args, "conversation_id") {
|
||||
Some(v) => v,
|
||||
None => return make_error_reply("InvalidRequest", "Missing conversation_id"),
|
||||
};
|
||||
let text = dict_get_str(args, "text").unwrap_or_default();
|
||||
let attachment_guids: Vec<String> = match args.get(&cstr("attachment_guids")) {
|
||||
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(),
|
||||
};
|
||||
match agent
|
||||
.send_event(|r| Event::SendMessage(conversation_id, text, attachment_guids, r))
|
||||
.await
|
||||
{
|
||||
Ok(uuid) => {
|
||||
let mut reply: XpcMap = HashMap::new();
|
||||
dict_put_str(&mut reply, "type", "SendMessageResponse");
|
||||
dict_put_str(&mut reply, "uuid", &uuid.to_string());
|
||||
Message::Dictionary(reply)
|
||||
}
|
||||
Err(e) => make_error_reply("DaemonError", &format!("{}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
// GetAttachmentInfo
|
||||
"GetAttachmentInfo" => {
|
||||
let args = match get_dictionary_field(root, "arguments") {
|
||||
Some(a) => a,
|
||||
None => return make_error_reply("InvalidRequest", "Missing arguments"),
|
||||
};
|
||||
let attachment_id = match dict_get_str(args, "attachment_id") {
|
||||
Some(v) => v,
|
||||
None => return make_error_reply("InvalidRequest", "Missing attachment_id"),
|
||||
};
|
||||
match agent
|
||||
.send_event(|r| Event::GetAttachment(attachment_id, r))
|
||||
.await
|
||||
{
|
||||
Ok(attachment) => {
|
||||
let mut reply: XpcMap = HashMap::new();
|
||||
dict_put_str(&mut reply, "type", "GetAttachmentInfoResponse");
|
||||
dict_put_str(
|
||||
&mut reply,
|
||||
"path",
|
||||
&attachment.get_path_for_preview(false).to_string_lossy(),
|
||||
);
|
||||
dict_put_str(
|
||||
&mut reply,
|
||||
"preview_path",
|
||||
&attachment.get_path_for_preview(true).to_string_lossy(),
|
||||
);
|
||||
dict_put_str(
|
||||
&mut reply,
|
||||
"downloaded",
|
||||
&attachment.is_downloaded(false).to_string(),
|
||||
);
|
||||
dict_put_str(
|
||||
&mut reply,
|
||||
"preview_downloaded",
|
||||
&attachment.is_downloaded(true).to_string(),
|
||||
);
|
||||
Message::Dictionary(reply)
|
||||
}
|
||||
Err(e) => make_error_reply("DaemonError", &format!("{}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
// OpenAttachmentFd (return file descriptor in reply)
|
||||
"OpenAttachmentFd" => {
|
||||
let args = match get_dictionary_field(root, "arguments") {
|
||||
Some(a) => a,
|
||||
None => return make_error_reply("InvalidRequest", "Missing arguments"),
|
||||
};
|
||||
let attachment_id = match dict_get_str(args, "attachment_id") {
|
||||
Some(v) => v,
|
||||
None => return make_error_reply("InvalidRequest", "Missing attachment_id"),
|
||||
};
|
||||
let preview = dict_get_str(args, "preview")
|
||||
.map(|s| s == "true")
|
||||
.unwrap_or(false);
|
||||
|
||||
match agent
|
||||
.send_event(|r| Event::GetAttachment(attachment_id, r))
|
||||
.await
|
||||
{
|
||||
Ok(attachment) => {
|
||||
use std::os::fd::AsRawFd;
|
||||
|
||||
let path = attachment.get_path_for_preview(preview);
|
||||
match std::fs::OpenOptions::new().read(true).open(&path) {
|
||||
Ok(file) => {
|
||||
let fd = file.as_raw_fd();
|
||||
|
||||
// Keep file alive until after conversion to XPC
|
||||
std::mem::forget(file);
|
||||
|
||||
// Return file descriptor in reply
|
||||
let mut reply: XpcMap = HashMap::new();
|
||||
dict_put_str(&mut reply, "type", "OpenAttachmentFdResponse");
|
||||
reply.insert(cstr("fd"), Message::Fd(fd));
|
||||
|
||||
Message::Dictionary(reply)
|
||||
}
|
||||
Err(e) => make_error_reply("OpenFailed", &format!("{}", e)),
|
||||
}
|
||||
}
|
||||
Err(e) => make_error_reply("DaemonError", &format!("{}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
// DownloadAttachment
|
||||
"DownloadAttachment" => {
|
||||
let args = match get_dictionary_field(root, "arguments") {
|
||||
Some(a) => a,
|
||||
None => return make_error_reply("InvalidRequest", "Missing arguments"),
|
||||
};
|
||||
let attachment_id = match dict_get_str(args, "attachment_id") {
|
||||
Some(v) => v,
|
||||
None => return make_error_reply("InvalidRequest", "Missing attachment_id"),
|
||||
};
|
||||
let preview = dict_get_str(args, "preview")
|
||||
.map(|s| s == "true")
|
||||
.unwrap_or(false);
|
||||
match agent
|
||||
.send_event(|r| Event::DownloadAttachment(attachment_id, preview, r))
|
||||
.await
|
||||
{
|
||||
Ok(()) => make_ok_reply(),
|
||||
Err(e) => make_error_reply("DaemonError", &format!("{}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
// UploadAttachment
|
||||
"UploadAttachment" => {
|
||||
use std::path::PathBuf;
|
||||
let args = match get_dictionary_field(root, "arguments") {
|
||||
Some(a) => a,
|
||||
None => return make_error_reply("InvalidRequest", "Missing arguments"),
|
||||
};
|
||||
let path = match dict_get_str(args, "path") {
|
||||
Some(v) => v,
|
||||
None => return make_error_reply("InvalidRequest", "Missing path"),
|
||||
};
|
||||
match agent
|
||||
.send_event(|r| Event::UploadAttachment(PathBuf::from(path), r))
|
||||
.await
|
||||
{
|
||||
Ok(upload_guid) => {
|
||||
let mut reply: XpcMap = HashMap::new();
|
||||
dict_put_str(&mut reply, "type", "UploadAttachmentResponse");
|
||||
dict_put_str(&mut reply, "upload_guid", &upload_guid);
|
||||
Message::Dictionary(reply)
|
||||
}
|
||||
Err(e) => make_error_reply("DaemonError", &format!("{}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
// Settings
|
||||
"GetAllSettings" => match agent.send_event(Event::GetAllSettings).await {
|
||||
Ok(settings) => {
|
||||
let mut reply: XpcMap = HashMap::new();
|
||||
dict_put_str(&mut reply, "type", "GetAllSettingsResponse");
|
||||
dict_put_str(
|
||||
&mut reply,
|
||||
"server_url",
|
||||
&settings.server_url.unwrap_or_default(),
|
||||
);
|
||||
dict_put_str(
|
||||
&mut reply,
|
||||
"username",
|
||||
&settings.username.unwrap_or_default(),
|
||||
);
|
||||
Message::Dictionary(reply)
|
||||
}
|
||||
Err(e) => make_error_reply("DaemonError", &format!("{}", e)),
|
||||
},
|
||||
"UpdateSettings" => {
|
||||
let args = match get_dictionary_field(root, "arguments") {
|
||||
Some(a) => a,
|
||||
None => return make_error_reply("InvalidRequest", "Missing arguments"),
|
||||
};
|
||||
let server_url = dict_get_str(args, "server_url");
|
||||
let username = dict_get_str(args, "username");
|
||||
let settings = Settings {
|
||||
server_url,
|
||||
username,
|
||||
token: None,
|
||||
};
|
||||
match agent
|
||||
.send_event(|r| Event::UpdateSettings(settings, r))
|
||||
.await
|
||||
{
|
||||
Ok(()) => make_ok_reply(),
|
||||
Err(e) => make_error_reply("DaemonError", &format!("{}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe
|
||||
"SubscribeSignals" => {
|
||||
let mut list = subscribers.lock().unwrap();
|
||||
if !list.iter().any(|c| c.0 == current_client) {
|
||||
list.push(XpcConn(current_client));
|
||||
log::trace!(target: LOG_TARGET, "Client subscribed to signals (total subscribers: {})", list.len());
|
||||
}
|
||||
make_ok_reply()
|
||||
}
|
||||
|
||||
// Unknown method fallback
|
||||
other => make_error_reply("UnknownMethod", other),
|
||||
};
|
||||
|
||||
response = attach_request_id(response, request_id);
|
||||
response
|
||||
}
|
||||
100
kordophoned/src/xpc/util.rs
Normal file
100
kordophoned/src/xpc/util.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
use kordophoned::daemon::signals::Signal;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::CString;
|
||||
use xpc_connection::Message;
|
||||
|
||||
pub type XpcMap = HashMap<CString, Message>;
|
||||
|
||||
pub fn cstr(s: &str) -> CString {
|
||||
CString::new(s).unwrap_or_else(|_| CString::new("").unwrap())
|
||||
}
|
||||
|
||||
pub fn get_dictionary_field<'a>(
|
||||
map: &'a HashMap<CString, Message>,
|
||||
key: &str,
|
||||
) -> Option<&'a HashMap<CString, Message>> {
|
||||
let k = CString::new(key).ok()?;
|
||||
map.get(&k).and_then(|v| match v {
|
||||
Message::Dictionary(d) => Some(d),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn dict_get_str(map: &HashMap<CString, Message>, key: &str) -> Option<String> {
|
||||
let k = CString::new(key).ok()?;
|
||||
match map.get(&k) {
|
||||
Some(Message::String(v)) => Some(v.to_string_lossy().into_owned()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dict_get_i64_from_str(map: &HashMap<CString, Message>, key: &str) -> Option<i64> {
|
||||
dict_get_str(map, key).and_then(|s| s.parse::<i64>().ok())
|
||||
}
|
||||
|
||||
pub fn dict_put_str(map: &mut XpcMap, key: &str, value: impl AsRef<str>) {
|
||||
map.insert(cstr(key), Message::String(cstr(value.as_ref())));
|
||||
}
|
||||
|
||||
pub fn dict_put_i64_as_str(map: &mut XpcMap, key: &str, value: i64) {
|
||||
dict_put_str(map, key, value.to_string());
|
||||
}
|
||||
|
||||
pub fn array_from_strs(values: impl IntoIterator<Item = String>) -> Message {
|
||||
let arr = values
|
||||
.into_iter()
|
||||
.map(|s| Message::String(cstr(&s)))
|
||||
.collect();
|
||||
Message::Array(arr)
|
||||
}
|
||||
|
||||
pub fn make_ok_reply() -> Message {
|
||||
let mut reply: XpcMap = HashMap::new();
|
||||
dict_put_str(&mut reply, "type", "Ok");
|
||||
Message::Dictionary(reply)
|
||||
}
|
||||
|
||||
pub fn make_error_reply(code: &str, message: &str) -> Message {
|
||||
let mut reply: HashMap<CString, Message> = HashMap::new();
|
||||
reply.insert(cstr("type"), Message::String(cstr("Error")));
|
||||
reply.insert(cstr("error"), Message::String(cstr(code)));
|
||||
reply.insert(cstr("message"), Message::String(cstr(message)));
|
||||
Message::Dictionary(reply)
|
||||
}
|
||||
|
||||
pub fn attach_request_id(mut message: Message, request_id: Option<String>) -> Message {
|
||||
if let (Some(id), Message::Dictionary(ref mut m)) = (request_id, &mut message) {
|
||||
dict_put_str(m, "request_id", &id);
|
||||
}
|
||||
message
|
||||
}
|
||||
|
||||
pub fn signal_to_message(signal: Signal) -> Message {
|
||||
let mut root: XpcMap = HashMap::new();
|
||||
let mut args: XpcMap = HashMap::new();
|
||||
match signal {
|
||||
Signal::ConversationsUpdated => {
|
||||
dict_put_str(&mut root, "name", "ConversationsUpdated");
|
||||
}
|
||||
Signal::MessagesUpdated(conversation_id) => {
|
||||
dict_put_str(&mut root, "name", "MessagesUpdated");
|
||||
dict_put_str(&mut args, "conversation_id", &conversation_id);
|
||||
}
|
||||
Signal::AttachmentDownloaded(attachment_id) => {
|
||||
dict_put_str(&mut root, "name", "AttachmentDownloadCompleted");
|
||||
dict_put_str(&mut args, "attachment_id", &attachment_id);
|
||||
}
|
||||
Signal::AttachmentUploaded(upload_guid, attachment_guid) => {
|
||||
dict_put_str(&mut root, "name", "AttachmentUploadCompleted");
|
||||
dict_put_str(&mut args, "upload_guid", &upload_guid);
|
||||
dict_put_str(&mut args, "attachment_guid", &attachment_guid);
|
||||
}
|
||||
Signal::UpdateStreamReconnected => {
|
||||
dict_put_str(&mut root, "name", "UpdateStreamReconnected");
|
||||
}
|
||||
}
|
||||
if !args.is_empty() {
|
||||
root.insert(cstr("arguments"), Message::Dictionary(args));
|
||||
}
|
||||
Message::Dictionary(root)
|
||||
}
|
||||
@@ -29,3 +29,10 @@ dbus-tree = "0.9.2"
|
||||
# D-Bus codegen only on Linux
|
||||
[target.'cfg(target_os = "linux")'.build-dependencies]
|
||||
dbus-codegen = "0.10.0"
|
||||
|
||||
# XPC (libxpc) interface only on macOS
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
block = "0.1.6"
|
||||
futures = "0.3.4"
|
||||
xpc-connection = { git = "https://github.com/dfrankland/xpc-connection-rs.git", rev = "cd4fb3d", package = "xpc-connection" }
|
||||
xpc-connection-sys = { git = "https://github.com/dfrankland/xpc-connection-rs.git", rev = "cd4fb3d", package = "xpc-connection-sys" }
|
||||
|
||||
@@ -16,11 +16,10 @@ fn main() {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let xml = std::fs::read_to_string(KORDOPHONE_XML)
|
||||
.expect("Error reading server dbus interface");
|
||||
let xml = std::fs::read_to_string(KORDOPHONE_XML).expect("Error reading server dbus interface");
|
||||
|
||||
let output = dbus_codegen::generate(&xml, &opts)
|
||||
.expect("Error generating client dbus interface");
|
||||
let output =
|
||||
dbus_codegen::generate(&xml, &opts).expect("Error generating client dbus interface");
|
||||
|
||||
std::fs::write(out_path, output).expect("Error writing client dbus code");
|
||||
|
||||
|
||||
@@ -6,7 +6,11 @@ use clap::Subcommand;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod dbus;
|
||||
|
||||
#[async_trait]
|
||||
#[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<()>;
|
||||
@@ -37,43 +41,76 @@ impl StubDaemonInterface {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[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"))
|
||||
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"))
|
||||
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"))
|
||||
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"))
|
||||
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 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 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"))
|
||||
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"))
|
||||
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"))
|
||||
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"))
|
||||
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"))
|
||||
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"))
|
||||
Err(anyhow::anyhow!(
|
||||
"Daemon interface not implemented on this platform"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +119,11 @@ pub fn new_daemon_interface() -> Result<Box<dyn DaemonInterface>> {
|
||||
{
|
||||
Ok(Box::new(dbus::DBusDaemonInterface::new()?))
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
Ok(Box::new(xpc::XpcDaemonInterface::new()?))
|
||||
}
|
||||
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
||||
{
|
||||
Ok(Box::new(StubDaemonInterface::new()?))
|
||||
}
|
||||
@@ -162,7 +203,9 @@ impl Commands {
|
||||
conversation_id,
|
||||
last_message_id,
|
||||
} => {
|
||||
client.print_messages(conversation_id, last_message_id).await
|
||||
client
|
||||
.print_messages(conversation_id, last_message_id)
|
||||
.await
|
||||
}
|
||||
Commands::DeleteAllConversations => client.delete_all_conversations().await,
|
||||
Commands::SendMessage {
|
||||
|
||||
604
kpcli/src/daemon/xpc.rs
Normal file
604
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