From 717138b371e36be838a04c040b4c0d96bb0e49d6 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Sat, 1 Nov 2025 21:39:53 -0700 Subject: [PATCH] first attempt: notification code is in dbus::agent --- core/Cargo.lock | 840 +++++++++++++++++- core/kordophone-db/src/repository.rs | 2 +- core/kordophone/src/api/http_client.rs | 23 +- core/kordophoned/Cargo.toml | 1 + .../net.buzzert.kordophonecd.Server.xml | 7 + core/kordophoned/src/daemon/events.rs | 6 + core/kordophoned/src/daemon/mod.rs | 35 +- .../src/daemon/models/attachment.rs | 17 +- core/kordophoned/src/dbus/agent.rs | 145 ++- core/kordophoned/src/xpc/mod.rs | 10 +- core/kordophoned/src/xpc/rpc.rs | 141 ++- core/kpcli/src/client/mod.rs | 16 +- core/kpcli/src/daemon/dbus.rs | 5 + core/kpcli/src/daemon/mod.rs | 12 + core/utilities/src/bin/snoozer.rs | 82 +- 15 files changed, 1222 insertions(+), 120 deletions(-) diff --git a/core/Cargo.lock b/core/Cargo.lock index 645ea55..2d29c46 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -111,6 +111,126 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.1.2", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 1.1.2", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-signal" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.2", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.88" @@ -122,6 +242,12 @@ dependencies = [ "syn", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "atty" version = "0.2.14" @@ -219,6 +345,28 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -252,6 +400,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.38" @@ -339,6 +493,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -374,6 +537,12 @@ dependencies = [ "libc", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crypto-common" version = "0.1.6" @@ -615,7 +784,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.0", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -629,6 +798,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.9.3", + "objc2", +] + [[package]] name = "dotenv" version = "0.15.0" @@ -661,6 +840,33 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "env_filter" version = "0.1.2" @@ -705,12 +911,33 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.60.2", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", ] [[package]] @@ -788,6 +1015,19 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.31" @@ -926,6 +1166,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "http" version = "0.2.12" @@ -1025,7 +1271,7 @@ dependencies = [ "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows-core", + "windows-core 0.52.0", ] [[package]] @@ -1202,6 +1448,7 @@ dependencies = [ "kordophone-db", "log", "mime_guess", + "notify-rust", "once_cell", "serde", "serde_json", @@ -1274,7 +1521,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.53.5", ] [[package]] @@ -1303,6 +1550,12 @@ version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + [[package]] name = "lock_api" version = "0.4.11" @@ -1319,12 +1572,33 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "mac-notification-sys" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee70bb2bba058d58e252d2944582d634fc884fc9c489a966d428dedcf653e97" +dependencies = [ + "cc", + "objc2", + "objc2-foundation", + "time", +] + [[package]] name = "memchr" version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "migrations_internals" version = "2.2.0" @@ -1401,6 +1675,19 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.9.3", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + [[package]] name = "nom" version = "5.1.3" @@ -1411,6 +1698,20 @@ dependencies = [ "version_check", ] +[[package]] +name = "notify-rust" +version = "4.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6442248665a5aa2514e794af3b39661a8e73033b1cc5e59899e1276117ee4400" +dependencies = [ + "futures-lite", + "log", + "mac-notification-sys", + "serde", + "tauri-winrt-notification", + "zbus", +] + [[package]] name = "num" version = "0.4.3" @@ -1490,6 +1791,45 @@ dependencies = [ "autocfg", ] +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.9.3", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.9.3", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + [[package]] name = "object" version = "0.32.2" @@ -1555,6 +1895,22 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.1" @@ -1596,12 +1952,37 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkg-config" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.5.0", + "pin-project-lite", + "rustix 1.1.2", + "windows-sys 0.61.2", +] + [[package]] name = "portable-atomic" version = "1.11.0" @@ -1655,6 +2036,15 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.4", +] + [[package]] name = "proc-macro2" version = "1.0.95" @@ -1664,6 +2054,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.40" @@ -1834,10 +2233,23 @@ dependencies = [ "bitflags 2.9.3", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.13", "windows-sys 0.52.0", ] +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags 2.9.3", + "errno", + "libc", + "linux-raw-sys 0.11.0", + "windows-sys 0.60.2", +] + [[package]] name = "rustls" version = "0.23.29" @@ -1975,6 +2387,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_spanned" version = "0.6.8" @@ -2041,6 +2464,12 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.8.0" @@ -2070,6 +2499,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tauri-winrt-notification" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9" +dependencies = [ + "quick-xml", + "thiserror 2.0.12", + "windows", + "windows-version", +] + [[package]] name = "tempfile" version = "3.10.1" @@ -2078,7 +2519,7 @@ checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", - "rustix", + "rustix 0.38.34", "windows-sys 0.52.0", ] @@ -2278,8 +2719,8 @@ checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ "serde", "serde_spanned", - "toml_datetime", - "toml_edit", + "toml_datetime 0.6.8", + "toml_edit 0.22.22", ] [[package]] @@ -2291,6 +2732,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" +dependencies = [ + "serde", +] + [[package]] name = "toml_edit" version = "0.22.22" @@ -2300,8 +2750,29 @@ dependencies = [ "indexmap", "serde", "serde_spanned", - "toml_datetime", - "winnow", + "toml_datetime 0.6.8", + "winnow 0.6.20", +] + +[[package]] +name = "toml_edit" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7211ff1b8f0d3adae1663b7da9ffe396eabe1ca25f0b0bee42b0da29a9ddce93" +dependencies = [ + "indexmap", + "toml_datetime 0.7.0", + "toml_parser", + "winnow 0.7.13", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow 0.7.13", ] [[package]] @@ -2317,9 +2788,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tracing-core" version = "0.1.32" @@ -2367,6 +2850,17 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + [[package]] name = "unicase" version = "2.8.1" @@ -2417,6 +2911,7 @@ checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" dependencies = [ "getrandom 0.3.2", "rand 0.9.1", + "serde", "uuid-macro-internal", ] @@ -2586,6 +3081,28 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + [[package]] name = "windows-core" version = "0.52.0" @@ -2595,6 +3112,92 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -2613,6 +3216,24 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -2637,13 +3258,48 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -2656,6 +3312,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -2668,6 +3330,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -2680,12 +3348,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -2698,6 +3378,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -2710,6 +3396,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -2722,6 +3414,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -2734,6 +3432,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.6.20" @@ -2743,6 +3447,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen-rt" version = "0.39.0" @@ -2776,8 +3489,109 @@ dependencies = [ "bindgen", ] +[[package]] +name = "zbus" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "nix", + "ordered-stream", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 0.7.13", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cdb94821ca8a87ca9c298b5d1cbd80e2a8b67115d99f6e4551ac49e42b6a314" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +dependencies = [ + "serde", + "static_assertions", + "winnow 0.7.13", + "zvariant", +] + [[package]] name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zvariant" +version = "5.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2be61892e4f2b1772727be11630a62664a1826b62efa43a6fe7449521cb8744c" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 0.7.13", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da58575a1b2b20766513b1ec59d8e2e68db2745379f961f86650655e862d2006" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn", + "winnow 0.7.13", +] diff --git a/core/kordophone-db/src/repository.rs b/core/kordophone-db/src/repository.rs index 79b6e40..caf4124 100644 --- a/core/kordophone-db/src/repository.rs +++ b/core/kordophone-db/src/repository.rs @@ -307,8 +307,8 @@ impl<'a> Repository<'a> { } pub fn delete_all_messages(&mut self) -> Result<()> { - use crate::schema::messages::dsl as messages_dsl; use crate::schema::message_aliases::dsl as aliases_dsl; + use crate::schema::messages::dsl as messages_dsl; diesel::delete(messages_dsl::messages).execute(self.connection)?; diesel::delete(aliases_dsl::message_aliases).execute(self.connection)?; diff --git a/core/kordophone/src/api/http_client.rs b/core/kordophone/src/api/http_client.rs index 9c25e5a..1015532 100644 --- a/core/kordophone/src/api/http_client.rs +++ b/core/kordophone/src/api/http_client.rs @@ -394,8 +394,7 @@ impl APIInterface for HTTPAPIClient { None => "updates".to_string(), }; - let uri = self - .uri_for_endpoint(&endpoint, Some(self.websocket_scheme()))?; + let uri = self.uri_for_endpoint(&endpoint, Some(self.websocket_scheme()))?; loop { log::debug!("Connecting to websocket: {:?}", uri); @@ -426,18 +425,20 @@ impl APIInterface for HTTPAPIClient { log::debug!("Websocket request: {:?}", request); - let mut should_retry = true; // retry once after authenticating. + let mut should_retry = true; // retry once after authenticating. match connect_async(request).await.map_err(Error::from) { Ok((socket, response)) => { log::debug!("Websocket connected: {:?}", response.status()); - break Ok(WebsocketEventSocket::new(socket)) + break Ok(WebsocketEventSocket::new(socket)); } Err(e) => match &e { Error::ClientError(ce) => match ce.as_str() { "HTTP error: 401 Unauthorized" | "Unauthorized" => { // Try to authenticate if let Some(credentials) = &self.auth_store.get_credentials().await { - log::warn!("Websocket connection failed, attempting to authenticate"); + log::warn!( + "Websocket connection failed, attempting to authenticate" + ); let new_token = self.authenticate(credentials.clone()).await?; self.auth_store.set_token(new_token.to_string()).await; @@ -469,16 +470,16 @@ impl HTTPAPIClient { let https = HttpsConnector::new(); let client = Client::builder().build::<_, Body>(https); - HTTPAPIClient { base_url, auth_store, client } + HTTPAPIClient { + base_url, + auth_store, + client, + } } fn uri_for_endpoint(&self, endpoint: &str, scheme: Option<&str>) -> Result { let mut parts = self.base_url.clone().into_parts(); - let root_path: PathBuf = parts - .path_and_query - .ok_or(Error::URLError)? - .path() - .into(); + let root_path: PathBuf = parts.path_and_query.ok_or(Error::URLError)?.path().into(); let path = root_path.join(endpoint); let path_str = path.to_str().ok_or(Error::URLError)?; diff --git a/core/kordophoned/Cargo.toml b/core/kordophoned/Cargo.toml index 9a15f04..3ca9c83 100644 --- a/core/kordophoned/Cargo.toml +++ b/core/kordophoned/Cargo.toml @@ -23,6 +23,7 @@ tokio-condvar = "0.3.0" uuid = "1.16.0" once_cell = "1.19.0" mime_guess = "2.0" +notify = { package = "notify-rust", version = "4.10.0" } # D-Bus dependencies only on Linux [target.'cfg(target_os = "linux")'.dependencies] diff --git a/core/kordophoned/include/net.buzzert.kordophonecd.Server.xml b/core/kordophoned/include/net.buzzert.kordophonecd.Server.xml index cdef983..6de3abc 100644 --- a/core/kordophoned/include/net.buzzert.kordophonecd.Server.xml +++ b/core/kordophoned/include/net.buzzert.kordophonecd.Server.xml @@ -103,6 +103,13 @@ "/> + + + + + + >), + /// Returns a conversation by its ID. + GetConversation(String, Reply>), + + /// Returns the most recent message for a conversation. + GetLastMessage(String, Reply>), + /// Returns all known settings from the database. GetAllSettings(Reply), diff --git a/core/kordophoned/src/daemon/mod.rs b/core/kordophoned/src/daemon/mod.rs index bb17208..346f715 100644 --- a/core/kordophoned/src/daemon/mod.rs +++ b/core/kordophoned/src/daemon/mod.rs @@ -271,6 +271,16 @@ impl Daemon { reply.send(conversations).unwrap(); } + Event::GetConversation(conversation_id, reply) => { + let conversation = self.get_conversation(conversation_id).await; + reply.send(conversation).unwrap(); + } + + Event::GetLastMessage(conversation_id, reply) => { + let message = self.get_last_message(conversation_id).await; + reply.send(message).unwrap(); + } + Event::GetAllSettings(reply) => { let settings = self.get_settings().await.unwrap_or_else(|e| { log::error!(target: target::SETTINGS, "Failed to get settings: {:#?}", e); @@ -433,6 +443,14 @@ impl Daemon { self.signal_receiver.take().unwrap() } + async fn get_conversation(&mut self, conversation_id: String) -> Option { + self.database + .lock() + .await + .with_repository(|r| r.get_conversation_by_guid(&conversation_id).unwrap()) + .await + } + async fn get_conversations_limit_offset( &mut self, limit: i32, @@ -445,6 +463,18 @@ impl Daemon { .await } + async fn get_last_message(&mut self, conversation_id: String) -> Option { + self.database + .lock() + .await + .with_repository(|r| { + r.get_last_message_for_conversation(&conversation_id) + .unwrap() + .map(|message| message.into()) + }) + .await + } + async fn get_messages( &mut self, conversation_id: String, @@ -471,9 +501,8 @@ impl Daemon { .await; // Convert DB messages to daemon model, substituting local_id when an alias exists. - let mut result: Vec = Vec::with_capacity( - db_messages.len() + outgoing_messages.len(), - ); + let mut result: Vec = + Vec::with_capacity(db_messages.len() + outgoing_messages.len()); for m in db_messages.into_iter() { let server_id = m.id.clone(); let mut dm: Message = m.into(); diff --git a/core/kordophoned/src/daemon/models/attachment.rs b/core/kordophoned/src/daemon/models/attachment.rs index 6adb261..452161c 100644 --- a/core/kordophoned/src/daemon/models/attachment.rs +++ b/core/kordophoned/src/daemon/models/attachment.rs @@ -25,15 +25,14 @@ impl Attachment { // Prefer common, user-friendly extensions over obscure ones match normalized { "image/jpeg" | "image/pjpeg" => Some("jpg"), - _ => mime_guess::get_mime_extensions_str(normalized) - .and_then(|list| { - // If jpg is one of the candidates, prefer it - if list.iter().any(|e| *e == "jpg") { - Some("jpg") - } else { - list.first().copied() - } - }), + _ => mime_guess::get_mime_extensions_str(normalized).and_then(|list| { + // If jpg is one of the candidates, prefer it + if list.iter().any(|e| *e == "jpg") { + Some("jpg") + } else { + list.first().copied() + } + }), } } pub fn get_path(&self) -> PathBuf { diff --git a/core/kordophoned/src/dbus/agent.rs b/core/kordophoned/src/dbus/agent.rs index d284132..62022c6 100644 --- a/core/kordophoned/src/dbus/agent.rs +++ b/core/kordophoned/src/dbus/agent.rs @@ -1,6 +1,7 @@ use dbus::arg; use dbus_tree::MethodErr; -use std::sync::Arc; +use notify::Notification; +use std::sync::{Arc, Mutex as StdMutex}; use std::{future::Future, thread}; use tokio::sync::{mpsc, oneshot, Mutex}; @@ -9,10 +10,11 @@ use kordophoned::daemon::{ events::{Event, Reply}, settings::Settings, signals::Signal, - DaemonResult, + DaemonResult, Message, }; use kordophone_db::models::participant::Participant; +use kordophone_db::models::Conversation; use crate::dbus::endpoint::DbusRegistry; use crate::dbus::interface; @@ -23,7 +25,7 @@ use dbus_tokio::connection; pub struct DBusAgent { event_sink: mpsc::Sender, signal_receiver: Arc>>>, - contact_resolver: ContactResolver, + contact_resolver: Arc>>, } impl DBusAgent { @@ -31,7 +33,9 @@ impl DBusAgent { Self { event_sink, signal_receiver: Arc::new(Mutex::new(Some(signal_receiver))), - contact_resolver: ContactResolver::new(DefaultContactResolverBackend::default()), + contact_resolver: Arc::new(StdMutex::new(ContactResolver::new( + DefaultContactResolverBackend::default(), + ))), } } @@ -68,6 +72,7 @@ impl DBusAgent { { let registry = dbus_registry.clone(); let receiver_arc = self.signal_receiver.clone(); + let agent_clone = self.clone(); tokio::spawn(async move { let mut receiver = receiver_arc .lock() @@ -94,6 +99,7 @@ impl DBusAgent { "Sending signal: MessagesUpdated for conversation {}", conversation_id ); + let conversation_id_for_notification = conversation_id.clone(); registry .send_signal( interface::OBJECT_PATH, @@ -103,6 +109,10 @@ impl DBusAgent { log::error!("Failed to send signal"); 0 }); + + agent_clone + .maybe_notify_on_messages_updated(&conversation_id_for_notification) + .await; } Signal::AttachmentDownloaded(attachment_id) => { log::debug!( @@ -181,7 +191,7 @@ impl DBusAgent { .map_err(|e| MethodErr::failed(&format!("Daemon error: {}", e))) } - fn resolve_participant_display_name(&mut self, participant: &Participant) -> String { + fn resolve_participant_display_name(&self, participant: &Participant) -> String { match participant { // Me (we should use a special string here...) Participant::Me => "(Me)".to_string(), @@ -191,10 +201,15 @@ impl DBusAgent { handle, contact_id: Some(contact_id), .. - } => self - .contact_resolver - .get_contact_display_name(contact_id) - .unwrap_or_else(|| handle.clone()), + } => { + if let Ok(mut resolver) = self.contact_resolver.lock() { + resolver + .get_contact_display_name(contact_id) + .unwrap_or_else(|| handle.clone()) + } else { + handle.clone() + } + } // Remote participant without a resolved contact_id Participant::Remote { handle, .. } => handle.clone(), @@ -202,6 +217,113 @@ impl DBusAgent { } } +impl DBusAgent { + fn conversation_display_name(&self, conversation: &Conversation) -> String { + if let Some(display_name) = &conversation.display_name { + if !display_name.trim().is_empty() { + return display_name.clone(); + } + } + + let names: Vec = conversation + .participants + .iter() + .filter(|participant| !matches!(participant, Participant::Me)) + .map(|participant| self.resolve_participant_display_name(participant)) + .collect(); + + if names.is_empty() { + "Kordophone".to_string() + } else { + names.join(", ") + } + } + + async fn prepare_incoming_message_notification( + &self, + conversation_id: &str, + ) -> DaemonResult> { + let conversation = match self + .send_event(|reply| Event::GetConversation(conversation_id.to_string(), reply)) + .await? + { + Some(conv) => conv, + None => return Ok(None), + }; + + if conversation.unread_count == 0 { + return Ok(None); + } + + let last_message: Option = self + .send_event(|reply| Event::GetLastMessage(conversation_id.to_string(), reply)) + .await?; + + let last_message = match last_message { + Some(message) => message, + None => return Ok(None), + }; + + let sender_participant: Participant = Participant::from(last_message.sender.clone()); + if matches!(sender_participant, Participant::Me) { + return Ok(None); + } + + let summary = self.conversation_display_name(&conversation); + let sender_display_name = self.resolve_participant_display_name(&sender_participant); + + let mut message_text = last_message.text.replace('\u{FFFC}', ""); + if message_text.trim().is_empty() { + if !last_message.attachments.is_empty() { + message_text = "Sent an attachment".to_string(); + } else { + message_text = "Sent a message".to_string(); + } + } + + let body = if sender_display_name.is_empty() { + message_text + } else { + format!("{}: {}", sender_display_name, message_text) + }; + + Ok(Some((summary, body))) + } + fn show_notification(&self, summary: &str, body: &str) -> Result<(), notify::error::Error> { + Notification::new() + .appname("Kordophone") + .summary(summary) + .body(body) + .show() + .map(|_| ()) + } + + async fn maybe_notify_on_messages_updated(&self, conversation_id: &str) { + match self + .prepare_incoming_message_notification(conversation_id) + .await + { + Ok(Some((summary, body))) => { + if let Err(error) = self.show_notification(&summary, &body) { + log::warn!( + "Failed to display notification for conversation {}: {}", + conversation_id, + error + ); + } + } + Ok(None) => {} + Err(error) => { + log::warn!( + "Unable to prepare notification for conversation {}: {}", + conversation_id, + error + ); + } + } + } +} + // // D-Bus repository interface implementation // @@ -398,6 +520,11 @@ impl DbusRepository for DBusAgent { .map(|uuid| uuid.to_string()) } + fn test_notification(&mut self, summary: String, body: String) -> Result<(), MethodErr> { + self.show_notification(&summary, &body) + .map_err(|e| MethodErr::failed(&format!("Failed to display notification: {}", e))) + } + fn get_attachment_info( &mut self, attachment_id: String, diff --git a/core/kordophoned/src/xpc/mod.rs b/core/kordophoned/src/xpc/mod.rs index d2bf926..cd9cef5 100644 --- a/core/kordophoned/src/xpc/mod.rs +++ b/core/kordophoned/src/xpc/mod.rs @@ -15,10 +15,16 @@ pub struct DispatchResult { impl DispatchResult { pub fn new(message: Message) -> Self { - Self { message, cleanup: None } + Self { + message, + cleanup: None, + } } pub fn with_cleanup(message: Message, cleanup: T) -> Self { - Self { message, cleanup: Some(Box::new(cleanup)) } + Self { + message, + cleanup: Some(Box::new(cleanup)), + } } } diff --git a/core/kordophoned/src/xpc/rpc.rs b/core/kordophoned/src/xpc/rpc.rs index 0d8b5c3..c057468 100644 --- a/core/kordophoned/src/xpc/rpc.rs +++ b/core/kordophoned/src/xpc/rpc.rs @@ -105,7 +105,12 @@ pub async fn dispatch( .and_then(|m| dict_get_str(m, "conversation_id")) { Some(id) => id, - None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing conversation_id")), + None => { + return DispatchResult::new(make_error_reply( + "InvalidRequest", + "Missing conversation_id", + )) + } }; match agent .send_event(|r| Event::SyncConversation(conversation_id, r)) @@ -122,7 +127,12 @@ pub async fn dispatch( .and_then(|m| dict_get_str(m, "conversation_id")) { Some(id) => id, - None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing conversation_id")), + None => { + return DispatchResult::new(make_error_reply( + "InvalidRequest", + "Missing conversation_id", + )) + } }; match agent .send_event(|r| Event::MarkConversationAsRead(conversation_id, r)) @@ -137,11 +147,21 @@ pub async fn dispatch( "GetMessages" => { let args = match get_dictionary_field(root, "arguments") { Some(a) => a, - None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing arguments")), + None => { + return DispatchResult::new(make_error_reply( + "InvalidRequest", + "Missing arguments", + )) + } }; let conversation_id = match dict_get_str(args, "conversation_id") { Some(id) => id, - None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing conversation_id")), + None => { + return DispatchResult::new(make_error_reply( + "InvalidRequest", + "Missing conversation_id", + )) + } }; let last_message_id = dict_get_str(args, "last_message_id"); match agent @@ -158,13 +178,10 @@ pub async fn dispatch( dict_put_str(&mut m, "sender", &msg.sender.display_name()); // Include attachment GUIDs for the client to resolve/download - let attachment_guids: Vec = msg - .attachments - .iter() - .map(|a| a.guid.clone()) - .collect(); + let attachment_guids: Vec = + 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 = Vec::new(); for attachment in msg.attachments.iter() { @@ -193,12 +210,23 @@ pub async fn dispatch( 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); + 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); + dict_put_i64_as_str( + &mut attribution_map, + "height", + height as i64, + ); } - metadata_map.insert(cstr("attribution_info"), Message::Dictionary(attribution_map)); + metadata_map.insert( + cstr("attribution_info"), + Message::Dictionary(attribution_map), + ); } if !metadata_map.is_empty() { a.insert(cstr("metadata"), Message::Dictionary(metadata_map)); @@ -208,7 +236,7 @@ pub async fn dispatch( 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(); @@ -230,11 +258,21 @@ pub async fn dispatch( "SendMessage" => { let args = match get_dictionary_field(root, "arguments") { Some(a) => a, - None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing arguments")), + None => { + return DispatchResult::new(make_error_reply( + "InvalidRequest", + "Missing arguments", + )) + } }; let conversation_id = match dict_get_str(args, "conversation_id") { Some(v) => v, - None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing conversation_id")), + None => { + return DispatchResult::new(make_error_reply( + "InvalidRequest", + "Missing conversation_id", + )) + } }; let text = dict_get_str(args, "text").unwrap_or_default(); let attachment_guids: Vec = match args.get(&cstr("attachment_guids")) { @@ -265,11 +303,21 @@ pub async fn dispatch( "GetAttachmentInfo" => { let args = match get_dictionary_field(root, "arguments") { Some(a) => a, - None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing arguments")), + None => { + return DispatchResult::new(make_error_reply( + "InvalidRequest", + "Missing arguments", + )) + } }; let attachment_id = match dict_get_str(args, "attachment_id") { Some(v) => v, - None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing attachment_id")), + None => { + return DispatchResult::new(make_error_reply( + "InvalidRequest", + "Missing attachment_id", + )) + } }; match agent .send_event(|r| Event::GetAttachment(attachment_id, r)) @@ -308,11 +356,21 @@ pub async fn dispatch( "OpenAttachmentFd" => { let args = match get_dictionary_field(root, "arguments") { Some(a) => a, - None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing arguments")), + None => { + return DispatchResult::new(make_error_reply( + "InvalidRequest", + "Missing arguments", + )) + } }; let attachment_id = match dict_get_str(args, "attachment_id") { Some(v) => v, - None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing attachment_id")), + None => { + return DispatchResult::new(make_error_reply( + "InvalidRequest", + "Missing attachment_id", + )) + } }; let preview = dict_get_str(args, "preview") .map(|s| s == "true") @@ -324,7 +382,7 @@ pub async fn dispatch( { 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) => { @@ -335,9 +393,14 @@ pub async fn dispatch( dict_put_str(&mut reply, "type", "OpenAttachmentFdResponse"); reply.insert(cstr("fd"), Message::Fd(fd)); - DispatchResult { message: Message::Dictionary(reply), cleanup: Some(Box::new(file)) } + DispatchResult { + message: Message::Dictionary(reply), + cleanup: Some(Box::new(file)), + } + } + Err(e) => { + DispatchResult::new(make_error_reply("OpenFailed", &format!("{}", e))) } - Err(e) => DispatchResult::new(make_error_reply("OpenFailed", &format!("{}", e))), } } Err(e) => DispatchResult::new(make_error_reply("DaemonError", &format!("{}", e))), @@ -348,11 +411,21 @@ pub async fn dispatch( "DownloadAttachment" => { let args = match get_dictionary_field(root, "arguments") { Some(a) => a, - None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing arguments")), + None => { + return DispatchResult::new(make_error_reply( + "InvalidRequest", + "Missing arguments", + )) + } }; let attachment_id = match dict_get_str(args, "attachment_id") { Some(v) => v, - None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing attachment_id")), + None => { + return DispatchResult::new(make_error_reply( + "InvalidRequest", + "Missing attachment_id", + )) + } }; let preview = dict_get_str(args, "preview") .map(|s| s == "true") @@ -371,11 +444,18 @@ pub async fn dispatch( use std::path::PathBuf; let args = match get_dictionary_field(root, "arguments") { Some(a) => a, - None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing arguments")), + None => { + return DispatchResult::new(make_error_reply( + "InvalidRequest", + "Missing arguments", + )) + } }; let path = match dict_get_str(args, "path") { Some(v) => v, - None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing path")), + None => { + return DispatchResult::new(make_error_reply("InvalidRequest", "Missing path")) + } }; match agent .send_event(|r| Event::UploadAttachment(PathBuf::from(path), r)) @@ -413,7 +493,12 @@ pub async fn dispatch( "UpdateSettings" => { let args = match get_dictionary_field(root, "arguments") { Some(a) => a, - None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing arguments")), + None => { + return DispatchResult::new(make_error_reply( + "InvalidRequest", + "Missing arguments", + )) + } }; let server_url = dict_get_str(args, "server_url"); let username = dict_get_str(args, "username"); diff --git a/core/kpcli/src/client/mod.rs b/core/kpcli/src/client/mod.rs index bce27c5..310967c 100644 --- a/core/kpcli/src/client/mod.rs +++ b/core/kpcli/src/client/mod.rs @@ -146,17 +146,15 @@ impl ClientCli { loop { match stream.next().await.unwrap() { - Ok(update) => { - match update { - SocketUpdate::Update(updates) => { - for update in updates { - println!("Got update: {:?}", update); - } - } - SocketUpdate::Pong => { - println!("Pong"); + Ok(update) => match update { + SocketUpdate::Update(updates) => { + for update in updates { + println!("Got update: {:?}", update); } } + SocketUpdate::Pong => { + println!("Pong"); + } }, Err(e) => { diff --git a/core/kpcli/src/daemon/dbus.rs b/core/kpcli/src/daemon/dbus.rs index 96d1178..a508b3a 100644 --- a/core/kpcli/src/daemon/dbus.rs +++ b/core/kpcli/src/daemon/dbus.rs @@ -209,4 +209,9 @@ impl DaemonInterface for DBusDaemonInterface { KordophoneRepository::mark_conversation_as_read(&self.proxy(), &conversation_id) .map_err(|e| anyhow::anyhow!("Failed to mark conversation as read: {}", e)) } + + async fn test_notification(&mut self, summary: String, body: String) -> Result<()> { + KordophoneRepository::test_notification(&self.proxy(), &summary, &body) + .map_err(|e| anyhow::anyhow!("Failed to trigger test notification: {}", e)) + } } diff --git a/core/kpcli/src/daemon/mod.rs b/core/kpcli/src/daemon/mod.rs index 872ab1b..4fb418e 100644 --- a/core/kpcli/src/daemon/mod.rs +++ b/core/kpcli/src/daemon/mod.rs @@ -32,6 +32,7 @@ pub trait DaemonInterface { async fn download_attachment(&mut self, attachment_id: String) -> Result<()>; async fn upload_attachment(&mut self, path: String) -> Result<()>; async fn mark_conversation_as_read(&mut self, conversation_id: String) -> Result<()>; + async fn test_notification(&mut self, summary: String, body: String) -> Result<()>; } struct StubDaemonInterface; @@ -112,6 +113,11 @@ impl DaemonInterface for StubDaemonInterface { "Daemon interface not implemented on this platform" )) } + async fn test_notification(&mut self, _summary: String, _body: String) -> Result<()> { + Err(anyhow::anyhow!( + "Daemon interface not implemented on this platform" + )) + } } pub fn new_daemon_interface() -> Result> { @@ -175,6 +181,9 @@ pub enum Commands { /// Marks a conversation as read. MarkConversationAsRead { conversation_id: String }, + + /// Displays a test notification using the daemon. + TestNotification { summary: String, body: String }, } #[derive(Subcommand)] @@ -219,6 +228,9 @@ impl Commands { Commands::MarkConversationAsRead { conversation_id } => { client.mark_conversation_as_read(conversation_id).await } + Commands::TestNotification { summary, body } => { + client.test_notification(summary, body).await + } } } } diff --git a/core/utilities/src/bin/snoozer.rs b/core/utilities/src/bin/snoozer.rs index 28f6c6c..825ecc6 100644 --- a/core/utilities/src/bin/snoozer.rs +++ b/core/utilities/src/bin/snoozer.rs @@ -1,13 +1,13 @@ use std::env; use std::process; -use kordophone::{ - api::{HTTPAPIClient, InMemoryAuthenticationStore, EventSocket}, - model::{ConversationID, event::EventData}, - APIInterface, -}; -use kordophone::api::http_client::Credentials; use kordophone::api::AuthenticationStore; +use kordophone::api::http_client::Credentials; +use kordophone::{ + APIInterface, + api::{EventSocket, HTTPAPIClient, InMemoryAuthenticationStore}, + model::{ConversationID, event::EventData}, +}; use futures_util::StreamExt; use hyper::Uri; @@ -18,7 +18,10 @@ async fn main() -> Result<(), Box> { let args: Vec = env::args().collect(); if args.len() < 2 { - eprintln!("Usage: {} [conversation_id2] [conversation_id3] ...", args[0]); + eprintln!( + "Usage: {} [conversation_id2] [conversation_id3] ...", + args[0] + ); eprintln!("Environment variables required:"); eprintln!(" KORDOPHONE_API_URL - Server URL"); eprintln!(" KORDOPHONE_USERNAME - Username for authentication"); @@ -30,65 +33,74 @@ async fn main() -> Result<(), Box> { let server_url: Uri = env::var("KORDOPHONE_API_URL") .map_err(|_| "KORDOPHONE_API_URL environment variable not set")? .parse()?; - + let username = env::var("KORDOPHONE_USERNAME") .map_err(|_| "KORDOPHONE_USERNAME environment variable not set")?; - + let password = env::var("KORDOPHONE_PASSWORD") .map_err(|_| "KORDOPHONE_PASSWORD environment variable not set")?; - + let credentials = Credentials { username, password }; - + // Collect all conversation IDs from command line arguments - let target_conversation_ids: Vec = args[1..].iter() - .map(|id| id.clone()) - .collect(); - - println!("Monitoring {} conversation(s) for updates: {:?}", - target_conversation_ids.len(), target_conversation_ids); - + let target_conversation_ids: Vec = + args[1..].iter().map(|id| id.clone()).collect(); + + println!( + "Monitoring {} conversation(s) for updates: {:?}", + target_conversation_ids.len(), + target_conversation_ids + ); + let auth_store = InMemoryAuthenticationStore::new(Some(credentials.clone())); let mut client = HTTPAPIClient::new(server_url, auth_store); let _ = client.authenticate(credentials).await?; - + // Open event socket let event_socket = client.open_event_socket(None).await?; let (mut stream, _sink) = event_socket.events().await; - + println!("Connected to event stream, waiting for updates..."); - + // Process events while let Some(event_result) = stream.next().await { match event_result { Ok(socket_event) => { match socket_event { - kordophone::api::event_socket::SocketEvent::Update(event) => { - match event.data { - EventData::MessageReceived(conversation, _message) => { - if target_conversation_ids.contains(&conversation.guid) { - println!("Message update detected for conversation {}, marking as read...", conversation.guid); - match client.mark_conversation_as_read(&conversation.guid).await { - Ok(_) => println!("Successfully marked conversation {} as read", conversation.guid), - Err(e) => eprintln!("Failed to mark conversation {} as read: {:?}", conversation.guid, e), - } + kordophone::api::event_socket::SocketEvent::Update(event) => match event.data { + EventData::MessageReceived(conversation, _message) => { + if target_conversation_ids.contains(&conversation.guid) { + println!( + "Message update detected for conversation {}, marking as read...", + conversation.guid + ); + match client.mark_conversation_as_read(&conversation.guid).await { + Ok(_) => println!( + "Successfully marked conversation {} as read", + conversation.guid + ), + Err(e) => eprintln!( + "Failed to mark conversation {} as read: {:?}", + conversation.guid, e + ), } - }, - - _ => {} + } } + + _ => {} }, kordophone::api::event_socket::SocketEvent::Pong => { // Ignore pong messages } } - }, + } Err(e) => { eprintln!("Error receiving event: {:?}", e); break; } } } - + println!("Event stream ended"); Ok(()) }