diff --git a/Cargo.lock b/Cargo.lock index 413a68724b4..c9dd3ef42f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,22 +63,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", - "cipher", + "cipher 0.4.4", "cpufeatures 0.2.17", ] [[package]] -name = "aes-gcm" -version = "0.10.3" +name = "aes" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +checksum = "66bd29a732b644c0431c6140f370d097879203d79b80c94a6747ba0872adaef8" dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "ghash", - "subtle", + "cipher 0.5.1", + "cpubits", + "cpufeatures 0.3.0", ] [[package]] @@ -199,7 +196,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -210,7 +207,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -623,7 +620,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sha1", + "sha1 0.10.6", "sync_wrapper", "tokio", "tokio-tungstenite 0.29.0", @@ -901,6 +898,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-padding" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block2" version = "0.6.2" @@ -1169,7 +1175,16 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" dependencies = [ - "cipher", + "cipher 0.4.4", +] + +[[package]] +name = "cbc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98db6aeaef0eeef2c1e3ce9a27b739218825dae116076352ac3777076aa22225" +dependencies = [ + "cipher 0.5.1", ] [[package]] @@ -1263,7 +1278,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", - "cipher", + "cipher 0.4.4", "cpufeatures 0.2.17", ] @@ -1286,7 +1301,7 @@ checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ "aead", "chacha20 0.9.1", - "cipher", + "cipher 0.4.4", "poly1305", "zeroize", ] @@ -1349,10 +1364,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common 0.1.7", - "inout", + "inout 0.1.4", "zeroize", ] +[[package]] +name = "cipher" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34d8227fe1ba289043aeb13792056ff80fd6de1a9f49137a5f499de8e8c78ea" +dependencies = [ + "block-buffer 0.12.0", + "crypto-common 0.2.1", + "inout 0.2.2", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -1483,6 +1509,7 @@ dependencies = [ "itoa", "rustversion", "ryu", + "serde", "static_assertions", ] @@ -1587,24 +1614,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "cookie_store" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206" -dependencies = [ - "cookie 0.18.1", - "document-features", - "idna", - "indexmap 2.14.0", - "log", - "serde", - "serde_derive", - "serde_json", - "time", - "url", -] - [[package]] name = "core-foundation" version = "0.10.1" @@ -1706,6 +1715,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -2093,7 +2108,16 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" dependencies = [ - "cipher", + "cipher 0.4.4", +] + +[[package]] +name = "ctr" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17469f8eb9bdbfad10f71f4cfddfd38b01143520c0e717d8796ccb4d44d44e42" +dependencies = [ + "cipher 0.5.1", ] [[package]] @@ -2202,20 +2226,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "dashmap" -version = "6.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" -dependencies = [ - "cfg-if", - "crossbeam-utils", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", -] - [[package]] name = "dasp_sample" version = "0.11.0" @@ -2501,7 +2511,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2647,7 +2657,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a8bfa975b1aec2145850fcaa1c6fe269a16578c44705a532ae3edc92b8881c7" dependencies = [ - "cipher", + "cipher 0.4.4", ] [[package]] @@ -2815,7 +2825,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3611,11 +3621,10 @@ dependencies = [ [[package]] name = "ghash" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" dependencies = [ - "opaque-debug", "polyval", ] @@ -3860,12 +3869,6 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - [[package]] name = "hashbrown" version = "0.15.5" @@ -3981,6 +3984,15 @@ dependencies = [ "hmac 0.12.1", ] +[[package]] +name = "hkdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +dependencies = [ + "hmac 0.13.0", +] + [[package]] name = "hmac" version = "0.12.1" @@ -4188,7 +4200,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core 0.61.2", ] [[package]] @@ -4557,10 +4569,20 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ - "block-padding", + "block-padding 0.3.3", "generic-array", ] +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "block-padding 0.4.2", + "hybrid-array", +] + [[package]] name = "instability" version = "0.3.12" @@ -5120,9 +5142,9 @@ version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7184fdea2bc3cd272a1acec4030c321a8f9875e877b3f92a53f2f6033fdc289" dependencies = [ - "aes", + "aes 0.8.4", "bitflags 2.11.1", - "cbc", + "cbc 0.1.2", "ecb", "encoding_rs", "flate2", @@ -5486,18 +5508,18 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "948582d5461fa4066117e0a08828df16c3041d5f0c17aca679041fed30b4284a" dependencies = [ - "aes", + "aes 0.8.4", "aquamarine", "as_variant", "async-trait", "bs58", "byteorder", "cfg-if", - "ctr", + "ctr 0.9.2", "eyeball", "futures-core", "futures-util", - "hkdf", + "hkdf 0.12.4", "hmac 0.12.1", "itertools 0.14.0", "js_option", @@ -5532,7 +5554,7 @@ dependencies = [ "futures-util", "getrandom 0.4.2", "gloo-utils", - "hkdf", + "hkdf 0.12.4", "js-sys", "matrix-sdk-base", "matrix-sdk-crypto", @@ -5789,26 +5811,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "moka" -version = "0.12.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" -dependencies = [ - "async-lock", - "crossbeam-channel", - "crossbeam-epoch", - "crossbeam-utils", - "equivalent", - "event-listener 5.4.1", - "futures-util", - "parking_lot", - "portable-atomic", - "smallvec", - "tagptr", - "uuid", -] - [[package]] name = "moxcms" version = "0.8.1" @@ -5840,12 +5842,6 @@ dependencies = [ "windows-sys 0.60.2", ] -[[package]] -name = "multimap" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" - [[package]] name = "nanohtml2text" version = "0.2.1" @@ -6004,12 +6000,12 @@ version = "0.44.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3aa5e3b6a278ed061835fe1ee293b71641e6bf8b401cfe4e1834bbf4ef0a34e1" dependencies = [ - "aes", + "aes 0.8.4", "base64 0.22.1", "bech32", "bip39", "bitcoin_hashes", - "cbc", + "cbc 0.1.2", "chacha20 0.9.1", "chacha20poly1305", "getrandom 0.2.17", @@ -6082,7 +6078,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -6167,7 +6163,7 @@ dependencies = [ "once_cell", "rustix 1.1.4", "slab", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -6176,7 +6172,7 @@ version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" dependencies = [ - "base64 0.22.1", + "base64 0.21.7", "chrono", "getrandom 0.2.17", "http 1.4.0", @@ -6596,7 +6592,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.45.0", ] [[package]] @@ -6785,17 +6781,6 @@ dependencies = [ "indexmap 2.14.0", ] -[[package]] -name = "petgraph" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" -dependencies = [ - "fixedbitset 0.5.7", - "hashbrown 0.15.5", - "indexmap 2.14.0", -] - [[package]] name = "phf" version = "0.8.0" @@ -7155,19 +7140,18 @@ checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ "cpufeatures 0.2.17", "opaque-debug", - "universal-hash", + "universal-hash 0.5.1", ] [[package]] name = "polyval" -version = "0.6.2" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +checksum = "7dfc63250416fea14f5749b90725916a6c903f599d51cb635aa7a52bfd03eede" dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "opaque-debug", - "universal-hash", + "cpubits", + "cpufeatures 0.3.0", + "universal-hash 0.6.1", ] [[package]] @@ -7458,23 +7442,6 @@ dependencies = [ "prost-derive", ] -[[package]] -name = "prost-build" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" -dependencies = [ - "heck 0.5.0", - "itertools 0.14.0", - "log", - "multimap", - "petgraph 0.8.3", - "prost", - "prost-types", - "regex", - "tempfile", -] - [[package]] name = "prost-derive" version = "0.14.3" @@ -7482,41 +7449,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" dependencies = [ "anyhow", - "itertools 0.14.0", + "itertools 0.10.5", "proc-macro2", "quote", "syn 2.0.117", ] -[[package]] -name = "prost-types" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" -dependencies = [ - "prost", -] - -[[package]] -name = "protobuf" -version = "3.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" -dependencies = [ - "once_cell", - "protobuf-support", - "thiserror 1.0.69", -] - -[[package]] -name = "protobuf-support" -version = "3.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" -dependencies = [ - "thiserror 1.0.69", -] - [[package]] name = "pulldown-cmark" version = "0.13.3" @@ -8410,7 +8348,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -8488,7 +8426,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -8547,7 +8485,7 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" dependencies = [ - "cipher", + "cipher 0.4.4", ] [[package]] @@ -9056,6 +8994,17 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.2", +] + [[package]] name = "sha2" version = "0.10.9" @@ -9226,7 +9175,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -9516,12 +9465,6 @@ dependencies = [ "winx", ] -[[package]] -name = "tagptr" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" - [[package]] name = "tao" version = "0.34.8" @@ -9894,7 +9837,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -10614,7 +10557,7 @@ dependencies = [ "rand 0.9.4", "rustls", "rustls-pki-types", - "sha1", + "sha1 0.10.6", "thiserror 2.0.18", "utf-8", ] @@ -10633,7 +10576,7 @@ dependencies = [ "rand 0.9.4", "rustls", "rustls-pki-types", - "sha1", + "sha1 0.10.6", "thiserror 2.0.18", ] @@ -10719,7 +10662,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -10890,6 +10833,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "universal-hash" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" +dependencies = [ + "crypto-common 0.2.1", + "ctutils", +] + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -10915,14 +10868,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" dependencies = [ "base64 0.22.1", - "cookie_store", "flate2", "log", "percent-encoding", "rustls", "rustls-pki-types", - "serde", - "serde_json", "ureq-proto", "utf8-zero", "webpki-roots 1.0.7", @@ -11044,16 +10994,16 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b98bf83c0992966775b8012f194b07b44928996163e5a05b741b43891571ae5b" dependencies = [ - "aes", + "aes 0.8.4", "arrayvec", "base64 0.22.1", "base64ct", - "cbc", + "cbc 0.1.2", "chacha20poly1305", "curve25519-dalek", "ed25519-dalek", "getrandom 0.2.17", - "hkdf", + "hkdf 0.12.4", "hmac 0.12.1", "matrix-pickle", "prost", @@ -11098,116 +11048,88 @@ dependencies = [ ] [[package]] -name = "wa-rs" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fecb468bdfe1e7d4c06a1bd12908c66edaca59024862cb64757ad11c3b948b1" +name = "wacore" +version = "0.6.0" +source = "git+https://github.com/oxidezap/whatsapp-rust?rev=9734fb2ec544e22b7055147aa3e73b6889e3ff0d#9734fb2ec544e22b7055147aa3e73b6889e3ff0d" dependencies = [ + "aes 0.9.0", "anyhow", "async-channel 2.5.0", + "async-lock", "async-trait", "base64 0.22.1", "bytes", "chrono", - "dashmap", - "env_logger", + "ctr 0.10.0", + "event-listener 5.4.1", + "flate2", + "futures", "hex", + "hkdf 0.13.0", + "hmac 0.13.0", + "itoa", "log", - "moka", + "md5", + "portable-atomic", "prost", - "rand 0.9.4", - "rand_core 0.10.1", - "scopeguard", + "rand 0.10.1", "serde", + "serde-big-array", "serde_json", + "sha1 0.11.0", + "sha2 0.11.0", + "subtle", "thiserror 2.0.18", - "tokio", - "wa-rs-binary", - "wa-rs-core", - "wa-rs-proto", + "typed-builder", + "wacore-appstate", + "wacore-binary", + "wacore-derive", + "wacore-libsignal", + "wacore-noise", + "waproto", ] [[package]] -name = "wa-rs-appstate" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3845137b3aead2d99de7c6744784bf2f5a908be9dc97a3dbd7585dc40296925c" +name = "wacore-appstate" +version = "0.6.0" +source = "git+https://github.com/oxidezap/whatsapp-rust?rev=9734fb2ec544e22b7055147aa3e73b6889e3ff0d#9734fb2ec544e22b7055147aa3e73b6889e3ff0d" dependencies = [ "anyhow", "bytemuck", "hex", - "hkdf", + "hkdf 0.13.0", "log", "prost", "serde", "serde-big-array", "serde_json", - "sha2 0.10.9", + "sha2 0.11.0", "thiserror 2.0.18", - "wa-rs-binary", - "wa-rs-libsignal", - "wa-rs-proto", -] - -[[package]] -name = "wa-rs-binary" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3b30a6e11aebb39c07392675256ead5e2570c31382bd4835d6ddc877284b6be" -dependencies = [ - "flate2", - "phf 0.13.1", - "phf_codegen 0.13.1", - "serde", - "serde_json", + "wacore-binary", + "wacore-libsignal", + "waproto", ] [[package]] -name = "wa-rs-core" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed13bb2aff2de43fc4dd821955f03ea48a1d31eda3c80efe6f905898e304d11f" +name = "wacore-binary" +version = "0.6.0" +source = "git+https://github.com/oxidezap/whatsapp-rust?rev=9734fb2ec544e22b7055147aa3e73b6889e3ff0d#9734fb2ec544e22b7055147aa3e73b6889e3ff0d" dependencies = [ - "aes", - "aes-gcm", - "anyhow", - "async-channel 2.5.0", - "async-trait", - "base64 0.22.1", "bytes", - "chrono", - "ctr", + "compact_str", "flate2", - "hex", - "hkdf", - "hmac 0.12.1", - "log", - "md5", - "once_cell", - "pbkdf2", - "prost", - "protobuf", - "rand 0.9.4", - "rand_core 0.10.1", + "hashify", + "itoa", "serde", - "serde-big-array", "serde_json", - "sha2 0.10.9", - "thiserror 2.0.18", - "typed-builder", - "wa-rs-appstate", - "wa-rs-binary", - "wa-rs-derive", - "wa-rs-libsignal", - "wa-rs-noise", - "wa-rs-proto", + "stable_deref_trait", + "yoke 0.8.2", ] [[package]] -name = "wa-rs-derive" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75c03f610c9bc960e653d5d6d2a4cced9013bedbe5e6e8948787bbd418e4137c" +name = "wacore-derive" +version = "0.6.0" +source = "git+https://github.com/oxidezap/whatsapp-rust?rev=9734fb2ec544e22b7055147aa3e73b6889e3ff0d#9734fb2ec544e22b7055147aa3e73b6889e3ff0d" dependencies = [ "proc-macro2", "quote", @@ -11215,103 +11137,53 @@ dependencies = [ ] [[package]] -name = "wa-rs-libsignal" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3471be8ff079ae4959fcddf2e7341281e5c6756bdc6a66454ea1a8e474d14576" +name = "wacore-libsignal" +version = "0.6.0" +source = "git+https://github.com/oxidezap/whatsapp-rust?rev=9734fb2ec544e22b7055147aa3e73b6889e3ff0d#9734fb2ec544e22b7055147aa3e73b6889e3ff0d" dependencies = [ - "aes", - "aes-gcm", + "aes 0.9.0", "arrayref", "async-trait", - "cbc", + "bytes", + "cbc 0.2.0", "chrono", - "ctr", + "ctr 0.10.0", "curve25519-dalek", "derive_more 2.1.1", "displaydoc", "ghash", "hex", - "hkdf", - "hmac 0.12.1", - "itertools 0.14.0", + "hkdf 0.13.0", + "hmac 0.13.0", "log", "prost", - "rand 0.9.4", + "rand 0.10.1", "serde", - "sha1", - "sha2 0.10.9", + "sha1 0.11.0", + "sha2 0.11.0", "subtle", "thiserror 2.0.18", "uuid", - "wa-rs-proto", + "waproto", "x25519-dalek", ] [[package]] -name = "wa-rs-noise" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3efb3891c1e22ce54646dc581e34e79377dc402ed8afb11a7671c5ef629b3ae" +name = "wacore-noise" +version = "0.6.0" +source = "git+https://github.com/oxidezap/whatsapp-rust?rev=9734fb2ec544e22b7055147aa3e73b6889e3ff0d#9734fb2ec544e22b7055147aa3e73b6889e3ff0d" dependencies = [ - "aes-gcm", "anyhow", "bytes", - "hkdf", + "hkdf 0.13.0", "log", "prost", - "rand 0.9.4", - "rand_core 0.10.1", - "sha2 0.10.9", + "rand 0.10.1", + "sha2 0.11.0", "thiserror 2.0.18", - "wa-rs-binary", - "wa-rs-libsignal", - "wa-rs-proto", -] - -[[package]] -name = "wa-rs-proto" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ada50ee03752f0e66ada8cf415ed5f90d572d34039b058ce23d8b13493e510" -dependencies = [ - "prost", - "prost-build", - "serde", -] - -[[package]] -name = "wa-rs-tokio-transport" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfc638c168949dc99cbb756a776869898d4ae654b36b90d5f7ce2d32bf92a404" -dependencies = [ - "anyhow", - "async-channel 2.5.0", - "async-trait", - "bytes", - "futures-util", - "http 1.4.0", - "log", - "rustls", - "tokio", - "tokio-rustls", - "tokio-websockets", - "wa-rs-core", - "webpki-roots 1.0.7", -] - -[[package]] -name = "wa-rs-ureq-http" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d0c7fff8a7bd93d0c17af8d797a3934144fa269fe47a615635f3bf04238806" -dependencies = [ - "anyhow", - "async-trait", - "tokio", - "ureq", - "wa-rs-core", + "wacore-binary", + "wacore-libsignal", + "waproto", ] [[package]] @@ -11333,6 +11205,15 @@ dependencies = [ "try-lock", ] +[[package]] +name = "waproto" +version = "0.6.0" +source = "git+https://github.com/oxidezap/whatsapp-rust?rev=9734fb2ec544e22b7055147aa3e73b6889e3ff0d#9734fb2ec544e22b7055147aa3e73b6889e3ff0d" +dependencies = [ + "prost", + "serde", +] + [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" @@ -11473,7 +11354,7 @@ dependencies = [ "im-rc", "indexmap 2.14.0", "log", - "petgraph 0.6.5", + "petgraph", "serde", "serde_derive", "serde_yaml", @@ -12145,6 +12026,69 @@ dependencies = [ "wezterm-dynamic", ] +[[package]] +name = "whatsapp-rust" +version = "0.6.0" +source = "git+https://github.com/oxidezap/whatsapp-rust?rev=9734fb2ec544e22b7055147aa3e73b6889e3ff0d#9734fb2ec544e22b7055147aa3e73b6889e3ff0d" +dependencies = [ + "anyhow", + "async-channel 2.5.0", + "async-lock", + "async-trait", + "base64 0.22.1", + "bytes", + "chrono", + "env_logger", + "event-listener 5.4.1", + "futures", + "hex", + "itoa", + "log", + "portable-atomic", + "prost", + "rand 0.10.1", + "scopeguard", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "wacore", + "wacore-binary", + "waproto", +] + +[[package]] +name = "whatsapp-rust-tokio-transport" +version = "0.6.0" +source = "git+https://github.com/oxidezap/whatsapp-rust?rev=9734fb2ec544e22b7055147aa3e73b6889e3ff0d#9734fb2ec544e22b7055147aa3e73b6889e3ff0d" +dependencies = [ + "anyhow", + "async-channel 2.5.0", + "async-trait", + "bytes", + "futures-util", + "http 1.4.0", + "log", + "rustls", + "tokio", + "tokio-rustls", + "tokio-websockets", + "wacore", + "webpki-roots 1.0.7", +] + +[[package]] +name = "whatsapp-rust-ureq-http-client" +version = "0.6.0" +source = "git+https://github.com/oxidezap/whatsapp-rust?rev=9734fb2ec544e22b7055147aa3e73b6889e3ff0d#9734fb2ec544e22b7055147aa3e73b6889e3ff0d" +dependencies = [ + "anyhow", + "async-trait", + "tokio", + "ureq", + "wacore", +] + [[package]] name = "which" version = "8.0.2" @@ -12236,7 +12180,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -12332,20 +12276,7 @@ dependencies = [ "windows-interface", "windows-link 0.1.3", "windows-result 0.3.4", - "windows-strings 0.4.2", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", + "windows-strings", ] [[package]] @@ -12421,15 +12352,6 @@ dependencies = [ "windows-link 0.1.3", ] -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link 0.2.1", -] - [[package]] name = "windows-strings" version = "0.4.2" @@ -12439,15 +12361,6 @@ dependencies = [ "windows-link 0.1.3", ] -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link 0.2.1", -] - [[package]] name = "windows-sys" version = "0.45.0" @@ -13166,12 +13079,13 @@ dependencies = [ name = "zeroclaw-channels" version = "0.8.0-beta-1" dependencies = [ - "aes", + "aes 0.8.4", "anyhow", "async-imap", "async-trait", "axum", "base64 0.22.1", + "bytes", "chrono", "cpal", "directories", @@ -13213,13 +13127,13 @@ dependencies = [ "toml 1.1.2+spec-1.1.0", "urlencoding", "uuid", - "wa-rs", - "wa-rs-binary", - "wa-rs-core", - "wa-rs-proto", - "wa-rs-tokio-transport", - "wa-rs-ureq-http", + "wacore", + "wacore-binary", + "waproto", "webpki-roots 1.0.7", + "whatsapp-rust", + "whatsapp-rust-tokio-transport", + "whatsapp-rust-ureq-http-client", "wiremock", "zeroclaw-api", "zeroclaw-config", diff --git a/Cargo.toml b/Cargo.toml index a6a4cfeac24..522c1c68c20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -232,7 +232,7 @@ mime_guess = { version = "2", optional = true } # (Linux, macOS, Windows). Android/Termux uses target_os="android" and is excluded. -# Uses wa-rs for Bot and Client, wa-rs-core for storage traits, custom rusqlite backend avoids Diesel conflict. +# Uses whatsapp-rust for Bot and Client, wacore for storage traits, custom rusqlite backend avoids Diesel conflict. # Unix-specific dependencies (for root check, etc.) [target.'cfg(unix)'.dependencies] diff --git a/crates/zeroclaw-channels/Cargo.toml b/crates/zeroclaw-channels/Cargo.toml index 230e8479399..0d7822e2718 100644 --- a/crates/zeroclaw-channels/Cargo.toml +++ b/crates/zeroclaw-channels/Cargo.toml @@ -58,16 +58,28 @@ webpki-roots = "1.0.6" tokio-socks = "0.5" # WhatsApp Web (optional) -wa-rs = { version = "0.2", optional = true, default-features = false } -wa-rs-core = { version = "0.2", optional = true, default-features = false } -wa-rs-binary = { version = "0.2", optional = true, default-features = false } -wa-rs-proto = { version = "0.2", optional = true, default-features = false } +# Upstream crates from oxidezap/whatsapp-rust, temporarily pinned to commit +# 9734fb2 (the MSRV-fix branch carrying oxidezap/whatsapp-rust#632). The +# published 0.6.0 crate uses an `if let` guard at wacore/src/history_sync.rs:93 +# that is stable only on Rust 1.94+; ZeroClaw CI pins 1.93.0. Pinning via a +# plain `git` source (not `[patch.crates-io]`) keeps the workspace buildable +# until oxidezap publishes the fix as 0.6.1, at which point this block +# reverts to versioned crates.io deps. +# Fixes the post-2026-04-24 WhatsApp Web protocol break (#6246). +whatsapp-rust = { git = "https://github.com/oxidezap/whatsapp-rust", rev = "9734fb2ec544e22b7055147aa3e73b6889e3ff0d", optional = true, default-features = false, features = ["tokio-runtime"] } +wacore = { git = "https://github.com/oxidezap/whatsapp-rust", rev = "9734fb2ec544e22b7055147aa3e73b6889e3ff0d", optional = true, default-features = false } +wacore-binary = { git = "https://github.com/oxidezap/whatsapp-rust", rev = "9734fb2ec544e22b7055147aa3e73b6889e3ff0d", optional = true, default-features = false } +waproto = { git = "https://github.com/oxidezap/whatsapp-rust", rev = "9734fb2ec544e22b7055147aa3e73b6889e3ff0d", optional = true, default-features = false } serde-big-array = { version = "0.5", optional = true } cpal = { version = "0.15", optional = true } -wa-rs-ureq-http = { version = "0.2", optional = true } -wa-rs-tokio-transport = { version = "0.2", optional = true, default-features = false } +whatsapp-rust-ureq-http-client = { git = "https://github.com/oxidezap/whatsapp-rust", rev = "9734fb2ec544e22b7055147aa3e73b6889e3ff0d", optional = true } +whatsapp-rust-tokio-transport = { git = "https://github.com/oxidezap/whatsapp-rust", rev = "9734fb2ec544e22b7055147aa3e73b6889e3ff0d", optional = true, default-features = false } qrcode = { version = "0.14", optional = true } +# `bytes` is required for the `Bytes` return types in wacore 0.6 storage +# traits (SignalStore::get_session / load_prekey). Feature-gated to +# whatsapp-web alongside the rest of the WA Web stack. +bytes = { version = "1", optional = true } shellexpand = "3.1" # WeChat iLink (optional) — AES-128-ECB and MD5 are mandated by the iLink @@ -93,7 +105,7 @@ channel-telegram = ["dep:image"] channel-lark = ["dep:prost"] channel-line = [] channel-nostr = ["dep:nostr-sdk", "zeroclaw-config/channel-nostr"] -whatsapp-web = ["dep:wa-rs", "dep:wa-rs-core", "dep:wa-rs-binary", "dep:wa-rs-proto", "dep:serde-big-array", "dep:wa-rs-ureq-http", "dep:wa-rs-tokio-transport", "dep:qrcode", "dep:prost"] +whatsapp-web = ["dep:whatsapp-rust", "dep:wacore", "dep:wacore-binary", "dep:waproto", "dep:serde-big-array", "dep:whatsapp-rust-ureq-http-client", "dep:whatsapp-rust-tokio-transport", "dep:qrcode", "dep:bytes", "dep:prost"] # Channels with no optional deps (cfg gate only) channel-discord = [] channel-slack = [] diff --git a/crates/zeroclaw-channels/src/whatsapp_storage.rs b/crates/zeroclaw-channels/src/whatsapp_storage.rs index 1382d73d00f..0800dfb0d79 100644 --- a/crates/zeroclaw-channels/src/whatsapp_storage.rs +++ b/crates/zeroclaw-channels/src/whatsapp_storage.rs @@ -22,21 +22,21 @@ use std::path::Path; use std::sync::Arc; #[cfg(feature = "whatsapp-web")] -use prost::Message; +use bytes::Bytes; #[cfg(feature = "whatsapp-web")] -use wa_rs_binary::jid::Jid; +use prost::Message; #[cfg(feature = "whatsapp-web")] -use wa_rs_core::appstate::hash::HashState; +use wacore::appstate::hash::HashState; #[cfg(feature = "whatsapp-web")] -use wa_rs_core::appstate::processor::AppStateMutationMAC; +use wacore::appstate::processor::AppStateMutationMAC; #[cfg(feature = "whatsapp-web")] -use wa_rs_core::store::Device as CoreDevice; +use wacore::store::Device as CoreDevice; #[cfg(feature = "whatsapp-web")] -use wa_rs_core::store::traits::DeviceInfo; +use wacore::store::traits::DeviceInfo; #[cfg(feature = "whatsapp-web")] -use wa_rs_core::store::traits::DeviceStore as DeviceStoreTrait; +use wacore::store::traits::DeviceStore as DeviceStoreTrait; #[cfg(feature = "whatsapp-web")] -use wa_rs_core::store::traits::*; +use wacore::store::traits::*; /// Custom wa-rs storage backend using rusqlite /// @@ -56,16 +56,25 @@ pub struct RusqliteStore { /// Helper macro to convert rusqlite errors to StoreError /// For execute statements that return usize, maps to () +/// +/// Wraps the underlying error in a `Box` +/// to match the `StoreError::Database` variant signature in wacore 0.6. macro_rules! to_store_err { // For expressions returning Result (execute: $expr:expr) => { - $expr - .map(|_| ()) - .map_err(|e| wa_rs_core::store::error::StoreError::Database(e.to_string())) + $expr.map(|_| ()).map_err(|e| { + wacore::store::error::StoreError::Database( + Box::new(e) as Box + ) + }) }; // For other expressions ($expr:expr) => { - $expr.map_err(|e| wa_rs_core::store::error::StoreError::Database(e.to_string())) + $expr.map_err(|e| { + wacore::store::error::StoreError::Database( + Box::new(e) as Box + ) + }) }; } @@ -105,8 +114,71 @@ impl RusqliteStore { /// Initialize all database tables fn init_schema(&self) -> anyhow::Result<()> { - let conn = self.conn.lock(); - to_store_err!(conn.execute_batch( + let mut conn = self.conn.lock(); + + // Decide whether the `raw_id` ALTER is needed BEFORE opening the tx. + // PRAGMA table_info is read-only and may target a not-yet-created + // table (returns no rows) — in that case the CREATE TABLE inside the + // transaction will produce the column anyway, so `needs_raw_id` stays + // false and we correctly skip the ALTER. + let needs_raw_id = { + let mut stmt = conn.prepare("PRAGMA table_info(device_registry)")?; + let mut has_raw_id = false; + let mut table_exists = false; + let rows = stmt.query_map([], |row| row.get::<_, String>(1))?; + for r in rows { + table_exists = true; + if r? == "raw_id" { + has_raw_id = true; + break; + } + } + table_exists && !has_raw_id + }; + + // Probe `device` for the 5 wacore-0.6 columns. Each entry is + // (column_name, SQL fragment for ALTER TABLE ... ADD COLUMN). + // The order mirrors upstream's sqlite-storage migration history + // so a sqlite-browser diff against an upstream DB is readable. + // SQLite has no `ADD COLUMN IF NOT EXISTS`, so we resolve which + // ones to add up-front and apply only the missing ones inside + // the transaction — same crash-safety contract as `raw_id`. + let device_06_migrations: Vec<(&'static str, &'static str)> = { + let mut existing: std::collections::HashSet = std::collections::HashSet::new(); + let mut stmt = conn.prepare("PRAGMA table_info(device)")?; + let rows = stmt.query_map([], |row| row.get::<_, String>(1))?; + for r in rows { + existing.insert(r?); + } + const ALL: &[(&str, &str)] = &[ + ("next_pre_key_id", "INTEGER NOT NULL DEFAULT 0"), + ("server_has_prekeys", "INTEGER NOT NULL DEFAULT 0"), + ("nct_salt", "BLOB"), + ("server_cert_chain", "BLOB"), + ("login_counter", "INTEGER NOT NULL DEFAULT 0"), + ]; + // If the table doesn't exist yet (existing is empty), the + // CREATE TABLE inside the transaction will define all five + // columns, so we want an empty migration list. The same + // empty-set check that `needs_raw_id` relies on applies here. + if existing.is_empty() { + Vec::new() + } else { + ALL.iter() + .copied() + .filter(|(col, _)| !existing.contains(*col)) + .collect() + } + }; + + // Wrap CREATEs + the conditional ALTER in a single transaction so a + // crash between them can't leave the DB with new tables but no + // `raw_id` column — that state survives reboots because the PRAGMA + // probe sees the column as missing yet the ALTER may have already + // been recorded as run. + let tx = to_store_err!(conn.transaction())?; + + to_store_err!(tx.execute_batch( "-- Main device table CREATE TABLE IF NOT EXISTS device ( id INTEGER PRIMARY KEY, @@ -126,7 +198,12 @@ impl RusqliteStore { app_version_tertiary INTEGER NOT NULL, app_version_last_fetched_ms INTEGER NOT NULL, edge_routing_info BLOB, - props_hash TEXT + props_hash TEXT, + next_pre_key_id INTEGER NOT NULL DEFAULT 0, + server_has_prekeys INTEGER NOT NULL DEFAULT 0, + nct_salt BLOB, + server_cert_chain BLOB, + login_counter INTEGER NOT NULL DEFAULT 0 ); -- Signal identity keys @@ -217,16 +294,45 @@ impl RusqliteStore { ); -- Device registry for multi-device + -- `raw_id` (NULL on legacy rows) is the ADV identity index added + -- in wacore 0.6 — used to detect identity changes that require + -- full session/sender-key invalidation per WA Web parity. CREATE TABLE IF NOT EXISTS device_registry ( user_id TEXT NOT NULL, devices_json TEXT NOT NULL, timestamp INTEGER NOT NULL, phash TEXT, + raw_id INTEGER, device_id INTEGER NOT NULL, updated_at INTEGER NOT NULL, PRIMARY KEY (user_id, device_id) ); + -- Per-device sender-key tracking (wacore 0.6: replaces the + -- skdm_recipients / sender_key_status pair). Each row records + -- whether a known group device has a valid sender key (1) or + -- needs a fresh SKDM (0). + CREATE TABLE IF NOT EXISTS sender_key_devices ( + group_jid TEXT NOT NULL, + device_jid TEXT NOT NULL, + has_key INTEGER NOT NULL, + device_id INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + PRIMARY KEY (group_jid, device_jid, device_id) + ); + + -- Sent message retry store (wacore 0.6: WA Web getMessageTable + -- parity). Stores serialized payloads keyed by (chat, message_id) + -- so retry-receipts can re-encrypt + resend the original message. + CREATE TABLE IF NOT EXISTS sent_messages ( + chat_jid TEXT NOT NULL, + message_id TEXT NOT NULL, + payload BLOB NOT NULL, + device_id INTEGER NOT NULL, + created_at INTEGER NOT NULL, + PRIMARY KEY (chat_jid, message_id, device_id) + ); + -- Base keys for collision detection CREATE TABLE IF NOT EXISTS base_keys ( address TEXT NOT NULL, @@ -255,8 +361,44 @@ impl RusqliteStore { device_id INTEGER NOT NULL, updated_at INTEGER NOT NULL, PRIMARY KEY (jid, device_id) - );", + ); + + -- Index supporting `delete_expired_sent_messages` + -- (WHERE device_id = ? AND created_at < ?). Without it the cleanup + -- pass would full-scan `sent_messages`, which grows unbounded until + -- the periodic cleanup hook lands. `IF NOT EXISTS` keeps re-init + -- idempotent across restarts. + CREATE INDEX IF NOT EXISTS idx_sent_messages_device_created + ON sent_messages(device_id, created_at);", ))?; + + // Migration: ensure `raw_id` column exists on legacy device_registry + // rows (added in wacore 0.6 for ADV identity-change detection). + // SQLite has no `IF NOT EXISTS` for ADD COLUMN, so we use the pragma + // probe performed above to skip the ALTER if it is already applied. + // Runs inside the same transaction as the CREATEs so a crash between + // them rolls everything back. + if needs_raw_id { + to_store_err!(execute: tx.execute( + "ALTER TABLE device_registry ADD COLUMN raw_id INTEGER", + [], + ))?; + } + + // Apply the wacore-0.6 device column migrations inside the same + // transaction as the CREATEs + raw_id ALTER. SQLite refuses to + // ALTER TABLE if the column already exists, so we use the + // pre-computed `device_06_migrations` list rather than a blanket + // probe inside the loop (which would re-read PRAGMA after each + // ALTER and complicate failure modes). + for (col, ty) in &device_06_migrations { + to_store_err!(execute: tx.execute( + &format!("ALTER TABLE device ADD COLUMN {col} {ty}"), + [], + ))?; + } + + to_store_err!(tx.commit())?; Ok(()) } } @@ -266,11 +408,7 @@ impl RusqliteStore { impl SignalStore for RusqliteStore { // --- Identity Operations --- - async fn put_identity( - &self, - address: &str, - key: [u8; 32], - ) -> wa_rs_core::store::error::Result<()> { + async fn put_identity(&self, address: &str, key: [u8; 32]) -> wacore::store::error::Result<()> { let conn = self.conn.lock(); to_store_err!(execute: conn.execute( "INSERT OR REPLACE INTO identities (address, key, device_id) @@ -279,10 +417,7 @@ impl SignalStore for RusqliteStore { )) } - async fn load_identity( - &self, - address: &str, - ) -> wa_rs_core::store::error::Result>> { + async fn load_identity(&self, address: &str) -> wacore::store::error::Result> { let conn = self.conn.lock(); let result = conn.query_row( "SELECT key FROM identities WHERE address = ?1 AND device_id = ?2", @@ -291,15 +426,23 @@ impl SignalStore for RusqliteStore { ); match result { - Ok(key) => Ok(Some(key)), + Ok(key) => { + if key.len() != 32 { + return Err(wacore::store::error::StoreError::Validation(format!( + "identity key has invalid length {}, expected 32", + key.len() + ))); + } + let mut arr = [0u8; 32]; + arr.copy_from_slice(&key); + Ok(Some(arr)) + } Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), - Err(e) => Err(wa_rs_core::store::error::StoreError::Database( - e.to_string(), - )), + Err(e) => Err(wacore::store::error::StoreError::Database(Box::new(e))), } } - async fn delete_identity(&self, address: &str) -> wa_rs_core::store::error::Result<()> { + async fn delete_identity(&self, address: &str) -> wacore::store::error::Result<()> { let conn = self.conn.lock(); to_store_err!(execute: conn.execute( "DELETE FROM identities WHERE address = ?1 AND device_id = ?2", @@ -309,10 +452,7 @@ impl SignalStore for RusqliteStore { // --- Session Operations --- - async fn get_session( - &self, - address: &str, - ) -> wa_rs_core::store::error::Result>> { + async fn get_session(&self, address: &str) -> wacore::store::error::Result> { let conn = self.conn.lock(); let result = conn.query_row( "SELECT record FROM sessions WHERE address = ?1 AND device_id = ?2", @@ -321,19 +461,13 @@ impl SignalStore for RusqliteStore { ); match result { - Ok(record) => Ok(Some(record)), + Ok(record) => Ok(Some(Bytes::from(record))), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), - Err(e) => Err(wa_rs_core::store::error::StoreError::Database( - e.to_string(), - )), + Err(e) => Err(wacore::store::error::StoreError::Database(Box::new(e))), } } - async fn put_session( - &self, - address: &str, - session: &[u8], - ) -> wa_rs_core::store::error::Result<()> { + async fn put_session(&self, address: &str, session: &[u8]) -> wacore::store::error::Result<()> { let conn = self.conn.lock(); to_store_err!(execute: conn.execute( "INSERT OR REPLACE INTO sessions (address, record, device_id) @@ -342,7 +476,7 @@ impl SignalStore for RusqliteStore { )) } - async fn delete_session(&self, address: &str) -> wa_rs_core::store::error::Result<()> { + async fn delete_session(&self, address: &str) -> wacore::store::error::Result<()> { let conn = self.conn.lock(); to_store_err!(execute: conn.execute( "DELETE FROM sessions WHERE address = ?1 AND device_id = ?2", @@ -357,7 +491,7 @@ impl SignalStore for RusqliteStore { id: u32, record: &[u8], uploaded: bool, - ) -> wa_rs_core::store::error::Result<()> { + ) -> wacore::store::error::Result<()> { let conn = self.conn.lock(); to_store_err!(execute: conn.execute( "INSERT OR REPLACE INTO prekeys (id, key, uploaded, device_id) @@ -366,7 +500,7 @@ impl SignalStore for RusqliteStore { )) } - async fn load_prekey(&self, id: u32) -> wa_rs_core::store::error::Result>> { + async fn load_prekey(&self, id: u32) -> wacore::store::error::Result> { let conn = self.conn.lock(); let result = conn.query_row( "SELECT key FROM prekeys WHERE id = ?1 AND device_id = ?2", @@ -375,15 +509,33 @@ impl SignalStore for RusqliteStore { ); match result { - Ok(key) => Ok(Some(key)), + Ok(key) => Ok(Some(Bytes::from(key))), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), - Err(e) => Err(wa_rs_core::store::error::StoreError::Database( - e.to_string(), - )), + Err(e) => Err(wacore::store::error::StoreError::Database(Box::new(e))), + } + } + + /// Get the maximum pre-key ID currently stored, or 0 if none exist. + /// Added in wacore 0.6: used for migrating `next_pre_key_id` counter when + /// initializing fresh devices that share storage with the legacy schema. + async fn get_max_prekey_id(&self) -> wacore::store::error::Result { + let conn = self.conn.lock(); + let result = conn.query_row( + "SELECT MAX(id) FROM prekeys WHERE device_id = ?1", + params![self.device_id], + |row| row.get::<_, Option>(0), + ); + + match result { + // MAX returns NULL on empty table → Some(None); on non-empty → Some(Some(n)) + Ok(Some(id)) => Ok(u32::try_from(id).unwrap_or(0)), + Ok(None) => Ok(0), + Err(rusqlite::Error::QueryReturnedNoRows) => Ok(0), + Err(e) => Err(wacore::store::error::StoreError::Database(Box::new(e))), } } - async fn remove_prekey(&self, id: u32) -> wa_rs_core::store::error::Result<()> { + async fn remove_prekey(&self, id: u32) -> wacore::store::error::Result<()> { let conn = self.conn.lock(); to_store_err!(execute: conn.execute( "DELETE FROM prekeys WHERE id = ?1 AND device_id = ?2", @@ -397,7 +549,7 @@ impl SignalStore for RusqliteStore { &self, id: u32, record: &[u8], - ) -> wa_rs_core::store::error::Result<()> { + ) -> wacore::store::error::Result<()> { let conn = self.conn.lock(); to_store_err!(execute: conn.execute( "INSERT OR REPLACE INTO signed_prekeys (id, record, device_id) @@ -406,10 +558,7 @@ impl SignalStore for RusqliteStore { )) } - async fn load_signed_prekey( - &self, - id: u32, - ) -> wa_rs_core::store::error::Result>> { + async fn load_signed_prekey(&self, id: u32) -> wacore::store::error::Result>> { let conn = self.conn.lock(); let result = conn.query_row( "SELECT record FROM signed_prekeys WHERE id = ?1 AND device_id = ?2", @@ -420,15 +569,11 @@ impl SignalStore for RusqliteStore { match result { Ok(record) => Ok(Some(record)), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), - Err(e) => Err(wa_rs_core::store::error::StoreError::Database( - e.to_string(), - )), + Err(e) => Err(wacore::store::error::StoreError::Database(Box::new(e))), } } - async fn load_all_signed_prekeys( - &self, - ) -> wa_rs_core::store::error::Result)>> { + async fn load_all_signed_prekeys(&self) -> wacore::store::error::Result)>> { let conn = self.conn.lock(); let mut stmt = to_store_err!( conn.prepare("SELECT id, record FROM signed_prekeys WHERE device_id = ?1") @@ -446,7 +591,7 @@ impl SignalStore for RusqliteStore { Ok(result) } - async fn remove_signed_prekey(&self, id: u32) -> wa_rs_core::store::error::Result<()> { + async fn remove_signed_prekey(&self, id: u32) -> wacore::store::error::Result<()> { let conn = self.conn.lock(); to_store_err!(execute: conn.execute( "DELETE FROM signed_prekeys WHERE id = ?1 AND device_id = ?2", @@ -460,7 +605,7 @@ impl SignalStore for RusqliteStore { &self, address: &str, record: &[u8], - ) -> wa_rs_core::store::error::Result<()> { + ) -> wacore::store::error::Result<()> { let conn = self.conn.lock(); to_store_err!(execute: conn.execute( "INSERT OR REPLACE INTO sender_keys (address, record, device_id) @@ -469,10 +614,7 @@ impl SignalStore for RusqliteStore { )) } - async fn get_sender_key( - &self, - address: &str, - ) -> wa_rs_core::store::error::Result>> { + async fn get_sender_key(&self, address: &str) -> wacore::store::error::Result>> { let conn = self.conn.lock(); let result = conn.query_row( "SELECT record FROM sender_keys WHERE address = ?1 AND device_id = ?2", @@ -483,13 +625,11 @@ impl SignalStore for RusqliteStore { match result { Ok(record) => Ok(Some(record)), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), - Err(e) => Err(wa_rs_core::store::error::StoreError::Database( - e.to_string(), - )), + Err(e) => Err(wacore::store::error::StoreError::Database(Box::new(e))), } } - async fn delete_sender_key(&self, address: &str) -> wa_rs_core::store::error::Result<()> { + async fn delete_sender_key(&self, address: &str) -> wacore::store::error::Result<()> { let conn = self.conn.lock(); to_store_err!(execute: conn.execute( "DELETE FROM sender_keys WHERE address = ?1 AND device_id = ?2", @@ -504,7 +644,7 @@ impl AppSyncStore for RusqliteStore { async fn get_sync_key( &self, key_id: &[u8], - ) -> wa_rs_core::store::error::Result> { + ) -> wacore::store::error::Result> { let conn = self.conn.lock(); let result = conn.query_row( "SELECT key_data FROM app_state_keys WHERE key_id = ?1 AND device_id = ?2", @@ -519,9 +659,7 @@ impl AppSyncStore for RusqliteStore { match result { Ok(key) => Ok(Some(key)), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), - Err(e) => Err(wa_rs_core::store::error::StoreError::Database( - e.to_string(), - )), + Err(e) => Err(wacore::store::error::StoreError::Database(Box::new(e))), } } @@ -529,7 +667,7 @@ impl AppSyncStore for RusqliteStore { &self, key_id: &[u8], key: AppStateSyncKey, - ) -> wa_rs_core::store::error::Result<()> { + ) -> wacore::store::error::Result<()> { let conn = self.conn.lock(); let key_data = to_store_err!(serde_json::to_vec(&key))?; @@ -540,7 +678,7 @@ impl AppSyncStore for RusqliteStore { )) } - async fn get_version(&self, name: &str) -> wa_rs_core::store::error::Result { + async fn get_version(&self, name: &str) -> wacore::store::error::Result { let conn = self.conn.lock(); let state_data: Vec = to_store_err!(conn.query_row( "SELECT state_data FROM app_state_versions WHERE name = ?1 AND device_id = ?2", @@ -551,11 +689,7 @@ impl AppSyncStore for RusqliteStore { to_store_err!(serde_json::from_slice(&state_data)) } - async fn set_version( - &self, - name: &str, - state: HashState, - ) -> wa_rs_core::store::error::Result<()> { + async fn set_version(&self, name: &str, state: HashState) -> wacore::store::error::Result<()> { let conn = self.conn.lock(); let state_data = to_store_err!(serde_json::to_vec(&state))?; @@ -571,7 +705,7 @@ impl AppSyncStore for RusqliteStore { name: &str, version: u64, mutations: &[AppStateMutationMAC], - ) -> wa_rs_core::store::error::Result<()> { + ) -> wacore::store::error::Result<()> { let conn = self.conn.lock(); for mutation in mutations { @@ -593,7 +727,7 @@ impl AppSyncStore for RusqliteStore { &self, name: &str, index_mac: &[u8], - ) -> wa_rs_core::store::error::Result>> { + ) -> wacore::store::error::Result>> { let conn = self.conn.lock(); let index_mac_json = to_store_err!(serde_json::to_vec(index_mac))?; @@ -607,9 +741,7 @@ impl AppSyncStore for RusqliteStore { match result { Ok(mac) => Ok(Some(mac)), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), - Err(e) => Err(wa_rs_core::store::error::StoreError::Database( - e.to_string(), - )), + Err(e) => Err(wacore::store::error::StoreError::Database(Box::new(e))), } } @@ -617,7 +749,7 @@ impl AppSyncStore for RusqliteStore { &self, name: &str, index_macs: &[Vec], - ) -> wa_rs_core::store::error::Result<()> { + ) -> wacore::store::error::Result<()> { let conn = self.conn.lock(); for index_mac in index_macs { @@ -632,70 +764,142 @@ impl AppSyncStore for RusqliteStore { Ok(()) } + + /// Get the most recently stored app state sync key ID. + /// Added in wacore 0.6: used to seed app-state sync requests with the + /// freshest key identifier we hold rather than scanning the table on each + /// request. + async fn get_latest_sync_key_id(&self) -> wacore::store::error::Result>> { + let conn = self.conn.lock(); + let result = conn.query_row( + "SELECT key_id FROM app_state_keys + WHERE device_id = ?1 + ORDER BY key_id DESC + LIMIT 1", + params![self.device_id], + |row| row.get::<_, Vec>(0), + ); + + match result { + Ok(key_id) => Ok(Some(key_id)), + Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), + Err(e) => Err(wacore::store::error::StoreError::Database(Box::new(e))), + } + } } #[cfg(feature = "whatsapp-web")] #[async_trait] impl ProtocolStore for RusqliteStore { - // --- SKDM Tracking --- - - async fn get_skdm_recipients( + // --- Per-Device Sender Key Tracking --- + // + // Replaces the wacore 0.2 SKDM-recipients model with WA Web's + // `participant.senderKey` map. Tracks per-device `(has_key)` status: + // `true` = SKDM already distributed, `false` = needs fresh SKDM. + // The legacy `skdm_recipients` table is kept around (no migration drops it) + // but is no longer read or written. + + async fn get_sender_key_devices( &self, group_jid: &str, - ) -> wa_rs_core::store::error::Result> { + ) -> wacore::store::error::Result> { let conn = self.conn.lock(); let mut stmt = to_store_err!(conn.prepare( - "SELECT device_jid FROM skdm_recipients WHERE group_jid = ?1 AND device_id = ?2" + "SELECT device_jid, has_key FROM sender_key_devices + WHERE group_jid = ?1 AND device_id = ?2" ))?; let rows = to_store_err!(stmt.query_map(params![group_jid, self.device_id], |row| { - row.get::<_, String>(0) + Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?)) }))?; let mut result = Vec::new(); for row in rows { - let jid_str = to_store_err!(row)?; - if let Ok(jid) = jid_str.parse() { - result.push(jid); - } + let (device_jid, has_key) = to_store_err!(row)?; + result.push((device_jid, has_key != 0)); } Ok(result) } - async fn add_skdm_recipients( + async fn set_sender_key_status( &self, group_jid: &str, - device_jids: &[Jid], - ) -> wa_rs_core::store::error::Result<()> { - let conn = self.conn.lock(); + entries: &[(&str, bool)], + ) -> wacore::store::error::Result<()> { + if entries.is_empty() { + return Ok(()); + } + let mut conn = self.conn.lock(); let now = chrono::Utc::now().timestamp(); - for device_jid in device_jids { - to_store_err!(execute: conn.execute( - "INSERT OR IGNORE INTO skdm_recipients (group_jid, device_jid, device_id, created_at) - VALUES (?1, ?2, ?3, ?4)", - params![group_jid, device_jid.to_string(), self.device_id, now], + // Wrap the per-entry upserts in a transaction so a panic or connection + // drop mid-batch can't leave some (group, device) pairs flipped and + // others not — partial state would silently break SKDM resend logic. + let tx = to_store_err!(conn.transaction())?; + + for (device_jid, has_key) in entries { + to_store_err!(execute: tx.execute( + "INSERT INTO sender_key_devices + (group_jid, device_jid, has_key, device_id, updated_at) + VALUES (?1, ?2, ?3, ?4, ?5) + ON CONFLICT(group_jid, device_jid, device_id) DO UPDATE SET + has_key = excluded.has_key, + updated_at = excluded.updated_at", + params![ + group_jid, + device_jid, + if *has_key { 1_i64 } else { 0_i64 }, + self.device_id, + now, + ], ))?; } + to_store_err!(tx.commit())?; Ok(()) } - async fn clear_skdm_recipients(&self, group_jid: &str) -> wa_rs_core::store::error::Result<()> { + async fn clear_sender_key_devices(&self, group_jid: &str) -> wacore::store::error::Result<()> { let conn = self.conn.lock(); to_store_err!(execute: conn.execute( - "DELETE FROM skdm_recipients WHERE group_jid = ?1 AND device_id = ?2", + "DELETE FROM sender_key_devices WHERE group_jid = ?1 AND device_id = ?2", params![group_jid, self.device_id], )) } + async fn delete_sender_key_device_rows( + &self, + device_jids: &[&str], + ) -> wacore::store::error::Result<()> { + if device_jids.is_empty() { + return Ok(()); + } + let conn = self.conn.lock(); + for device_jid in device_jids { + to_store_err!(execute: conn.execute( + "DELETE FROM sender_key_devices + WHERE device_jid = ?1 AND device_id = ?2", + params![device_jid, self.device_id], + ))?; + } + Ok(()) + } + + async fn clear_all_sender_key_devices(&self) -> wacore::store::error::Result<()> { + let conn = self.conn.lock(); + to_store_err!(execute: conn.execute( + "DELETE FROM sender_key_devices WHERE device_id = ?1", + params![self.device_id], + )) + } + // --- LID-PN Mapping --- async fn get_lid_mapping( &self, lid: &str, - ) -> wa_rs_core::store::error::Result> { + ) -> wacore::store::error::Result> { let conn = self.conn.lock(); let result = conn.query_row( "SELECT lid, phone_number, created_at, learning_source, updated_at @@ -715,16 +919,14 @@ impl ProtocolStore for RusqliteStore { match result { Ok(entry) => Ok(Some(entry)), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), - Err(e) => Err(wa_rs_core::store::error::StoreError::Database( - e.to_string(), - )), + Err(e) => Err(wacore::store::error::StoreError::Database(Box::new(e))), } } async fn get_pn_mapping( &self, phone: &str, - ) -> wa_rs_core::store::error::Result> { + ) -> wacore::store::error::Result> { let conn = self.conn.lock(); let result = conn.query_row( "SELECT lid, phone_number, created_at, learning_source, updated_at @@ -745,16 +947,11 @@ impl ProtocolStore for RusqliteStore { match result { Ok(entry) => Ok(Some(entry)), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), - Err(e) => Err(wa_rs_core::store::error::StoreError::Database( - e.to_string(), - )), + Err(e) => Err(wacore::store::error::StoreError::Database(Box::new(e))), } } - async fn put_lid_mapping( - &self, - entry: &LidPnMappingEntry, - ) -> wa_rs_core::store::error::Result<()> { + async fn put_lid_mapping(&self, entry: &LidPnMappingEntry) -> wacore::store::error::Result<()> { let conn = self.conn.lock(); to_store_err!(execute: conn.execute( "INSERT OR REPLACE INTO lid_pn_mapping @@ -771,9 +968,7 @@ impl ProtocolStore for RusqliteStore { )) } - async fn get_all_lid_mappings( - &self, - ) -> wa_rs_core::store::error::Result> { + async fn get_all_lid_mappings(&self) -> wacore::store::error::Result> { let conn = self.conn.lock(); let mut stmt = to_store_err!(conn.prepare( "SELECT lid, phone_number, created_at, learning_source, updated_at @@ -805,7 +1000,7 @@ impl ProtocolStore for RusqliteStore { address: &str, message_id: &str, base_key: &[u8], - ) -> wa_rs_core::store::error::Result<()> { + ) -> wacore::store::error::Result<()> { let conn = self.conn.lock(); let now = chrono::Utc::now().timestamp(); @@ -821,7 +1016,7 @@ impl ProtocolStore for RusqliteStore { address: &str, message_id: &str, current_base_key: &[u8], - ) -> wa_rs_core::store::error::Result { + ) -> wacore::store::error::Result { let conn = self.conn.lock(); let result = conn.query_row( "SELECT base_key FROM base_keys @@ -836,9 +1031,7 @@ impl ProtocolStore for RusqliteStore { match result { Ok(same) => Ok(same), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(false), - Err(e) => Err(wa_rs_core::store::error::StoreError::Database( - e.to_string(), - )), + Err(e) => Err(wacore::store::error::StoreError::Database(Box::new(e))), } } @@ -846,7 +1039,7 @@ impl ProtocolStore for RusqliteStore { &self, address: &str, message_id: &str, - ) -> wa_rs_core::store::error::Result<()> { + ) -> wacore::store::error::Result<()> { let conn = self.conn.lock(); to_store_err!(execute: conn.execute( "DELETE FROM base_keys WHERE address = ?1 AND message_id = ?2 AND device_id = ?3", @@ -859,20 +1052,25 @@ impl ProtocolStore for RusqliteStore { async fn update_device_list( &self, record: DeviceListRecord, - ) -> wa_rs_core::store::error::Result<()> { + ) -> wacore::store::error::Result<()> { let conn = self.conn.lock(); let devices_json = to_store_err!(serde_json::to_string(&record.devices))?; let now = chrono::Utc::now().timestamp(); + // raw_id is a wacore 0.6 addition for ADV identity-change detection. + // Stored as nullable INTEGER on the new `raw_id` column added by the + // schema migration; older rows with a NULL value behave as if no + // raw_id was ever recorded for the user (matching upstream behavior). to_store_err!(execute: conn.execute( "INSERT OR REPLACE INTO device_registry - (user_id, devices_json, timestamp, phash, device_id, updated_at) - VALUES (?1, ?2, ?3, ?4, ?5, ?6)", + (user_id, devices_json, timestamp, phash, raw_id, device_id, updated_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", params![ record.user, devices_json, record.timestamp, record.phash, + record.raw_id.map(|r| r as i64), self.device_id, now, ], @@ -882,10 +1080,10 @@ impl ProtocolStore for RusqliteStore { async fn get_devices( &self, user: &str, - ) -> wa_rs_core::store::error::Result> { + ) -> wacore::store::error::Result> { let conn = self.conn.lock(); let result = conn.query_row( - "SELECT user_id, devices_json, timestamp, phash + "SELECT user_id, devices_json, timestamp, phash, raw_id FROM device_registry WHERE user_id = ?1 AND device_id = ?2", params![user, self.device_id], |row| { @@ -899,11 +1097,13 @@ impl ProtocolStore for RusqliteStore { let devices_json: String = row.get(1)?; let devices: Vec = serde_json::from_str(&devices_json).map_err(to_rusqlite_err)?; + let raw_id: Option = row.get(4)?; Ok(DeviceListRecord { user: row.get(0)?, devices, timestamp: row.get(2)?, phash: row.get(3)?, + raw_id: raw_id.map(|r| r as u32), }) }, ); @@ -911,63 +1111,30 @@ impl ProtocolStore for RusqliteStore { match result { Ok(record) => Ok(Some(record)), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), - Err(e) => Err(wa_rs_core::store::error::StoreError::Database( - e.to_string(), - )), + Err(e) => Err(wacore::store::error::StoreError::Database(Box::new(e))), } } - // --- Sender Key Status (Lazy Deletion) --- - - async fn mark_forget_sender_key( - &self, - group_jid: &str, - participant: &str, - ) -> wa_rs_core::store::error::Result<()> { + /// Delete a device list record, forcing a network re-fetch on next query. + /// Added in wacore 0.6. + async fn delete_devices(&self, user: &str) -> wacore::store::error::Result<()> { let conn = self.conn.lock(); - let now = chrono::Utc::now().timestamp(); - to_store_err!(execute: conn.execute( - "INSERT OR REPLACE INTO sender_key_status (group_jid, participant, device_id, marked_at) - VALUES (?1, ?2, ?3, ?4)", - params![group_jid, participant, self.device_id, now], + "DELETE FROM device_registry WHERE user_id = ?1 AND device_id = ?2", + params![user, self.device_id], )) } - async fn consume_forget_marks( - &self, - group_jid: &str, - ) -> wa_rs_core::store::error::Result> { - let conn = self.conn.lock(); - let mut stmt = to_store_err!(conn.prepare( - "SELECT participant FROM sender_key_status - WHERE group_jid = ?1 AND device_id = ?2" - ))?; - - let rows = to_store_err!(stmt.query_map(params![group_jid, self.device_id], |row| { - row.get::<_, String>(0) - }))?; - - let mut result = Vec::new(); - for row in rows { - result.push(to_store_err!(row)?); - } - - // Delete the marks after consuming them - to_store_err!(execute: conn.execute( - "DELETE FROM sender_key_status WHERE group_jid = ?1 AND device_id = ?2", - params![group_jid, self.device_id], - ))?; - - Ok(result) - } + // NOTE: `mark_forget_sender_key` / `consume_forget_marks` were dropped from + // ProtocolStore in wacore 0.6. The lazy-deletion semantics they implemented + // (a separate "marked for forget" set drained on next send) are now handled + // in-band by the boolean status column on `sender_key_devices` (see + // `set_sender_key_status` above). The old `sender_key_status` table is left + // in place but is no longer read or written. // --- TcToken Storage --- - async fn get_tc_token( - &self, - jid: &str, - ) -> wa_rs_core::store::error::Result> { + async fn get_tc_token(&self, jid: &str) -> wacore::store::error::Result> { let conn = self.conn.lock(); let result = conn.query_row( "SELECT token, token_timestamp, sender_timestamp FROM tc_tokens @@ -985,9 +1152,7 @@ impl ProtocolStore for RusqliteStore { match result { Ok(entry) => Ok(Some(entry)), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), - Err(e) => Err(wa_rs_core::store::error::StoreError::Database( - e.to_string(), - )), + Err(e) => Err(wacore::store::error::StoreError::Database(Box::new(e))), } } @@ -995,7 +1160,7 @@ impl ProtocolStore for RusqliteStore { &self, jid: &str, entry: &TcTokenEntry, - ) -> wa_rs_core::store::error::Result<()> { + ) -> wacore::store::error::Result<()> { let conn = self.conn.lock(); let now = chrono::Utc::now().timestamp(); @@ -1014,7 +1179,7 @@ impl ProtocolStore for RusqliteStore { )) } - async fn delete_tc_token(&self, jid: &str) -> wa_rs_core::store::error::Result<()> { + async fn delete_tc_token(&self, jid: &str) -> wacore::store::error::Result<()> { let conn = self.conn.lock(); to_store_err!(execute: conn.execute( "DELETE FROM tc_tokens WHERE jid = ?1 AND device_id = ?2", @@ -1022,7 +1187,7 @@ impl ProtocolStore for RusqliteStore { )) } - async fn get_all_tc_token_jids(&self) -> wa_rs_core::store::error::Result> { + async fn get_all_tc_token_jids(&self) -> wacore::store::error::Result> { let conn = self.conn.lock(); let mut stmt = to_store_err!(conn.prepare("SELECT jid FROM tc_tokens WHERE device_id = ?1"))?; @@ -1042,29 +1207,120 @@ impl ProtocolStore for RusqliteStore { async fn delete_expired_tc_tokens( &self, cutoff_timestamp: i64, - ) -> wa_rs_core::store::error::Result { + ) -> wacore::store::error::Result { let conn = self.conn.lock(); let deleted = conn .execute( "DELETE FROM tc_tokens WHERE token_timestamp < ?1 AND device_id = ?2", params![cutoff_timestamp, self.device_id], ) - .map_err(|e| wa_rs_core::store::error::StoreError::Database(e.to_string()))?; + .map_err(|e| { + wacore::store::error::StoreError::Database( + Box::new(e) as Box + ) + })?; let deleted = u32::try_from(deleted).map_err(|_| { - wa_rs_core::store::error::StoreError::Database(format!( + wacore::store::error::StoreError::Validation(format!( "Affected row count overflowed u32: {deleted}" )) })?; Ok(deleted) } + + // --- Sent Message Store (retry support) --- + // + // Added in wacore 0.6 to mirror WA Web's `getMessageTable`. Each outbound + // send writes the protobuf-encoded payload here keyed by (chat_jid, + // message_id); retry-receipt handling consumes (atomic SELECT + DELETE) + // the entry so we don't double-retry. Expiry is invoked from a periodic + // cleanup hook ZeroClaw doesn't yet schedule — see TODO in + // `delete_expired_sent_messages`. + + async fn store_sent_message( + &self, + chat_jid: &str, + message_id: &str, + payload: &[u8], + ) -> wacore::store::error::Result<()> { + let conn = self.conn.lock(); + let now = chrono::Utc::now().timestamp(); + to_store_err!(execute: conn.execute( + "INSERT OR REPLACE INTO sent_messages + (chat_jid, message_id, payload, device_id, created_at) + VALUES (?1, ?2, ?3, ?4, ?5)", + params![chat_jid, message_id, payload, self.device_id, now], + )) + } + + async fn take_sent_message( + &self, + chat_jid: &str, + message_id: &str, + ) -> wacore::store::error::Result>> { + let mut conn = self.conn.lock(); + // Atomic SELECT+DELETE under an immediate transaction matches upstream's + // SqliteStore::take_sent_message: prevents two concurrent retry-receipts + // from each consuming and re-encrypting the same payload. + let tx = to_store_err!(conn.transaction())?; + + let payload: Option> = match tx.query_row( + "SELECT payload FROM sent_messages + WHERE chat_jid = ?1 AND message_id = ?2 AND device_id = ?3", + params![chat_jid, message_id, self.device_id], + |row| row.get::<_, Vec>(0), + ) { + Ok(p) => Some(p), + Err(rusqlite::Error::QueryReturnedNoRows) => None, + Err(e) => { + return Err(wacore::store::error::StoreError::Database(Box::new(e))); + } + }; + + if payload.is_some() { + to_store_err!(execute: tx.execute( + "DELETE FROM sent_messages + WHERE chat_jid = ?1 AND message_id = ?2 AND device_id = ?3", + params![chat_jid, message_id, self.device_id], + ))?; + } + + to_store_err!(tx.commit())?; + Ok(payload) + } + + /// Delete sent messages older than `cutoff_timestamp` (unix seconds). + /// TODO(wacore-0.6): wire to a periodic cleanup cron in the daemon. The + /// current implementation is correct but the table will grow unbounded + /// until the cron is hooked up. + async fn delete_expired_sent_messages( + &self, + cutoff_timestamp: i64, + ) -> wacore::store::error::Result { + let conn = self.conn.lock(); + let deleted = conn + .execute( + "DELETE FROM sent_messages WHERE created_at < ?1 AND device_id = ?2", + params![cutoff_timestamp, self.device_id], + ) + .map_err(|e| { + wacore::store::error::StoreError::Database( + Box::new(e) as Box + ) + })?; + u32::try_from(deleted).map_err(|_| { + wacore::store::error::StoreError::Validation(format!( + "Affected row count overflowed u32: {deleted}" + )) + }) + } } #[cfg(feature = "whatsapp-web")] #[async_trait] impl DeviceStoreTrait for RusqliteStore { - async fn save(&self, device: &CoreDevice) -> wa_rs_core::store::error::Result<()> { + async fn save(&self, device: &CoreDevice) -> wacore::store::error::Result<()> { let conn = self.conn.lock(); // Serialize KeyPairs to bytes @@ -1096,14 +1352,27 @@ impl DeviceStoreTrait for RusqliteStore { // rusqlite errors without logging parameter values. let account = device.account.as_ref().map(|a| a.encode_to_vec()); + let server_cert_chain_blob = device + .server_cert_chain + .as_ref() + .map(serde_json::to_vec) + .transpose() + .map_err(|e| wacore::store::error::StoreError::Serialization(Box::new(e)))?; + to_store_err!(execute: conn.execute( "INSERT OR REPLACE INTO device ( id, lid, pn, registration_id, noise_key, identity_key, signed_pre_key, signed_pre_key_id, signed_pre_key_signature, adv_secret_key, account, push_name, app_version_primary, app_version_secondary, app_version_tertiary, app_version_last_fetched_ms, - edge_routing_info, props_hash - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18)", + edge_routing_info, props_hash, + next_pre_key_id, server_has_prekeys, nct_salt, + server_cert_chain, login_counter + ) VALUES ( + ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, + ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, + ?19, ?20, ?21, ?22, ?23 + )", params![ self.device_id, device.lid.as_ref().map(|j| j.to_string()), @@ -1123,11 +1392,16 @@ impl DeviceStoreTrait for RusqliteStore { device.app_version_last_fetched_ms, device.edge_routing_info.clone(), device.props_hash.clone(), + device.next_pre_key_id, + device.server_has_prekeys as i64, + device.nct_salt.clone(), + server_cert_chain_blob, + device.login_counter, ], )) } - async fn load(&self) -> wa_rs_core::store::error::Result> { + async fn load(&self) -> wacore::store::error::Result> { let conn = self.conn.lock(); let result = conn.query_row( "SELECT * FROM device WHERE id = ?1", @@ -1152,7 +1426,7 @@ impl DeviceStoreTrait for RusqliteStore { return Err(rusqlite::Error::InvalidParameterName("key_pair".into())); } - use wa_rs_core::libsignal::protocol::{KeyPair, PrivateKey, PublicKey}; + use wacore::libsignal::protocol::{KeyPair, PrivateKey, PublicKey}; let noise_key = KeyPair::new( PublicKey::from_djb_public_key_bytes(&noise_key_bytes[32..64]) @@ -1186,13 +1460,22 @@ impl DeviceStoreTrait for RusqliteStore { let account = if let Some(bytes) = account_bytes { Some( - wa_rs_proto::whatsapp::AdvSignedDeviceIdentity::decode(&*bytes) + waproto::whatsapp::AdvSignedDeviceIdentity::decode(&*bytes) .map_err(to_rusqlite_err)?, ) } else { None }; + let server_cert_chain: Option = { + let bytes: Option> = row.get("server_cert_chain")?; + match bytes { + Some(b) => Some(serde_json::from_slice(&b).map_err(to_rusqlite_err)?), + None => None, + } + }; + let server_has_prekeys_int: i64 = row.get("server_has_prekeys")?; + Ok(CoreDevice { lid: lid_str.and_then(|s| s.parse().ok()), pn: pn_str.and_then(|s| s.parse().ok()), @@ -1211,6 +1494,11 @@ impl DeviceStoreTrait for RusqliteStore { app_version_last_fetched_ms: row.get("app_version_last_fetched_ms")?, edge_routing_info: row.get("edge_routing_info")?, props_hash: row.get("props_hash")?, + next_pre_key_id: row.get("next_pre_key_id")?, + server_has_prekeys: server_has_prekeys_int != 0, + nct_salt: row.get("nct_salt")?, + server_cert_chain, + login_counter: row.get("login_counter")?, ..Default::default() }) }, @@ -1219,13 +1507,11 @@ impl DeviceStoreTrait for RusqliteStore { match result { Ok(device) => Ok(Some(device)), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), - Err(e) => Err(wa_rs_core::store::error::StoreError::Database( - e.to_string(), - )), + Err(e) => Err(wacore::store::error::StoreError::Database(Box::new(e))), } } - async fn exists(&self) -> wa_rs_core::store::error::Result { + async fn exists(&self) -> wacore::store::error::Result { let conn = self.conn.lock(); let count: i64 = to_store_err!(conn.query_row( "SELECT COUNT(*) FROM device WHERE id = ?1", @@ -1236,7 +1522,7 @@ impl DeviceStoreTrait for RusqliteStore { Ok(count > 0) } - async fn create(&self) -> wa_rs_core::store::error::Result { + async fn create(&self) -> wacore::store::error::Result { // Device already created in constructor, just return the ID Ok(self.device_id) } @@ -1245,7 +1531,7 @@ impl DeviceStoreTrait for RusqliteStore { &self, name: &str, extra_content: Option<&[u8]>, - ) -> wa_rs_core::store::error::Result<()> { + ) -> wacore::store::error::Result<()> { // Create a snapshot by copying the database file let snapshot_path = format!("{}.snapshot.{}", self.db_path, name); @@ -1265,7 +1551,7 @@ impl DeviceStoreTrait for RusqliteStore { mod tests { use super::*; #[cfg(feature = "whatsapp-web")] - use wa_rs_core::store::traits::{LidPnMappingEntry, ProtocolStore, TcTokenEntry}; + use wacore::store::traits::{LidPnMappingEntry, ProtocolStore, TcTokenEntry}; #[cfg(feature = "whatsapp-web")] #[test] @@ -1348,4 +1634,124 @@ mod tests { .is_some() ); } + + #[cfg(feature = "whatsapp-web")] + #[tokio::test] + async fn device_save_load_round_trips_wacore_06_fields() { + use wacore::store::Device as CoreDevice; + use wacore::store::device::{CachedNoiseCert, CachedServerCertChain}; + use wacore::store::traits::DeviceStore as DeviceStoreTrait; + + let tmp = tempfile::NamedTempFile::new().unwrap(); + let path = tmp.path().to_path_buf(); + + // First boot: populate the 5 wacore-0.6 device fields with + // non-default values and persist. + { + let store = RusqliteStore::new(&path).unwrap(); + let mut device = CoreDevice::new(); + device.next_pre_key_id = 42; + device.server_has_prekeys = true; + device.nct_salt = Some(vec![0xDE, 0xAD, 0xBE, 0xEF]); + device.server_cert_chain = Some(CachedServerCertChain { + intermediate: CachedNoiseCert { + key: [1u8; 32], + not_before: 1_700_000_000, + not_after: 1_800_000_000, + }, + leaf: CachedNoiseCert { + key: [2u8; 32], + not_before: 1_700_000_000, + not_after: 1_800_000_000, + }, + }); + device.login_counter = 7; + DeviceStoreTrait::save(&store, &device).await.unwrap(); + } + + // Second boot: reopen the on-disk database and confirm the + // values survived the restart. + let store = RusqliteStore::new(&path).unwrap(); + let loaded = DeviceStoreTrait::load(&store) + .await + .unwrap() + .expect("device row should exist after save"); + + assert_eq!(loaded.next_pre_key_id, 42); + assert!(loaded.server_has_prekeys); + assert_eq!( + loaded.nct_salt.as_deref(), + Some(&[0xDE, 0xAD, 0xBE, 0xEF][..]) + ); + let cert = loaded + .server_cert_chain + .as_ref() + .expect("server_cert_chain should round-trip"); + assert_eq!(cert.intermediate.key, [1u8; 32]); + assert_eq!(cert.leaf.key, [2u8; 32]); + assert_eq!(cert.intermediate.not_before, 1_700_000_000); + assert_eq!(cert.leaf.not_after, 1_800_000_000); + assert_eq!(loaded.login_counter, 7); + } + + #[cfg(feature = "whatsapp-web")] + #[tokio::test] + async fn pre_06_device_table_gets_new_columns_on_open() { + use wacore::store::Device as CoreDevice; + use wacore::store::traits::DeviceStore as DeviceStoreTrait; + + let tmp = tempfile::NamedTempFile::new().unwrap(); + let path = tmp.path().to_path_buf(); + + // Hand-create a legacy pre-0.6 device table (18 columns, no + // wacore-0.6 fields) to simulate an existing on-disk database + // from a daemon that ran against whatsapp-rust 0.5. + { + let conn = rusqlite::Connection::open(&path).unwrap(); + conn.execute_batch( + "CREATE TABLE device ( + id INTEGER PRIMARY KEY, + lid TEXT, + pn TEXT, + registration_id INTEGER NOT NULL, + noise_key BLOB NOT NULL, + identity_key BLOB NOT NULL, + signed_pre_key BLOB NOT NULL, + signed_pre_key_id INTEGER NOT NULL, + signed_pre_key_signature BLOB NOT NULL, + adv_secret_key BLOB NOT NULL, + account BLOB, + push_name TEXT NOT NULL, + app_version_primary INTEGER NOT NULL, + app_version_secondary INTEGER NOT NULL, + app_version_tertiary INTEGER NOT NULL, + app_version_last_fetched_ms INTEGER NOT NULL, + edge_routing_info BLOB, + props_hash TEXT + );", + ) + .unwrap(); + } + + // Opening the store must add the 5 wacore-0.6 columns idempotently; + // a subsequent save+load round-trip must succeed. + let store = RusqliteStore::new(&path).unwrap(); + let mut device = CoreDevice::new(); + device.next_pre_key_id = 99; + device.login_counter = 3; + DeviceStoreTrait::save(&store, &device).await.unwrap(); + + let loaded = DeviceStoreTrait::load(&store) + .await + .unwrap() + .expect("device row should exist after save"); + assert_eq!(loaded.next_pre_key_id, 99); + assert_eq!(loaded.login_counter, 3); + + // Re-opening a second time must be a no-op (idempotent ALTER). + drop(store); + let store2 = RusqliteStore::new(&path).unwrap(); + let loaded2 = DeviceStoreTrait::load(&store2).await.unwrap().unwrap(); + assert_eq!(loaded2.next_pre_key_id, 99); + } } diff --git a/crates/zeroclaw-channels/src/whatsapp_web.rs b/crates/zeroclaw-channels/src/whatsapp_web.rs index 7d8e5db9c8a..7432e407b22 100644 --- a/crates/zeroclaw-channels/src/whatsapp_web.rs +++ b/crates/zeroclaw-channels/src/whatsapp_web.rs @@ -34,7 +34,7 @@ use async_trait::async_trait; use parking_lot::Mutex; use std::sync::Arc; use tokio::select; -use wa_rs_proto::whatsapp::device_props::PlatformType; +use waproto::whatsapp::device_props::PlatformType; use zeroclaw_api::channel::{Channel, ChannelMessage, SendMessage}; #[cfg(not(feature = "whatsapp-web"))] use zeroclaw_runtime::i18n; @@ -83,10 +83,12 @@ pub struct WhatsAppWebChannel { group_policy: zeroclaw_config::schema::WhatsAppChatPolicy, /// Whether to always respond in self-chat when mode = personal self_chat_mode: bool, - /// Bot handle for shutdown - bot_handle: Arc>>>, + /// Bot handle for shutdown. + /// whatsapp-rust 0.6: `Bot::run()` now returns `BotHandle` (a Future + abort) + /// rather than a tokio JoinHandle directly (oxidezap/whatsapp-rust BotHandle wrapper). + bot_handle: Arc>>, /// Client handle for sending messages and typing indicators - client: Arc>>>, + client: Arc>>>, /// Message sender channel tx: Arc>>>, /// Voice transcription (STT) config @@ -310,7 +312,7 @@ impl WhatsAppWebChannel { /// processed normally. #[cfg(feature = "whatsapp-web")] fn lid_rejection_diagnostic( - sender: &wa_rs_binary::jid::Jid, + sender: &wacore_binary::jid::Jid, mapped_phone: Option<&str>, ) -> String { if !sender.is_lid() { @@ -332,8 +334,8 @@ impl WhatsAppWebChannel { /// Build normalized sender candidates from sender JID, optional alt JID, and optional LID->PN mapping. #[cfg(feature = "whatsapp-web")] fn sender_phone_candidates( - sender: &wa_rs_binary::jid::Jid, - sender_alt: Option<&wa_rs_binary::jid::Jid>, + sender: &wacore_binary::jid::Jid, + sender_alt: Option<&wacore_binary::jid::Jid>, mapped_phone: Option<&str>, ) -> Vec { let mut candidates = Vec::new(); @@ -410,14 +412,14 @@ impl WhatsAppWebChannel { /// - Full JIDs (e.g. "12345@s.whatsapp.net") /// - E.164-like numbers (e.g. "+1234567890") #[cfg(feature = "whatsapp-web")] - fn recipient_to_jid(&self, recipient: &str) -> Result { + fn recipient_to_jid(&self, recipient: &str) -> Result { let trimmed = recipient.trim(); if trimmed.is_empty() { anyhow::bail!("Recipient cannot be empty"); } if trimmed.contains('@') { - return trimmed.parse::().map_err(|e| { + return trimmed.parse::().map_err(|e| { ::zeroclaw_log::record!( WARN, ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject) @@ -437,7 +439,7 @@ impl WhatsAppWebChannel { anyhow::bail!("Recipient `{trimmed}` does not contain a valid phone number"); } - Ok(wa_rs_binary::jid::Jid::pn(digits)) + Ok(wacore_binary::jid::Jid::pn(digits)) } // ── Reconnect state-machine helpers (used by listen() and tested directly) ── @@ -488,8 +490,8 @@ impl WhatsAppWebChannel { /// transcription fails (all logged as warnings). #[cfg(feature = "whatsapp-web")] async fn try_transcribe_voice_note( - client: &wa_rs::Client, - audio: &wa_rs_proto::whatsapp::message::AudioMessage, + client: &whatsapp_rust::Client, + audio: &waproto::whatsapp::message::AudioMessage, transcription_config: Option<&zeroclaw_config::schema::TranscriptionConfig>, transcription_manager: Option<&super::transcription::TranscriptionManager>, ) -> Option { @@ -512,7 +514,7 @@ impl WhatsAppWebChannel { } // Download the encrypted audio - use wa_rs::download::Downloadable; + use whatsapp_rust::download::Downloadable; let audio_data = match client.download(audio as &dyn Downloadable).await { Ok(data) => data, Err(e) => { @@ -579,8 +581,8 @@ impl WhatsAppWebChannel { /// Synthesize text to speech and send as a WhatsApp voice note (static version for spawned tasks). #[cfg(feature = "whatsapp-web")] async fn synthesize_voice_static( - client: &wa_rs::Client, - to: &wa_rs_binary::jid::Jid, + client: &whatsapp_rust::Client, + to: &wacore_binary::jid::Jid, text: &str, tts_manager: &super::tts::TtsManager, ) -> Result<()> { @@ -596,9 +598,10 @@ impl WhatsAppWebChannel { anyhow::bail!("TTS returned empty audio"); } - use wa_rs_core::download::MediaType; + use wacore::download::MediaType; + use whatsapp_rust::upload::UploadOptions; let upload = client - .upload(audio_bytes, MediaType::Audio) + .upload(audio_bytes, MediaType::Audio, UploadOptions::default()) .await .map_err(|e| { ::zeroclaw_log::record!( @@ -625,13 +628,20 @@ impl WhatsAppWebChannel { #[allow(clippy::cast_possible_truncation)] let estimated_seconds = std::cmp::max(1, (upload.file_length / 4000) as u32); - let voice_msg = wa_rs_proto::whatsapp::Message { - audio_message: Some(Box::new(wa_rs_proto::whatsapp::message::AudioMessage { + // whatsapp-rust 0.6: UploadResponse cryptographic fields became + // `[u8; 32]` for type safety. Pull the Vec copies before + // consuming the strings so the partial-move on `upload.direct_path` + // doesn't bite. + let media_key = upload.media_key_vec(); + let file_enc_sha256 = upload.file_enc_sha256_vec(); + let file_sha256 = upload.file_sha256_vec(); + let voice_msg = waproto::whatsapp::Message { + audio_message: Some(Box::new(waproto::whatsapp::message::AudioMessage { url: Some(upload.url), direct_path: Some(upload.direct_path), - media_key: Some(upload.media_key), - file_enc_sha256: Some(upload.file_enc_sha256), - file_sha256: Some(upload.file_sha256), + media_key: Some(media_key), + file_enc_sha256: Some(file_enc_sha256), + file_sha256: Some(file_sha256), file_length: Some(upload.file_length), mimetype: Some("audio/ogg; codecs=opus".to_string()), ptt: Some(true), @@ -682,8 +692,8 @@ impl WhatsAppWebChannel { /// document) carry mentions in their own `context_info`, but `text_content()` already /// ignores captions so those messages are filtered out upstream as empty text. #[cfg(feature = "whatsapp-web")] - fn extract_mentioned_jids(msg: &wa_rs_proto::whatsapp::Message) -> Vec { - use wa_rs_core::proto_helpers::MessageExt; + fn extract_mentioned_jids(msg: &waproto::whatsapp::Message) -> Vec { + use wacore::proto_helpers::MessageExt; let base = msg.get_base_message(); if let Some(ref ext) = base.extended_text_message @@ -901,16 +911,23 @@ impl Channel for WhatsAppWebChannel { } // Send text message - let outgoing = wa_rs_proto::whatsapp::Message { + let outgoing = waproto::whatsapp::Message { conversation: Some(message.content.clone()), ..Default::default() }; - let message_id = client.send_message(to, outgoing).await?; + // Box::pin the large future (~34KB) so it doesn't inflate the + // enclosing Send future's stack slot — clippy::large_futures. + // whatsapp-rust 0.6: send_message returns `SendResult { message_id, to }` + // instead of a bare `String` (oxidezap/whatsapp-rust#597). + let send_result = Box::pin(client.send_message(to, outgoing)).await?; ::zeroclaw_log::record!( DEBUG, ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note), - &format!("sent text to {} (id: {})", message.recipient, message_id) + &format!( + "sent text to {} (id: {})", + message.recipient, send_result.message_id + ) ); Ok(()) } @@ -924,14 +941,16 @@ impl Channel for WhatsAppWebChannel { // borrowing `self` for its 'static lifetime. let alias = std::sync::Arc::new(self.alias.clone()); - use wa_rs::bot::Bot; - use wa_rs::pair_code::PairCodeOptions; - use wa_rs::store::{Device, DeviceStore}; - use wa_rs_binary::jid::JidExt as _; - use wa_rs_core::proto_helpers::MessageExt; - use wa_rs_core::types::events::Event; - use wa_rs_tokio_transport::TokioWebSocketTransportFactory; - use wa_rs_ureq_http::UreqHttpClient; + use wacore::proto_helpers::MessageExt; + use wacore::store::DevicePropsOverride; + use wacore::types::events::Event; + use wacore_binary::jid::JidExt as _; + use whatsapp_rust::TokioRuntime; + use whatsapp_rust::bot::Bot; + use whatsapp_rust::pair_code::PairCodeOptions; + use whatsapp_rust::store::{Device, DeviceStore}; + use whatsapp_rust_tokio_transport::TokioWebSocketTransportFactory; + use whatsapp_rust_ureq_http_client::UreqHttpClient; let retry_count = Arc::new(std::sync::atomic::AtomicU32::new(0)); @@ -1003,14 +1022,21 @@ impl Channel for WhatsAppWebChannel { let wa_dm_mention_patterns = self.dm_mention_patterns.clone(); let wa_group_mention_patterns = self.group_mention_patterns.clone(); + // whatsapp-rust 0.6: BotBuilder gained a 4th typestate slot for the + // async runtime (oxidezap/whatsapp-rust#621). `with_runtime` is + // required before `.build()` resolves; we use the bundled + // `TokioRuntime`. `with_device_props` switched from three + // positional Options to a `DevicePropsOverride` builder + // (oxidezap/whatsapp-rust#586). let mut builder = Bot::builder() .with_backend(backend) .with_transport_factory(transport_factory) .with_http_client(http_client) + .with_runtime(TokioRuntime) .with_device_props( - Some("ZeroClaw".to_string()), - None, - Some(PlatformType::Desktop), + DevicePropsOverride::new() + .with_os("ZeroClaw") + .with_platform_type(PlatformType::Desktop), ) .on_event({ let alias = Arc::clone(&alias); @@ -1031,7 +1057,10 @@ impl Channel for WhatsAppWebChannel { let wa_dm_mention_patterns = wa_dm_mention_patterns.clone(); let wa_group_mention_patterns = wa_group_mention_patterns.clone(); async move { - match event { + // whatsapp-rust 0.6: event handlers receive `Arc` + // per PR #613, so we match against `&*event` to get a + // `&Event` reference and bind variant fields by ref. + match &*event { Event::Message(msg, info) => { let sender_jid = info.source.sender.clone(); let sender_alt = info.source.sender_alt.clone(); @@ -1039,8 +1068,18 @@ impl Channel for WhatsAppWebChannel { let _is_group = info.source.chat.is_group(); let chat = info.source.chat.to_string(); + // whatsapp-rust 0.6: `Client::get_phone_number_from_lid` + // was replaced by the unified `get_lid_pn_entry` + // (oxidezap/whatsapp-rust#487). The new helper + // returns the full LID↔phone entry; we extract + // the phone field on hit, swallow lookup errors + // back to `None` (consistent with the legacy + // semantics — best-effort enrichment). let mapped_phone = if sender_jid.is_lid() { - client.get_phone_number_from_lid(&sender_jid.user).await + match client.get_lid_pn_entry(&sender_jid).await { + Ok(Some(entry)) => Some(entry.phone_number), + _ => None, + } } else { None }; @@ -1215,7 +1254,7 @@ impl Channel for WhatsAppWebChannel { let bot_phone = bot_phone_inner.lock(); if let Some(ref bp) = *bot_phone { let mentioned_jids = - Self::extract_mentioned_jids(&msg); + Self::extract_mentioned_jids(msg); if !Self::contains_bot_mention( &content, &mentioned_jids, @@ -1310,7 +1349,7 @@ impl Channel for WhatsAppWebChannel { } Event::PairingQrCode { code, .. } => { ::zeroclaw_log::record!(INFO, ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note), "WhatsApp Web QR code received (scan with WhatsApp > Linked Devices)"); - match Self::render_pairing_qr(&code) { + match Self::render_pairing_qr(code) { Ok(rendered) => { eprintln!(); eprintln!( @@ -1626,7 +1665,7 @@ impl Channel for WhatsAppWebChannel { mod tests { use super::*; #[cfg(feature = "whatsapp-web")] - use wa_rs_binary::jid::Jid; + use wacore_binary::jid::Jid; #[test] #[cfg(feature = "whatsapp-web")]