mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(whatsapp): upgrade WhatsApp Web channel to upstream whatsapp-rust 0.5 (#916)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
238a9a5ad9
commit
e3c46d1c3d
Generated
+403
-216
@@ -69,7 +69,7 @@ dependencies = [
|
||||
"aes",
|
||||
"cipher",
|
||||
"ctr",
|
||||
"ghash",
|
||||
"ghash 0.5.1",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
@@ -502,6 +502,26 @@ version = "1.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740"
|
||||
dependencies = [
|
||||
"bincode_derive",
|
||||
"serde",
|
||||
"unty",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bincode_derive"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09"
|
||||
dependencies = [
|
||||
"virtue",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.72.1"
|
||||
@@ -1156,6 +1176,12 @@ dependencies = [
|
||||
"windows 0.54.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpubits"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ef0c543070d296ea414df2dd7625d1b24866ce206709d8a4a424f28377f5861"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
@@ -1194,24 +1220,6 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
@@ -1291,17 +1299,38 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dashmap"
|
||||
version = "6.1.0"
|
||||
name = "darling"
|
||||
version = "0.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
|
||||
checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crossbeam-utils",
|
||||
"hashbrown 0.14.5",
|
||||
"lock_api",
|
||||
"once_cell",
|
||||
"parking_lot_core",
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1482,6 +1511,53 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diesel"
|
||||
version = "2.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78df0e4e8c596662edb07fbfbb7f23769cca35049827df5f909084d956b6aeaf"
|
||||
dependencies = [
|
||||
"diesel_derives",
|
||||
"downcast-rs",
|
||||
"libsqlite3-sys",
|
||||
"r2d2",
|
||||
"sqlite-wasm-rs",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diesel_derives"
|
||||
version = "2.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b79402bd1cfb25b65650f0f4901d0e79c095729e2139c8ab779d025968c7099"
|
||||
dependencies = [
|
||||
"diesel_table_macro_syntax",
|
||||
"dsl_auto_type",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diesel_migrations"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28d0f4a98124ba6d4ca75da535f65984badec16a003b6e2f94a01e31a79490b8"
|
||||
dependencies = [
|
||||
"diesel",
|
||||
"migrations_internals",
|
||||
"migrations_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diesel_table_macro_syntax"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe2444076b48641147115697648dc743c2c00b61adade0f01ce67133c7babe8c"
|
||||
dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
@@ -1592,6 +1668,26 @@ version = "0.15.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
||||
|
||||
[[package]]
|
||||
name = "downcast-rs"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc"
|
||||
|
||||
[[package]]
|
||||
name = "dsl_auto_type"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd122633e4bef06db27737f21d3738fb89c8f6d5360d6d9d7635dda142a7757e"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"either",
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dunce"
|
||||
version = "1.0.5"
|
||||
@@ -1988,6 +2084,12 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
@@ -2230,7 +2332,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
|
||||
dependencies = [
|
||||
"opaque-debug",
|
||||
"polyval",
|
||||
"polyval 0.6.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ghash"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5"
|
||||
dependencies = [
|
||||
"polyval 0.7.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2312,19 +2423,22 @@ dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[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"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"foldhash",
|
||||
"foldhash 0.1.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
dependencies = [
|
||||
"foldhash 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2818,6 +2932,12 @@ version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.1.0"
|
||||
@@ -3801,6 +3921,27 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "migrations_internals"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36c791ecdf977c99f45f23280405d7723727470f6689a5e6dbf513ac547ae10d"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"toml 0.9.12+spec-1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "migrations_macros"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36fc5ac76be324cfd2d3f2cf0fdf5d5d3c4f14ed8aaebadb09e304ba42282703"
|
||||
dependencies = [
|
||||
"migrations_internals",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
@@ -3850,26 +3991,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[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"
|
||||
@@ -4506,14 +4627,12 @@ dependencies = [
|
||||
"url",
|
||||
"urlencoding",
|
||||
"uuid",
|
||||
"wa-rs",
|
||||
"wa-rs-binary",
|
||||
"wa-rs-core",
|
||||
"wa-rs-proto",
|
||||
"wa-rs-tokio-transport",
|
||||
"wa-rs-ureq-http",
|
||||
"wacore",
|
||||
"wait-timeout",
|
||||
"webpki-roots 1.0.6",
|
||||
"whatsapp-rust",
|
||||
"whatsapp-rust-tokio-transport",
|
||||
"whatsapp-rust-ureq-http-client",
|
||||
"whisper-rs",
|
||||
"xz2",
|
||||
"zip",
|
||||
@@ -4915,7 +5034,7 @@ checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
|
||||
dependencies = [
|
||||
"cpufeatures 0.2.17",
|
||||
"opaque-debug",
|
||||
"universal-hash",
|
||||
"universal-hash 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4927,7 +5046,18 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.17",
|
||||
"opaque-debug",
|
||||
"universal-hash",
|
||||
"universal-hash 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "polyval"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dfc63250416fea14f5749b90725916a6c903f599d51cb635aa7a52bfd03eede"
|
||||
dependencies = [
|
||||
"cpubits",
|
||||
"cpufeatures 0.3.0",
|
||||
"universal-hash 0.6.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5166,26 +5296,6 @@ dependencies = [
|
||||
"prost 0.14.3",
|
||||
]
|
||||
|
||||
[[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"
|
||||
@@ -5299,6 +5409,17 @@ version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "r2d2"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93"
|
||||
dependencies = [
|
||||
"log",
|
||||
"parking_lot",
|
||||
"scheduled-thread-pool",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
@@ -5639,6 +5760,16 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rsqlite-vfs"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d"
|
||||
dependencies = [
|
||||
"hashbrown 0.16.1",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruma"
|
||||
version = "0.14.1"
|
||||
@@ -5970,6 +6101,15 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scheduled-thread-pool"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19"
|
||||
dependencies = [
|
||||
"parking_lot",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "1.2.1"
|
||||
@@ -6471,6 +6611,18 @@ dependencies = [
|
||||
"der",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlite-wasm-rs"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b2c760607300407ddeaee518acf28c795661b7108c75421303dbefb237d3a36"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"js-sys",
|
||||
"rsqlite-vfs",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.1"
|
||||
@@ -6592,12 +6744,6 @@ dependencies = [
|
||||
"windows 0.57.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tagptr"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
|
||||
|
||||
[[package]]
|
||||
name = "tar"
|
||||
version = "0.4.45"
|
||||
@@ -7304,6 +7450,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"
|
||||
@@ -7316,6 +7472,12 @@ version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "unty"
|
||||
version = "0.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
|
||||
|
||||
[[package]]
|
||||
name = "ureq"
|
||||
version = "3.3.0"
|
||||
@@ -7420,6 +7582,12 @@ version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "virtue"
|
||||
version = "0.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1"
|
||||
|
||||
[[package]]
|
||||
name = "vodozemac"
|
||||
version = "0.9.0"
|
||||
@@ -7451,40 +7619,53 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wa-rs"
|
||||
version = "0.2.0"
|
||||
name = "wacore"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fecb468bdfe1e7d4c06a1bd12908c66edaca59024862cb64757ad11c3b948b1"
|
||||
checksum = "5292fb723da7505e90cfd7a828dd397802b98e0f819a2bc058a6b3a9f2345306"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
"async-channel 2.5.0",
|
||||
"async-lock",
|
||||
"async-trait",
|
||||
"base64",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"dashmap",
|
||||
"env_logger",
|
||||
"ctr",
|
||||
"event-listener 5.4.1",
|
||||
"flate2",
|
||||
"futures",
|
||||
"hex",
|
||||
"hkdf",
|
||||
"hmac 0.12.1",
|
||||
"log",
|
||||
"moka",
|
||||
"md5",
|
||||
"once_cell",
|
||||
"pbkdf2",
|
||||
"prost 0.14.3",
|
||||
"rand 0.9.4",
|
||||
"rand_core 0.10.1",
|
||||
"scopeguard",
|
||||
"rand 0.10.1",
|
||||
"serde",
|
||||
"serde-big-array",
|
||||
"serde_json",
|
||||
"sha1",
|
||||
"sha2 0.10.9",
|
||||
"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"
|
||||
name = "wacore-appstate"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3845137b3aead2d99de7c6744784bf2f5a908be9dc97a3dbd7585dc40296925c"
|
||||
checksum = "ab29b1c5198e16e2619868cc3e48c32f0bcecec936fd20d09460a8f7e374604d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytemuck",
|
||||
@@ -7497,16 +7678,16 @@ dependencies = [
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
"thiserror 2.0.18",
|
||||
"wa-rs-binary",
|
||||
"wa-rs-libsignal",
|
||||
"wa-rs-proto",
|
||||
"wacore-binary",
|
||||
"wacore-libsignal",
|
||||
"waproto",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wa-rs-binary"
|
||||
version = "0.2.0"
|
||||
name = "wacore-binary"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3b30a6e11aebb39c07392675256ead5e2570c31382bd4835d6ddc877284b6be"
|
||||
checksum = "127a3a6e7554ce002092f1711aa927b0efa3d3fb1ee83506525565c626e68834"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"phf 0.13.1",
|
||||
@@ -7516,51 +7697,10 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wa-rs-core"
|
||||
version = "0.2.0"
|
||||
name = "wacore-derive"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed13bb2aff2de43fc4dd821955f03ea48a1d31eda3c80efe6f905898e304d11f"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
"async-channel 2.5.0",
|
||||
"async-trait",
|
||||
"base64",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"ctr",
|
||||
"flate2",
|
||||
"hex",
|
||||
"hkdf",
|
||||
"hmac 0.12.1",
|
||||
"log",
|
||||
"md5",
|
||||
"once_cell",
|
||||
"pbkdf2",
|
||||
"prost 0.14.3",
|
||||
"protobuf",
|
||||
"rand 0.9.4",
|
||||
"rand_core 0.10.1",
|
||||
"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",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wa-rs-derive"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75c03f610c9bc960e653d5d6d2a4cced9013bedbe5e6e8948787bbd418e4137c"
|
||||
checksum = "3f9546a14730f112eca3bc2e4cbd815df88046fa78530f110c3665492604e770"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -7568,10 +7708,10 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wa-rs-libsignal"
|
||||
version = "0.2.0"
|
||||
name = "wacore-libsignal"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3471be8ff079ae4959fcddf2e7341281e5c6756bdc6a66454ea1a8e474d14576"
|
||||
checksum = "c89676f7e09708d6b3a58e02bfea42000e3d7bffbc1bf67d0c8bf58ddbdf6373"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
@@ -7583,29 +7723,28 @@ dependencies = [
|
||||
"curve25519-dalek",
|
||||
"derive_more 2.1.1",
|
||||
"displaydoc",
|
||||
"ghash",
|
||||
"ghash 0.6.0",
|
||||
"hex",
|
||||
"hkdf",
|
||||
"hmac 0.12.1",
|
||||
"itertools 0.14.0",
|
||||
"log",
|
||||
"prost 0.14.3",
|
||||
"rand 0.9.4",
|
||||
"rand 0.10.1",
|
||||
"serde",
|
||||
"sha1",
|
||||
"sha2 0.10.9",
|
||||
"subtle",
|
||||
"thiserror 2.0.18",
|
||||
"uuid",
|
||||
"wa-rs-proto",
|
||||
"waproto",
|
||||
"x25519-dalek",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wa-rs-noise"
|
||||
version = "0.2.0"
|
||||
name = "wacore-noise"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3efb3891c1e22ce54646dc581e34e79377dc402ed8afb11a7671c5ef629b3ae"
|
||||
checksum = "3e1c5a0ef2ddaf0f7c5a227649c09de5f354571d95feea4dbac55f2b31c2da53"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
@@ -7613,58 +7752,12 @@ dependencies = [
|
||||
"hkdf",
|
||||
"log",
|
||||
"prost 0.14.3",
|
||||
"rand 0.9.4",
|
||||
"rand_core 0.10.1",
|
||||
"rand 0.10.1",
|
||||
"sha2 0.10.9",
|
||||
"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 0.14.3",
|
||||
"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.6",
|
||||
]
|
||||
|
||||
[[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]]
|
||||
@@ -7695,6 +7788,17 @@ dependencies = [
|
||||
"try-lock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "waproto"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8fb5d942027d97e5ca1e542a9915388742cc5efab4802910ecc2fd1f430cefe"
|
||||
dependencies = [
|
||||
"prost 0.14.3",
|
||||
"prost-build",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
@@ -7943,6 +8047,89 @@ version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
|
||||
|
||||
[[package]]
|
||||
name = "whatsapp-rust"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5b79e16847fe6e7ea8d103b4b575b1a1b77bd57cb4c292f7c50616b72a74374"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.5.0",
|
||||
"async-lock",
|
||||
"async-trait",
|
||||
"base64",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"env_logger",
|
||||
"event-listener 5.4.1",
|
||||
"futures",
|
||||
"hex",
|
||||
"log",
|
||||
"prost 0.14.3",
|
||||
"rand 0.10.1",
|
||||
"scopeguard",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"wacore",
|
||||
"wacore-binary",
|
||||
"waproto",
|
||||
"whatsapp-rust-sqlite-storage",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "whatsapp-rust-sqlite-storage"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dce56415e95e6f22caf45ea1aec64c67d69b9d6f636a1b20fcc322dff8a787cd"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bincode",
|
||||
"diesel",
|
||||
"diesel_migrations",
|
||||
"libsqlite3-sys",
|
||||
"log",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"wacore",
|
||||
"wacore-binary",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "whatsapp-rust-tokio-transport"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0aca6e068f01d56f7d360a04aeb16d34a3edb623e7d43e7cd857682a186c0130"
|
||||
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.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "whatsapp-rust-ureq-http-client"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45ae9349f172ab8e50031ef5c429bb56ffdd677fbfeb22ed09fff3ce3b62c135"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"tokio",
|
||||
"ureq",
|
||||
"wacore",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "whisper-rs"
|
||||
version = "0.16.0"
|
||||
|
||||
+10
-7
@@ -117,12 +117,15 @@ matrix-sdk = { version = "0.16", optional = true, default-features = false, feat
|
||||
fantoccini = { version = "0.22.0", optional = true, default-features = false, features = ["rustls-tls"] }
|
||||
serde-big-array = { version = "0.5", optional = true }
|
||||
pdf-extract = { version = "0.10", optional = true }
|
||||
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 }
|
||||
wa-rs-ureq-http = { version = "0.2", optional = true }
|
||||
wa-rs-tokio-transport = { version = "0.2", optional = true, default-features = false }
|
||||
# WhatsApp Web — upstream `whatsapp-rust` 0.5. Replaces the previous `wa-rs`
|
||||
# 0.2 fork: upstream now ships its own SqliteStore (so we no longer need the
|
||||
# 1.3K-line custom RusqliteStore) and dispatches `Event::Message` for
|
||||
# LID-addressed contacts and group sender-key (skmsg) messages — both of
|
||||
# which the 0.2 fork silently dropped after decryption.
|
||||
whatsapp-rust = { version = "0.5", optional = true, default-features = false, features = ["sqlite-storage", "tokio-runtime"] }
|
||||
whatsapp-rust-tokio-transport = { version = "0.5", optional = true, default-features = false }
|
||||
whatsapp-rust-ureq-http-client = { version = "0.5", optional = true }
|
||||
wacore = { version = "0.5", optional = true, default-features = false }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
whisper-rs = { version = "0.16", features = ["metal"] }
|
||||
@@ -142,7 +145,7 @@ browser-native = ["dep:fantoccini"]
|
||||
fantoccini = ["browser-native"]
|
||||
landlock = ["sandbox-landlock"]
|
||||
rag-pdf = ["dep:pdf-extract"]
|
||||
whatsapp-web = ["dep:wa-rs", "dep:wa-rs-core", "dep:wa-rs-binary", "dep:wa-rs-proto", "dep:wa-rs-ureq-http", "dep:wa-rs-tokio-transport", "serde-big-array"]
|
||||
whatsapp-web = ["dep:whatsapp-rust", "dep:whatsapp-rust-tokio-transport", "dep:whatsapp-rust-ureq-http-client", "dep:wacore", "serde-big-array"]
|
||||
|
||||
# Fix whisper-rs-sys CRT mismatch on Windows MSVC (LNK2038).
|
||||
# Upstream cmake build defaults to /MD but Rust uses /MT.
|
||||
|
||||
@@ -33,8 +33,6 @@ pub use providers::telegram;
|
||||
pub use providers::web;
|
||||
pub use providers::whatsapp;
|
||||
#[cfg(feature = "whatsapp-web")]
|
||||
pub use providers::whatsapp_storage;
|
||||
#[cfg(feature = "whatsapp-web")]
|
||||
pub use providers::whatsapp_web;
|
||||
|
||||
pub use cli::CliChannel;
|
||||
|
||||
@@ -18,6 +18,4 @@ pub mod telegram;
|
||||
pub mod web;
|
||||
pub mod whatsapp;
|
||||
#[cfg(feature = "whatsapp-web")]
|
||||
pub mod whatsapp_storage;
|
||||
#[cfg(feature = "whatsapp-web")]
|
||||
pub mod whatsapp_web;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,81 +0,0 @@
|
||||
use super::*;
|
||||
#[cfg(feature = "whatsapp-web")]
|
||||
use wa_rs_core::store::traits::{LidPnMappingEntry, ProtocolStore, TcTokenEntry};
|
||||
|
||||
#[cfg(feature = "whatsapp-web")]
|
||||
#[test]
|
||||
fn rusqlite_store_creates_database() {
|
||||
let tmp = tempfile::NamedTempFile::new().unwrap();
|
||||
let store = RusqliteStore::new(tmp.path()).unwrap();
|
||||
assert_eq!(store.device_id, 1);
|
||||
}
|
||||
|
||||
#[cfg(feature = "whatsapp-web")]
|
||||
#[tokio::test]
|
||||
async fn lid_mapping_round_trip_preserves_learning_source_and_updated_at() {
|
||||
let tmp = tempfile::NamedTempFile::new().unwrap();
|
||||
let store = RusqliteStore::new(tmp.path()).unwrap();
|
||||
let entry = LidPnMappingEntry {
|
||||
lid: "100000012345678".to_string(),
|
||||
phone_number: "15551234567".to_string(),
|
||||
created_at: 1_700_000_000,
|
||||
updated_at: 1_700_000_100,
|
||||
learning_source: "usync".to_string(),
|
||||
};
|
||||
|
||||
ProtocolStore::put_lid_mapping(&store, &entry)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let loaded = ProtocolStore::get_lid_mapping(&store, &entry.lid)
|
||||
.await
|
||||
.unwrap()
|
||||
.expect("expected lid mapping to be present");
|
||||
assert_eq!(loaded.learning_source, entry.learning_source);
|
||||
assert_eq!(loaded.updated_at, entry.updated_at);
|
||||
|
||||
let loaded_by_pn = ProtocolStore::get_pn_mapping(&store, &entry.phone_number)
|
||||
.await
|
||||
.unwrap()
|
||||
.expect("expected pn mapping to be present");
|
||||
assert_eq!(loaded_by_pn.learning_source, entry.learning_source);
|
||||
assert_eq!(loaded_by_pn.updated_at, entry.updated_at);
|
||||
}
|
||||
|
||||
#[cfg(feature = "whatsapp-web")]
|
||||
#[tokio::test]
|
||||
async fn delete_expired_tc_tokens_returns_deleted_row_count() {
|
||||
let tmp = tempfile::NamedTempFile::new().unwrap();
|
||||
let store = RusqliteStore::new(tmp.path()).unwrap();
|
||||
|
||||
let expired = TcTokenEntry {
|
||||
token: vec![1, 2, 3],
|
||||
token_timestamp: 10,
|
||||
sender_timestamp: None,
|
||||
};
|
||||
let fresh = TcTokenEntry {
|
||||
token: vec![4, 5, 6],
|
||||
token_timestamp: 1000,
|
||||
sender_timestamp: Some(1000),
|
||||
};
|
||||
|
||||
ProtocolStore::put_tc_token(&store, "15550000001", &expired)
|
||||
.await
|
||||
.unwrap();
|
||||
ProtocolStore::put_tc_token(&store, "15550000002", &fresh)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let deleted = ProtocolStore::delete_expired_tc_tokens(&store, 100)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(deleted, 1);
|
||||
assert!(ProtocolStore::get_tc_token(&store, "15550000001")
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none());
|
||||
assert!(ProtocolStore::get_tc_token(&store, "15550000002")
|
||||
.await
|
||||
.unwrap()
|
||||
.is_some());
|
||||
}
|
||||
@@ -1,13 +1,19 @@
|
||||
//! WhatsApp Web channel using wa-rs (native Rust implementation)
|
||||
//! WhatsApp Web channel backed by upstream [`whatsapp-rust`] 0.5.
|
||||
//!
|
||||
//! This channel provides direct WhatsApp Web integration with:
|
||||
//! - QR code and pair code linking
|
||||
//! - End-to-end encryption via Signal Protocol
|
||||
//! - Full Baileys parity (groups, media, presence, reactions, editing/deletion)
|
||||
//! # Why the upgrade
|
||||
//!
|
||||
//! The previous implementation used `wa-rs` 0.2 (a fork that pinned to stable
|
||||
//! Rust). That fork silently dropped `Event::Message` for LID-addressed
|
||||
//! contacts and group sender-key (`skmsg`) messages: the protocol layer
|
||||
//! decrypted the payload but never dispatched it to user code, breaking
|
||||
//! agent dispatch for the bulk of modern WhatsApp traffic (LID is the
|
||||
//! current default). Upstream `whatsapp-rust` 0.5 fixed this in PRs #170
|
||||
//! (SKDM tracking) + #181 (LID/PN mapping) + sender-key dispatch, and also
|
||||
//! ships its own [`SqliteStore`] — so the previous custom 1,345-line
|
||||
//! `RusqliteStore` is no longer needed.
|
||||
//!
|
||||
//! # Feature Flag
|
||||
//!
|
||||
//! This channel requires the `whatsapp-web` feature flag:
|
||||
//! ```sh
|
||||
//! cargo build --features whatsapp-web
|
||||
//! ```
|
||||
@@ -17,65 +23,75 @@
|
||||
//! ```toml
|
||||
//! [channels.whatsapp]
|
||||
//! session_path = "~/.openhuman/whatsapp-session.db" # Required for Web mode
|
||||
//! pair_phone = "15551234567" # Optional: for pair code linking
|
||||
//! allowed_numbers = ["+1234567890", "*"] # Same as Cloud API
|
||||
//! pair_phone = "15551234567" # Optional: pair-code linking
|
||||
//! allowed_numbers = ["+1234567890", "*"] # Same shape as Cloud API
|
||||
//! ```
|
||||
//!
|
||||
//! # Runtime Negotiation
|
||||
//! # Runtime negotiation
|
||||
//!
|
||||
//! This channel is automatically selected when `session_path` is set in the config.
|
||||
//! The Cloud API channel is used when `phone_number_id` is set.
|
||||
//! Selected automatically by [`crate::openhuman::channels::runtime::startup`]
|
||||
//! when `session_path` is set. The Cloud API channel ([`super::whatsapp`]) is
|
||||
//! used when `phone_number_id` is set instead.
|
||||
//!
|
||||
//! # Migration note
|
||||
//!
|
||||
//! The on-disk SQLite schema differs between the wa-rs 0.2 fork and the
|
||||
//! upstream 0.5 store. Existing paired sessions will fail to load and will
|
||||
//! prompt for a fresh QR scan on first launch after this upgrade. Pairing
|
||||
//! takes about 30 seconds; the old `whatsapp-session.db` can be deleted by
|
||||
//! the user afterwards.
|
||||
//!
|
||||
//! [`whatsapp-rust`]: https://docs.rs/whatsapp-rust/0.5
|
||||
//! [`SqliteStore`]: whatsapp_rust::store::SqliteStore
|
||||
|
||||
use super::whatsapp_storage::RusqliteStore;
|
||||
use crate::openhuman::channels::traits::{Channel, ChannelMessage, SendMessage};
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use parking_lot::Mutex;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tokio::select;
|
||||
|
||||
/// WhatsApp Web channel using wa-rs with custom rusqlite storage
|
||||
/// WhatsApp Web channel.
|
||||
///
|
||||
/// # Status: Functional Implementation
|
||||
///
|
||||
/// This implementation uses the wa-rs Bot with our custom RusqliteStore backend.
|
||||
///
|
||||
/// # Configuration
|
||||
///
|
||||
/// ```toml
|
||||
/// [channels.whatsapp]
|
||||
/// session_path = "~/.openhuman/whatsapp-session.db"
|
||||
/// pair_phone = "15551234567" # Optional
|
||||
/// allowed_numbers = ["+1234567890", "*"]
|
||||
/// ```
|
||||
/// Wraps a `whatsapp-rust` Bot with our `Channel` trait. The bot owns an
|
||||
/// `Arc<Client>` for outbound operations (`send`, typing) and a `BotHandle`
|
||||
/// for shutdown. Inbound messages are pushed onto an [`mpsc::Sender`] so
|
||||
/// the existing channel inbound subscriber pipeline can process them.
|
||||
#[cfg(feature = "whatsapp-web")]
|
||||
pub struct WhatsAppWebChannel {
|
||||
/// Session database path
|
||||
/// Path to the SQLite session database.
|
||||
session_path: String,
|
||||
/// Phone number for pair code linking (optional)
|
||||
/// Optional phone number for pair-code linking (E.164 digits, no leading `+`).
|
||||
pair_phone: Option<String>,
|
||||
/// Custom pair code (optional)
|
||||
/// Optional pre-allocated pair code paired with `pair_phone`.
|
||||
pair_code: Option<String>,
|
||||
/// Allowed phone numbers (E.164 format) or "*" for all
|
||||
/// E.164 numbers (with leading `+`) allowed to interact, or `["*"]` for any.
|
||||
/// Empty also means "allow all" — same convention as the Cloud API channel.
|
||||
allowed_numbers: Vec<String>,
|
||||
/// Bot handle for shutdown
|
||||
bot_handle: Arc<Mutex<Option<tokio::task::JoinHandle<()>>>>,
|
||||
/// Client handle for sending messages and typing indicators
|
||||
client: Arc<Mutex<Option<Arc<wa_rs::Client>>>>,
|
||||
/// Message sender channel
|
||||
/// Bot run handle, retained for graceful shutdown.
|
||||
bot_handle: Arc<Mutex<Option<whatsapp_rust::bot::BotHandle>>>,
|
||||
/// Live client used for outbound calls; populated after `Bot::build` returns.
|
||||
client: Arc<Mutex<Option<Arc<whatsapp_rust::Client>>>>,
|
||||
/// Liveness signal driven by upstream `Event::Connected` / `LoggedOut` /
|
||||
/// `StreamError`. Used by `health_check` so a dropped session no longer
|
||||
/// reports healthy until process shutdown.
|
||||
connected: Arc<AtomicBool>,
|
||||
/// Group JIDs (`...@g.us`) we've already accepted an allowed inbound
|
||||
/// from. Acts as outbound provenance: replies into a group are only
|
||||
/// permitted after a participant on the per-number allowlist messaged
|
||||
/// in. Without this, any caller able to pass a `recipient` could post
|
||||
/// into arbitrary joined groups via the @g.us suffix.
|
||||
allowed_groups: Arc<Mutex<HashSet<String>>>,
|
||||
/// Sink for inbound `ChannelMessage`s. Populated when [`Channel::listen`]
|
||||
/// is called and shared with the event-handler closure.
|
||||
tx: Arc<Mutex<Option<tokio::sync::mpsc::Sender<ChannelMessage>>>>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "whatsapp-web")]
|
||||
impl WhatsAppWebChannel {
|
||||
/// Create a new WhatsApp Web channel
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `session_path` - Path to the SQLite session database
|
||||
/// * `pair_phone` - Optional phone number for pair code linking (format: "15551234567")
|
||||
/// * `pair_code` - Optional custom pair code (leave empty for auto-generated)
|
||||
/// * `allowed_numbers` - Phone numbers allowed to interact (E.164 format) or "*" for all
|
||||
#[cfg(feature = "whatsapp-web")]
|
||||
/// Construct a channel. The bot does not connect until [`Channel::listen`]
|
||||
/// is invoked.
|
||||
pub fn new(
|
||||
session_path: String,
|
||||
pair_phone: Option<String>,
|
||||
@@ -89,19 +105,88 @@ impl WhatsAppWebChannel {
|
||||
allowed_numbers,
|
||||
bot_handle: Arc::new(Mutex::new(None)),
|
||||
client: Arc::new(Mutex::new(None)),
|
||||
connected: Arc::new(AtomicBool::new(false)),
|
||||
allowed_groups: Arc::new(Mutex::new(HashSet::new())),
|
||||
tx: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a phone number is allowed (E.164 format: +1234567890)
|
||||
#[cfg(feature = "whatsapp-web")]
|
||||
/// Allowlist check. Empty list ⇒ allow-all (matches Cloud API behaviour).
|
||||
fn is_number_allowed(&self, phone: &str) -> bool {
|
||||
self.allowed_numbers.is_empty()
|
||||
|| self.allowed_numbers.iter().any(|n| n == "*" || n == phone)
|
||||
}
|
||||
|
||||
/// Normalize phone number to E.164 format
|
||||
#[cfg(feature = "whatsapp-web")]
|
||||
/// Recognise WhatsApp group JIDs (`...@g.us`). Group recipients bypass
|
||||
/// the per-number outbound allowlist because group membership is
|
||||
/// governed by WhatsApp itself; the inbound side already gated on the
|
||||
/// participant's allowlist status before we ever decided to reply.
|
||||
fn is_group_jid(recipient: &str) -> bool {
|
||||
recipient.trim().ends_with("@g.us")
|
||||
}
|
||||
|
||||
/// Outbound gate combining group-provenance with the per-number allowlist.
|
||||
/// Group JIDs are only permitted when an allowed inbound has already
|
||||
/// been received from that exact group — populated in the inbound
|
||||
/// handler when an allow-listed participant posts. This narrows the
|
||||
/// previous "all `@g.us` is fine" path so an attacker that can supply
|
||||
/// a `recipient` cannot post into arbitrary groups the bot has joined.
|
||||
fn should_allow_outbound(&self, recipient: &str) -> bool {
|
||||
if Self::is_group_jid(recipient) {
|
||||
return self.allowed_groups.lock().contains(recipient.trim());
|
||||
}
|
||||
let normalized = self.normalize_phone(recipient);
|
||||
self.is_number_allowed(&normalized)
|
||||
}
|
||||
|
||||
/// Mask a recipient identifier for log emission. Handles bare phone
|
||||
/// numbers, `<digits>@s.whatsapp.net`/`@lid` DM JIDs, and `@g.us`
|
||||
/// group JIDs uniformly so warning paths never carry a full ID.
|
||||
fn redact_recipient(recipient: &str) -> String {
|
||||
let trimmed = recipient.trim();
|
||||
if let Some((user, server)) = trimmed.split_once('@') {
|
||||
format!("{}@{}", Self::redact_phone(user), server)
|
||||
} else {
|
||||
Self::redact_phone(trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
/// Pick the address downstream replies should be sent back to.
|
||||
///
|
||||
/// Group chats are addressed by the group JID (`...@g.us`); a reply that
|
||||
/// targeted the participant's phone instead would leak the conversation
|
||||
/// into a private DM.
|
||||
fn compute_reply_target(chat_jid: &str, sender_normalized: &str) -> String {
|
||||
if chat_jid.ends_with("@g.us") {
|
||||
chat_jid.to_string()
|
||||
} else {
|
||||
sender_normalized.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Mask the middle digits of an E.164 number so logs only carry a coarse
|
||||
/// fingerprint instead of the full identifier.
|
||||
fn redact_phone(phone: &str) -> String {
|
||||
let prefix = if phone.starts_with('+') { "+" } else { "" };
|
||||
if phone.len() <= prefix.len() + 4 {
|
||||
return format!("{prefix}****");
|
||||
}
|
||||
let tail = &phone[phone.len() - 4..];
|
||||
format!("{prefix}***{tail}")
|
||||
}
|
||||
|
||||
/// Pull the displayable text out of an inbound WhatsApp Message proto.
|
||||
/// Falls back from `conversation` to `extended_text_message.text`, then
|
||||
/// to an empty string for non-text payloads.
|
||||
fn extract_message_text(conversation: Option<&str>, extended_text: Option<&str>) -> String {
|
||||
conversation
|
||||
.or(extended_text)
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Render an arbitrary recipient string as E.164 with a leading `+`,
|
||||
/// stripping any `@server` JID suffix the caller passed in.
|
||||
fn normalize_phone(&self, phone: &str) -> String {
|
||||
let trimmed = phone.trim();
|
||||
let user_part = trimmed
|
||||
@@ -109,20 +194,12 @@ impl WhatsAppWebChannel {
|
||||
.map(|(user, _)| user)
|
||||
.unwrap_or(trimmed);
|
||||
let normalized_user = user_part.trim_start_matches('+');
|
||||
if user_part.starts_with('+') {
|
||||
format!("+{normalized_user}")
|
||||
} else {
|
||||
format!("+{normalized_user}")
|
||||
}
|
||||
format!("+{normalized_user}")
|
||||
}
|
||||
|
||||
/// Convert a recipient to a wa-rs JID.
|
||||
///
|
||||
/// Supports:
|
||||
/// - 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<wa_rs_binary::jid::Jid> {
|
||||
/// Convert a recipient (full JID like `12345@s.whatsapp.net` or an E.164
|
||||
/// number like `+1234567890`) into a `whatsapp-rust` JID.
|
||||
fn recipient_to_jid(&self, recipient: &str) -> Result<whatsapp_rust::Jid> {
|
||||
let trimmed = recipient.trim();
|
||||
if trimmed.is_empty() {
|
||||
anyhow::bail!("Recipient cannot be empty");
|
||||
@@ -130,7 +207,7 @@ impl WhatsAppWebChannel {
|
||||
|
||||
if trimmed.contains('@') {
|
||||
return trimmed
|
||||
.parse::<wa_rs_binary::jid::Jid>()
|
||||
.parse::<whatsapp_rust::Jid>()
|
||||
.map_err(|e| anyhow!("Invalid WhatsApp JID `{trimmed}`: {e}"));
|
||||
}
|
||||
|
||||
@@ -139,7 +216,7 @@ impl WhatsAppWebChannel {
|
||||
anyhow::bail!("Recipient `{trimmed}` does not contain a valid phone number");
|
||||
}
|
||||
|
||||
Ok(wa_rs_binary::jid::Jid::pn(digits))
|
||||
Ok(whatsapp_rust::Jid::pn(digits))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,18 +233,16 @@ impl Channel for WhatsAppWebChannel {
|
||||
anyhow::bail!("WhatsApp Web client not connected. Initialize the bot first.");
|
||||
};
|
||||
|
||||
// Validate recipient is allowed
|
||||
let normalized = self.normalize_phone(&message.recipient);
|
||||
if !self.is_number_allowed(&normalized) {
|
||||
if !self.should_allow_outbound(&message.recipient) {
|
||||
tracing::warn!(
|
||||
"WhatsApp Web: recipient {} not in allowed list",
|
||||
message.recipient
|
||||
Self::redact_recipient(&message.recipient)
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let to = self.recipient_to_jid(&message.recipient)?;
|
||||
let outgoing = wa_rs_proto::whatsapp::Message {
|
||||
let outgoing = whatsapp_rust::waproto::whatsapp::Message {
|
||||
conversation: Some(message.content.clone()),
|
||||
..Default::default()
|
||||
};
|
||||
@@ -182,124 +257,178 @@ impl Channel for WhatsAppWebChannel {
|
||||
}
|
||||
|
||||
async fn listen(&self, tx: tokio::sync::mpsc::Sender<ChannelMessage>) -> Result<()> {
|
||||
// Store the sender channel for incoming messages
|
||||
*self.tx.lock() = Some(tx.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::types::events::Event;
|
||||
use whatsapp_rust::bot::Bot;
|
||||
use whatsapp_rust::pair_code::PairCodeOptions;
|
||||
use whatsapp_rust::store::SqliteStore;
|
||||
use whatsapp_rust::TokioRuntime;
|
||||
use whatsapp_rust_tokio_transport::TokioWebSocketTransportFactory;
|
||||
use whatsapp_rust_ureq_http_client::UreqHttpClient;
|
||||
|
||||
tracing::info!(
|
||||
"WhatsApp Web channel starting (session: {})",
|
||||
self.session_path
|
||||
);
|
||||
|
||||
// Initialize storage backend
|
||||
let storage = RusqliteStore::new(&self.session_path)?;
|
||||
let backend = Arc::new(storage);
|
||||
// Upstream's SqliteStore implements all four storage traits the bot
|
||||
// needs (Signal, AppSync, Protocol, Device). It also handles
|
||||
// first-run schema creation, so no separate `exists`/`load` dance.
|
||||
// If the on-disk DB is a leftover from the wa-rs 0.2 fork the schema
|
||||
// is incompatible — surface that explicitly so the user knows to
|
||||
// delete the old session file and re-pair.
|
||||
let backend = Arc::new(SqliteStore::new(&self.session_path).await.map_err(|e| {
|
||||
anyhow!(
|
||||
"Failed to open WhatsApp session store at `{}`: {}. \
|
||||
If upgrading from wa-rs 0.2, delete the old DB and re-pair \
|
||||
(see the module-level migration note).",
|
||||
self.session_path,
|
||||
e
|
||||
)
|
||||
})?);
|
||||
|
||||
// Check if we have a saved device to load
|
||||
let mut device = Device::new(backend.clone());
|
||||
if backend.exists().await? {
|
||||
tracing::info!("WhatsApp Web: found existing session, loading device");
|
||||
if let Some(core_device) = backend.load().await? {
|
||||
device.load_from_serializable(core_device);
|
||||
} else {
|
||||
anyhow::bail!("Device exists but failed to load");
|
||||
}
|
||||
} else {
|
||||
tracing::info!(
|
||||
"WhatsApp Web: no existing session, new device will be created during pairing"
|
||||
);
|
||||
};
|
||||
|
||||
// Create transport factory
|
||||
let mut transport_factory = TokioWebSocketTransportFactory::new();
|
||||
if let Ok(ws_url) = std::env::var("WHATSAPP_WS_URL") {
|
||||
transport_factory = transport_factory.with_url(ws_url);
|
||||
}
|
||||
|
||||
// Create HTTP client for media operations
|
||||
let http_client = UreqHttpClient::new();
|
||||
|
||||
// Build the bot
|
||||
let tx_clone = tx.clone();
|
||||
let tx_for_handler = tx.clone();
|
||||
let allowed_numbers = self.allowed_numbers.clone();
|
||||
let connected_for_handler = Arc::clone(&self.connected);
|
||||
let allowed_groups_for_handler = Arc::clone(&self.allowed_groups);
|
||||
|
||||
let mut builder = Bot::builder()
|
||||
.with_backend(backend)
|
||||
.with_transport_factory(transport_factory)
|
||||
.with_http_client(http_client)
|
||||
.with_runtime(TokioRuntime)
|
||||
.on_event(move |event, _client| {
|
||||
let tx_inner = tx_clone.clone();
|
||||
let tx_inner = tx_for_handler.clone();
|
||||
let allowed_numbers = allowed_numbers.clone();
|
||||
let connected = Arc::clone(&connected_for_handler);
|
||||
let allowed_groups = Arc::clone(&allowed_groups_for_handler);
|
||||
async move {
|
||||
match event {
|
||||
Event::Message(msg, info) => {
|
||||
// Extract message content
|
||||
let text = msg.text_content().unwrap_or("");
|
||||
let sender = info.source.sender.user().to_string();
|
||||
let chat = info.source.chat.to_string();
|
||||
// Self-echoes (messages this user sent from another
|
||||
// linked device) are mirrored to all devices via
|
||||
// the WhatsApp protocol. Drop them so the agent
|
||||
// doesn't react to its own outgoing messages.
|
||||
if info.source.is_from_me {
|
||||
return;
|
||||
}
|
||||
|
||||
tracing::info!("📨 WhatsApp message from {} in {}: {}", sender, chat, text);
|
||||
let text = Self::extract_message_text(
|
||||
msg.conversation.as_deref(),
|
||||
msg.extended_text_message
|
||||
.as_ref()
|
||||
.and_then(|e| e.text.as_deref()),
|
||||
);
|
||||
|
||||
// Check if sender is allowed
|
||||
let normalized = if sender.starts_with('+') {
|
||||
sender.clone()
|
||||
// Sender JID can use either the legacy `s.whatsapp.net`
|
||||
// server (phone-number addressing) or the newer `lid`
|
||||
// server (privacy-preserving identifier). Render the
|
||||
// user portion in E.164 with a leading `+` for the
|
||||
// allowed-list check + downstream subscriber.
|
||||
let sender_user = info.source.sender.user.clone();
|
||||
let normalized = if sender_user.starts_with('+') {
|
||||
sender_user.clone()
|
||||
} else {
|
||||
format!("+{sender}")
|
||||
format!("+{sender_user}")
|
||||
};
|
||||
let chat = info.source.chat.to_string();
|
||||
let reply_target = Self::compute_reply_target(&chat, &normalized);
|
||||
|
||||
// Routine logs only carry coarse metadata — no raw
|
||||
// sender identifier, no message body — so PII does
|
||||
// not leak into application logs at any level.
|
||||
// For DM chats `chat` is `<phone>@s.whatsapp.net`,
|
||||
// which still carries the participant's phone
|
||||
// number. Redact the user part so the routine
|
||||
// log keeps only the server suffix (DM vs group)
|
||||
// and a coarse identifier tail.
|
||||
tracing::info!(
|
||||
"📨 WhatsApp inbound: chat={} sender={} text_len={}",
|
||||
Self::redact_recipient(&chat),
|
||||
Self::redact_phone(&normalized),
|
||||
text.len()
|
||||
);
|
||||
|
||||
if allowed_numbers.is_empty()
|
||||
|| allowed_numbers.iter().any(|n| n == "*" || n == &normalized)
|
||||
{
|
||||
// Record group provenance: this group has had at
|
||||
// least one allow-listed participant message in,
|
||||
// so subsequent outbound replies into the same
|
||||
// group are legitimate. Outbound to groups
|
||||
// without provenance is rejected by
|
||||
// `should_allow_outbound`.
|
||||
if Self::is_group_jid(&chat) {
|
||||
allowed_groups.lock().insert(chat.clone());
|
||||
}
|
||||
if let Err(e) = tx_inner
|
||||
.send(ChannelMessage {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
channel: "whatsapp".to_string(),
|
||||
sender: normalized.clone(),
|
||||
reply_target: normalized.clone(),
|
||||
content: text.to_string(),
|
||||
reply_target,
|
||||
content: text,
|
||||
timestamp: chrono::Utc::now().timestamp_millis() as u64,
|
||||
thread_ts: None,
|
||||
})
|
||||
.await
|
||||
{
|
||||
tracing::error!("Failed to send message to channel: {}", e);
|
||||
tracing::error!(
|
||||
"Failed to forward WhatsApp message to channel: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
} else {
|
||||
tracing::warn!("WhatsApp Web: message from {} not in allowed list", normalized);
|
||||
tracing::warn!(
|
||||
"WhatsApp Web: message from {} not in allowed list",
|
||||
Self::redact_phone(&normalized)
|
||||
);
|
||||
}
|
||||
}
|
||||
Event::Connected(_) => {
|
||||
connected.store(true, Ordering::Release);
|
||||
tracing::info!("✅ WhatsApp Web connected successfully!");
|
||||
}
|
||||
Event::LoggedOut(_) => {
|
||||
connected.store(false, Ordering::Release);
|
||||
tracing::warn!("❌ WhatsApp Web was logged out!");
|
||||
}
|
||||
Event::StreamError(stream_error) => {
|
||||
connected.store(false, Ordering::Release);
|
||||
tracing::error!("❌ WhatsApp Web stream error: {:?}", stream_error);
|
||||
}
|
||||
Event::PairingCode { code, .. } => {
|
||||
tracing::info!("🔑 Pair code received: {}", code);
|
||||
tracing::info!("Link your phone by entering this code in WhatsApp > Linked Devices");
|
||||
// The pair code and QR payload are short-lived link
|
||||
// credentials — anyone reading the logs while they
|
||||
// are valid can hijack the session. Surface only a
|
||||
// non-sensitive notice; the raw payload is never
|
||||
// logged at any level. Surfacing the code to the
|
||||
// user is the responsibility of an upstream UX
|
||||
// path (e.g. a JSON-RPC event the frontend renders).
|
||||
Event::PairingCode { .. } => {
|
||||
tracing::info!(
|
||||
"🔑 WhatsApp Web pair code received. Enter the code shown on \
|
||||
your linking surface into WhatsApp > Linked Devices."
|
||||
);
|
||||
}
|
||||
Event::PairingQrCode { code, .. } => {
|
||||
tracing::info!("📱 QR code received (scan with WhatsApp > Linked Devices)");
|
||||
tracing::debug!("QR code: {}", code);
|
||||
Event::PairingQrCode { .. } => {
|
||||
tracing::info!(
|
||||
"📱 WhatsApp Web QR code received. Render via QR generator and \
|
||||
scan with WhatsApp > Linked Devices."
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
})
|
||||
;
|
||||
});
|
||||
|
||||
// Configure pair-code flow when a phone number is provided.
|
||||
if let Some(ref phone) = self.pair_phone {
|
||||
tracing::info!("WhatsApp Web: pair-code flow enabled for configured phone number");
|
||||
builder = builder.with_pair_code(PairCodeOptions {
|
||||
@@ -316,35 +445,43 @@ impl Channel for WhatsAppWebChannel {
|
||||
let mut bot = builder.build().await?;
|
||||
*self.client.lock() = Some(bot.client());
|
||||
|
||||
// Run the bot
|
||||
let bot_handle = bot.run().await?;
|
||||
|
||||
// Store the bot handle for later shutdown
|
||||
*self.bot_handle.lock() = Some(bot_handle);
|
||||
|
||||
// Wait for shutdown signal
|
||||
let (_shutdown_tx, mut shutdown_rx) = tokio::sync::broadcast::channel::<()>(1);
|
||||
|
||||
select! {
|
||||
_ = shutdown_rx.recv() => {
|
||||
tracing::info!("WhatsApp Web channel shutting down");
|
||||
// Wire into the shared shutdown machinery in `core::shutdown` so
|
||||
// SIGTERM and SIGINT both trigger a coordinated tear-down. The
|
||||
// previous `tokio::signal::ctrl_c()` path silently ignored
|
||||
// SIGTERM and bypassed the registered cleanup hooks the rest of
|
||||
// the process uses.
|
||||
let shutdown_notify = Arc::new(tokio::sync::Notify::new());
|
||||
let bot_handle_for_hook = Arc::clone(&self.bot_handle);
|
||||
let connected_for_hook = Arc::clone(&self.connected);
|
||||
let client_for_hook = Arc::clone(&self.client);
|
||||
let notify_for_hook = Arc::clone(&shutdown_notify);
|
||||
crate::core::shutdown::register(move || {
|
||||
let bot_handle = Arc::clone(&bot_handle_for_hook);
|
||||
let connected = Arc::clone(&connected_for_hook);
|
||||
let client = Arc::clone(&client_for_hook);
|
||||
let notify = Arc::clone(¬ify_for_hook);
|
||||
async move {
|
||||
tracing::info!("[whatsapp_web] graceful shutdown hook firing — aborting bot");
|
||||
connected.store(false, Ordering::Release);
|
||||
*client.lock() = None;
|
||||
if let Some(handle) = bot_handle.lock().take() {
|
||||
handle.abort();
|
||||
}
|
||||
notify.notify_waiters();
|
||||
}
|
||||
_ = tokio::signal::ctrl_c() => {
|
||||
tracing::info!("WhatsApp Web channel received Ctrl+C");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
*self.client.lock() = None;
|
||||
if let Some(handle) = self.bot_handle.lock().take() {
|
||||
handle.abort();
|
||||
}
|
||||
shutdown_notify.notified().await;
|
||||
tracing::info!("WhatsApp Web channel exited via shared shutdown");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn health_check(&self) -> bool {
|
||||
let bot_handle_guard = self.bot_handle.lock();
|
||||
bot_handle_guard.is_some()
|
||||
self.connected.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
async fn start_typing(&self, recipient: &str) -> Result<()> {
|
||||
@@ -353,11 +490,10 @@ impl Channel for WhatsAppWebChannel {
|
||||
anyhow::bail!("WhatsApp Web client not connected. Initialize the bot first.");
|
||||
};
|
||||
|
||||
let normalized = self.normalize_phone(recipient);
|
||||
if !self.is_number_allowed(&normalized) {
|
||||
if !self.should_allow_outbound(recipient) {
|
||||
tracing::warn!(
|
||||
"WhatsApp Web: typing target {} not in allowed list",
|
||||
recipient
|
||||
Self::redact_recipient(recipient)
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
@@ -379,11 +515,10 @@ impl Channel for WhatsAppWebChannel {
|
||||
anyhow::bail!("WhatsApp Web client not connected. Initialize the bot first.");
|
||||
};
|
||||
|
||||
let normalized = self.normalize_phone(recipient);
|
||||
if !self.is_number_allowed(&normalized) {
|
||||
if !self.should_allow_outbound(recipient) {
|
||||
tracing::warn!(
|
||||
"WhatsApp Web: typing target {} not in allowed list",
|
||||
recipient
|
||||
Self::redact_recipient(recipient)
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
@@ -400,7 +535,8 @@ impl Channel for WhatsAppWebChannel {
|
||||
}
|
||||
}
|
||||
|
||||
// Stub implementation when feature is not enabled
|
||||
// Stub implementation when the feature is not enabled. Keeps the public ctor
|
||||
// signature compatible so `runtime/startup.rs` compiles unchanged.
|
||||
#[cfg(not(feature = "whatsapp-web"))]
|
||||
pub struct WhatsAppWebChannel {
|
||||
_private: (),
|
||||
@@ -458,6 +594,5 @@ impl Channel for WhatsAppWebChannel {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "whatsapp_web_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -61,3 +61,194 @@ async fn whatsapp_web_health_check_disconnected() {
|
||||
let ch = make_channel();
|
||||
assert!(!ch.health_check().await);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg(feature = "whatsapp-web")]
|
||||
async fn whatsapp_web_health_check_tracks_connected_flag() {
|
||||
let ch = make_channel();
|
||||
assert!(!ch.health_check().await);
|
||||
ch.connected.store(true, Ordering::Release);
|
||||
assert!(ch.health_check().await);
|
||||
ch.connected.store(false, Ordering::Release);
|
||||
assert!(!ch.health_check().await);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "whatsapp-web")]
|
||||
fn whatsapp_web_compute_reply_target_dm_pn() {
|
||||
assert_eq!(
|
||||
WhatsAppWebChannel::compute_reply_target("123@s.whatsapp.net", "+1234567890"),
|
||||
"+1234567890"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "whatsapp-web")]
|
||||
fn whatsapp_web_compute_reply_target_dm_lid() {
|
||||
assert_eq!(
|
||||
WhatsAppWebChannel::compute_reply_target("abc@lid", "+1234567890"),
|
||||
"+1234567890"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "whatsapp-web")]
|
||||
fn whatsapp_web_compute_reply_target_group() {
|
||||
assert_eq!(
|
||||
WhatsAppWebChannel::compute_reply_target("987654@g.us", "+1234567890"),
|
||||
"987654@g.us"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "whatsapp-web")]
|
||||
fn whatsapp_web_redact_phone_e164() {
|
||||
assert_eq!(WhatsAppWebChannel::redact_phone("+1234567890"), "+***7890");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "whatsapp-web")]
|
||||
fn whatsapp_web_redact_phone_no_plus() {
|
||||
assert_eq!(WhatsAppWebChannel::redact_phone("1234567890"), "***7890");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "whatsapp-web")]
|
||||
fn whatsapp_web_redact_phone_short_input() {
|
||||
// Pathological short inputs collapse to a generic mask rather than
|
||||
// exposing the entire identifier.
|
||||
assert_eq!(WhatsAppWebChannel::redact_phone("+12"), "+****");
|
||||
assert_eq!(WhatsAppWebChannel::redact_phone("12"), "****");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "whatsapp-web")]
|
||||
fn whatsapp_web_extract_message_text_prefers_conversation() {
|
||||
assert_eq!(
|
||||
WhatsAppWebChannel::extract_message_text(Some("hello"), Some("ignored")),
|
||||
"hello"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "whatsapp-web")]
|
||||
fn whatsapp_web_extract_message_text_falls_back_to_extended() {
|
||||
assert_eq!(
|
||||
WhatsAppWebChannel::extract_message_text(None, Some("from extended")),
|
||||
"from extended"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "whatsapp-web")]
|
||||
fn whatsapp_web_extract_message_text_empty_when_missing() {
|
||||
assert_eq!(WhatsAppWebChannel::extract_message_text(None, None), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "whatsapp-web")]
|
||||
fn whatsapp_web_is_group_jid_recognises_group() {
|
||||
assert!(WhatsAppWebChannel::is_group_jid("123456@g.us"));
|
||||
assert!(WhatsAppWebChannel::is_group_jid(" 4567@g.us "));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "whatsapp-web")]
|
||||
fn whatsapp_web_is_group_jid_rejects_non_group() {
|
||||
assert!(!WhatsAppWebChannel::is_group_jid("+1234567890"));
|
||||
assert!(!WhatsAppWebChannel::is_group_jid("123@s.whatsapp.net"));
|
||||
assert!(!WhatsAppWebChannel::is_group_jid("abc@lid"));
|
||||
assert!(!WhatsAppWebChannel::is_group_jid(""));
|
||||
}
|
||||
|
||||
/// Regression for CodeRabbit finding: an `@g.us` reply target was being
|
||||
/// silently dropped because the outbound path normalised the JID to
|
||||
/// `+<group-id>` and missed the per-number allowlist. After provenance
|
||||
/// is recorded, an allowed user replying back into the group they came
|
||||
/// from must succeed.
|
||||
#[test]
|
||||
#[cfg(feature = "whatsapp-web")]
|
||||
fn whatsapp_web_should_allow_outbound_provenanced_group_allowed() {
|
||||
let ch = make_channel(); // allowed_numbers = ["+1234567890"]
|
||||
ch.allowed_groups
|
||||
.lock()
|
||||
.insert("987654321@g.us".to_string());
|
||||
assert!(ch.should_allow_outbound("987654321@g.us"));
|
||||
}
|
||||
|
||||
/// Regression for the follow-up CodeRabbit finding: a blanket `@g.us`
|
||||
/// bypass is itself a vulnerability — a caller able to set `recipient`
|
||||
/// could post into arbitrary joined groups. Groups without recorded
|
||||
/// provenance must stay blocked.
|
||||
#[test]
|
||||
#[cfg(feature = "whatsapp-web")]
|
||||
fn whatsapp_web_should_allow_outbound_unrelated_group_blocked() {
|
||||
let ch = make_channel();
|
||||
ch.allowed_groups
|
||||
.lock()
|
||||
.insert("987654321@g.us".to_string());
|
||||
assert!(!ch.should_allow_outbound("11111@g.us"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "whatsapp-web")]
|
||||
fn whatsapp_web_should_allow_outbound_group_without_provenance_blocked() {
|
||||
let ch = make_channel();
|
||||
// empty allowed_groups
|
||||
assert!(!ch.should_allow_outbound("987654321@g.us"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "whatsapp-web")]
|
||||
fn whatsapp_web_redact_recipient_pn_jid() {
|
||||
assert_eq!(
|
||||
WhatsAppWebChannel::redact_recipient("1234567890@s.whatsapp.net"),
|
||||
"***7890@s.whatsapp.net"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "whatsapp-web")]
|
||||
fn whatsapp_web_redact_recipient_group_jid() {
|
||||
assert_eq!(
|
||||
WhatsAppWebChannel::redact_recipient("987654321@g.us"),
|
||||
"***4321@g.us"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "whatsapp-web")]
|
||||
fn whatsapp_web_redact_recipient_bare_phone() {
|
||||
assert_eq!(
|
||||
WhatsAppWebChannel::redact_recipient("+1234567890"),
|
||||
"+***7890"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "whatsapp-web")]
|
||||
fn whatsapp_web_should_allow_outbound_dm_blocks_unallowed() {
|
||||
let ch = make_channel();
|
||||
assert!(!ch.should_allow_outbound("+9999999999"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "whatsapp-web")]
|
||||
fn whatsapp_web_should_allow_outbound_dm_allows_match() {
|
||||
let ch = make_channel();
|
||||
assert!(ch.should_allow_outbound("+1234567890"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "whatsapp-web")]
|
||||
fn whatsapp_web_should_allow_outbound_wildcard_passes_dm() {
|
||||
let ch = WhatsAppWebChannel::new("/tmp/t.db".into(), None, None, vec!["*".into()]);
|
||||
assert!(ch.should_allow_outbound("+9999999999"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "whatsapp-web")]
|
||||
fn whatsapp_web_should_allow_outbound_empty_allowlist_passes_dm() {
|
||||
let ch = WhatsAppWebChannel::new("/tmp/t.db".into(), None, None, vec![]);
|
||||
assert!(ch.should_allow_outbound("+9999999999"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user