From 78fc974aa39e06188aacb5a092285dcf6d95eecf Mon Sep 17 00:00:00 2001 From: tianrking Date: Sat, 7 Mar 2026 02:13:56 +0800 Subject: [PATCH] feat(channels): add MQTT pub/sub channel adapter MQTT is a lightweight messaging protocol widely used in IoT scenarios. This adapter enables OpenFang to receive and respond to messages via MQTT brokers, making it suitable for: - IoT device integration and control - Low-bandwidth/high-latency network environments - Real-time pub/sub messaging systems - Edge computing scenarios The implementation includes: - Configurable QoS levels (0, 1, 2) - TLS/SSL support for secure connections - Automatic reconnection with exponential backoff - JSON and plain text payload parsing - Topic-based routing for replies Configuration example added to README.md with usage instructions. --- Cargo.lock | 752 ++++++++++++---------- Cargo.toml | 3 + README.md | 30 + crates/openfang-api/src/channel_bridge.rs | 17 +- crates/openfang-channels/Cargo.toml | 3 + crates/openfang-channels/src/lib.rs | 1 + crates/openfang-channels/src/mqtt.rs | 563 ++++++++++++++++ crates/openfang-types/src/config.rs | 63 ++ 8 files changed, 1079 insertions(+), 353 deletions(-) create mode 100644 crates/openfang-channels/src/mqtt.rs diff --git a/Cargo.lock b/Cargo.lock index b764783ad..69f2fb512 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -221,9 +221,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d67d43201f4d20c78bcda740c142ca52482d81da80681533d33bf3f0596c8e2" +checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" dependencies = [ "compression-codecs", "compression-core", @@ -258,7 +258,7 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix 1.1.3", + "rustix 1.1.4", "slab", "windows-sys 0.61.2", ] @@ -289,7 +289,7 @@ dependencies = [ "cfg-if", "event-listener", "futures-lite", - "rustix 1.1.3", + "rustix 1.1.4", ] [[package]] @@ -315,7 +315,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 1.1.3", + "rustix 1.1.4", "signal-hook-registry", "slab", "windows-sys 0.61.2", @@ -735,9 +735,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -913,6 +913,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -931,12 +941,12 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core-graphics" -version = "0.24.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ "bitflags 2.11.0", - "core-foundation", + "core-foundation 0.10.1", "core-graphics-types", "foreign-types 0.5.0", "libc", @@ -949,7 +959,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ "bitflags 2.11.0", - "core-foundation", + "core-foundation 0.10.1", "libc", ] @@ -973,36 +983,36 @@ dependencies = [ [[package]] name = "cranelift-assembler-x64" -version = "0.128.3" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0377b13bf002a0774fcccac4f1102a10f04893d24060cf4b7350c87e4cbb647c" +checksum = "50a04121a197fde2fe896f8e7cac9812fc41ed6ee9c63e1906090f9f497845f6" dependencies = [ "cranelift-assembler-x64-meta", ] [[package]] name = "cranelift-assembler-x64-meta" -version = "0.128.3" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfa027979140d023b25bf7509fb7ede3a54c3d3871fb5ead4673c4b633f671a2" +checksum = "a09e699a94f477303820fb2167024f091543d6240783a2d3b01a3f21c42bc744" dependencies = [ "cranelift-srcgen", ] [[package]] name = "cranelift-bforest" -version = "0.128.3" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "618e4da87d9179a70b3c2f664451ca8898987aa6eb9f487d16988588b5d8cc40" +checksum = "f07732c662a9755529e332d86f8c5842171f6e98ba4d5976a178043dad838654" dependencies = [ "cranelift-entity", ] [[package]] name = "cranelift-bitset" -version = "0.128.3" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db53764b5dad233b37b8f5dc54d3caa9900c54579195e00f17ea21f03f71aaa7" +checksum = "18391da761cf362a06def7a7cf11474d79e55801dd34c2e9ba105b33dc0aef88" dependencies = [ "serde", "serde_derive", @@ -1010,9 +1020,9 @@ dependencies = [ [[package]] name = "cranelift-codegen" -version = "0.128.3" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae927f1d8c0abddaa863acd201471d56e7fc6c3925104f4861ed4dc3e28b421" +checksum = "0b3a09b3042c69810d255aef59ddc3b3e4c0644d1d90ecfd6e3837798cc88a3c" dependencies = [ "bumpalo", "cranelift-assembler-x64", @@ -1037,9 +1047,9 @@ dependencies = [ [[package]] name = "cranelift-codegen-meta" -version = "0.128.3" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fcf1e3e6757834bd2584f4cbff023fcc198e9279dcb5d684b4bb27a9b19f54" +checksum = "75817926ec812241889208d1b190cadb7fedded4592a4bb01b8524babb9e4849" dependencies = [ "cranelift-assembler-x64-meta", "cranelift-codegen-shared", @@ -1050,24 +1060,24 @@ dependencies = [ [[package]] name = "cranelift-codegen-shared" -version = "0.128.3" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "205dcb9e6ccf9d368b7466be675ff6ee54a63e36da6fe20e72d45169cf6fd254" +checksum = "859158f87a59476476eda3884d883c32e08a143cf3d315095533b362a3250a63" [[package]] name = "cranelift-control" -version = "0.128.3" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "108eca9fcfe86026054f931eceaf57b722c1b97464bf8265323a9b5877238817" +checksum = "03b65a9aec442d715cbf54d14548b8f395476c09cef7abe03e104a378291ab88" dependencies = [ "arbitrary", ] [[package]] name = "cranelift-entity" -version = "0.128.3" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d96496910065d3165f84ff8e1e393916f4c086f88ac8e1b407678bc78735aa" +checksum = "8334c99a7e86060c24028732efd23bac84585770dcb752329c69f135d64f2fc1" dependencies = [ "cranelift-bitset", "serde", @@ -1076,9 +1086,9 @@ dependencies = [ [[package]] name = "cranelift-frontend" -version = "0.128.3" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e303983ad7e23c850f24d9c41fc3cb346e1b930f066d3966545e4c98dac5c9fb" +checksum = "43ac6c095aa5b3e845d7ca3461e67e2b65249eb5401477a5ff9100369b745111" dependencies = [ "cranelift-codegen", "log", @@ -1088,15 +1098,15 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.128.3" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24b0cf8d867d891245836cac7abafb0a5b0ea040a019d720702b3b8bcba40bfa" +checksum = "69d3d992870ed4f0f2e82e2175275cb3a123a46e9660c6558c46417b822c91fa" [[package]] name = "cranelift-native" -version = "0.128.3" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e24b641e315443e27807b69c440fe766737d7e718c68beb665a2d69259c77bf3" +checksum = "ee32e36beaf80f309edb535274cfe0349e1c5cf5799ba2d9f42e828285c6b52e" dependencies = [ "cranelift-codegen", "libc", @@ -1105,9 +1115,9 @@ dependencies = [ [[package]] name = "cranelift-srcgen" -version = "0.128.3" +version = "0.128.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e378a54e7168a689486d67ee1f818b7e5356e54ae51a1d7a53f4f13f7f8b7a" +checksum = "903adeaf4938e60209a97b53a2e4326cd2d356aab9764a1934630204bae381c9" [[package]] name = "crc32fast" @@ -1509,17 +1519,11 @@ dependencies = [ "winapi", ] -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - [[package]] name = "dispatch2" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ "bitflags 2.11.0", "block2", @@ -1720,9 +1724,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "erased-serde" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" dependencies = [ "serde", "serde_core", @@ -1836,6 +1840,17 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + [[package]] name = "fnv" version = "1.0.7" @@ -2154,7 +2169,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ - "rustix 1.1.3", + "rustix 1.1.4", "windows-link 0.2.1", ] @@ -2191,20 +2206,20 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "wasip2", "wasip3", ] @@ -2594,10 +2609,10 @@ dependencies = [ "http", "hyper", "hyper-util", - "rustls", + "rustls 0.23.37", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", "tower-service", "webpki-roots", ] @@ -2619,7 +2634,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.2", + "socket2 0.6.3", "tokio", "tower-service", "tracing", @@ -2890,9 +2905,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" @@ -3030,9 +3045,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.87" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f0862381daaec758576dcc22eb7bbf4d7efd67328553f3b45a412a51a3fb21" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -3127,10 +3142,10 @@ dependencies = [ "nom 8.0.0", "percent-encoding", "quoted_printable", - "rustls", - "socket2 0.6.2", + "rustls 0.23.37", + "socket2 0.6.3", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", "url", "webpki-roots", ] @@ -3196,13 +3211,14 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ "bitflags 2.11.0", "libc", - "redox_syscall 0.7.1", + "plain", + "redox_syscall 0.7.3", ] [[package]] @@ -3224,9 +3240,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -3272,9 +3288,9 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] name = "mac-notification-sys" -version = "0.6.9" +version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fd3f75411f4725061682ed91f131946e912859d0044d39c4ec0aac818d7621" +checksum = "26053f9919b5b032f327ab94d830f2465c4c88138e9df23c8fcd305060a9b28b" dependencies = [ "cc", "objc2", @@ -3360,7 +3376,7 @@ version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" dependencies = [ - "rustix 1.1.3", + "rustix 1.1.4", ] [[package]] @@ -3390,9 +3406,9 @@ dependencies = [ [[package]] name = "minisign-verify" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e856fdd13623a2f5f2f54676a4ee49502a96a80ef4a62bcedd23d52427c44d43" +checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e" [[package]] name = "miniz_oxide" @@ -3456,10 +3472,10 @@ dependencies = [ "libc", "log", "openssl", - "openssl-probe", + "openssl-probe 0.2.1", "openssl-sys", "schannel", - "security-framework", + "security-framework 3.7.0", "security-framework-sys", "tempfile", ] @@ -3563,9 +3579,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-traits" @@ -3592,7 +3608,7 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", "syn 2.0.117", @@ -3600,9 +3616,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", "objc2-exception-helper", @@ -3616,38 +3632,8 @@ checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ "bitflags 2.11.0", "block2", - "libc", "objc2", - "objc2-cloud-kit", - "objc2-core-data", "objc2-core-foundation", - "objc2-core-graphics", - "objc2-core-image", - "objc2-core-text", - "objc2-core-video", - "objc2-foundation", - "objc2-quartz-core", -] - -[[package]] -name = "objc2-cloud-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" -dependencies = [ - "bitflags 2.11.0", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-data" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" -dependencies = [ - "bitflags 2.11.0", - "objc2", "objc2-foundation", ] @@ -3675,41 +3661,6 @@ dependencies = [ "objc2-io-surface", ] -[[package]] -name = "objc2-core-image" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-text" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" -dependencies = [ - "bitflags 2.11.0", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", -] - -[[package]] -name = "objc2-core-video" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" -dependencies = [ - "bitflags 2.11.0", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-io-surface", -] - [[package]] name = "objc2-encode" version = "4.1.0" @@ -3749,16 +3700,6 @@ dependencies = [ "objc2-core-foundation", ] -[[package]] -name = "objc2-javascript-core" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586" -dependencies = [ - "objc2", - "objc2-core-foundation", -] - [[package]] name = "objc2-osa-kit" version = "0.3.2" @@ -3783,17 +3724,6 @@ dependencies = [ "objc2-foundation", ] -[[package]] -name = "objc2-security" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" -dependencies = [ - "bitflags 2.11.0", - "objc2", - "objc2-core-foundation", -] - [[package]] name = "objc2-ui-kit" version = "0.3.2" @@ -3818,8 +3748,6 @@ dependencies = [ "objc2-app-kit", "objc2-core-foundation", "objc2-foundation", - "objc2-javascript-core", - "objc2-security", ] [[package]] @@ -3866,7 +3794,7 @@ dependencies = [ [[package]] name = "openfang-api" -version = "0.3.23" +version = "0.3.24" dependencies = [ "async-trait", "axum", @@ -3903,7 +3831,7 @@ dependencies = [ [[package]] name = "openfang-channels" -version = "0.3.23" +version = "0.3.24" dependencies = [ "async-trait", "axum", @@ -3919,6 +3847,7 @@ dependencies = [ "native-tls", "openfang-types", "reqwest 0.12.28", + "rumqttc", "serde", "serde_json", "sha2", @@ -3934,7 +3863,7 @@ dependencies = [ [[package]] name = "openfang-cli" -version = "0.3.23" +version = "0.3.24" dependencies = [ "clap", "clap_complete", @@ -3961,7 +3890,7 @@ dependencies = [ [[package]] name = "openfang-desktop" -version = "0.3.23" +version = "0.3.24" dependencies = [ "axum", "open", @@ -3987,7 +3916,7 @@ dependencies = [ [[package]] name = "openfang-extensions" -version = "0.3.23" +version = "0.3.24" dependencies = [ "aes-gcm", "argon2", @@ -4015,7 +3944,7 @@ dependencies = [ [[package]] name = "openfang-hands" -version = "0.3.23" +version = "0.3.24" dependencies = [ "chrono", "dashmap", @@ -4032,7 +3961,7 @@ dependencies = [ [[package]] name = "openfang-kernel" -version = "0.3.23" +version = "0.3.24" dependencies = [ "async-trait", "chrono", @@ -4068,7 +3997,7 @@ dependencies = [ [[package]] name = "openfang-memory" -version = "0.3.23" +version = "0.3.24" dependencies = [ "async-trait", "chrono", @@ -4087,7 +4016,7 @@ dependencies = [ [[package]] name = "openfang-migrate" -version = "0.3.23" +version = "0.3.24" dependencies = [ "chrono", "dirs 6.0.0", @@ -4106,7 +4035,7 @@ dependencies = [ [[package]] name = "openfang-runtime" -version = "0.3.23" +version = "0.3.24" dependencies = [ "anyhow", "async-trait", @@ -4138,7 +4067,7 @@ dependencies = [ [[package]] name = "openfang-skills" -version = "0.3.23" +version = "0.3.24" dependencies = [ "chrono", "hex", @@ -4161,7 +4090,7 @@ dependencies = [ [[package]] name = "openfang-types" -version = "0.3.23" +version = "0.3.24" dependencies = [ "async-trait", "chrono", @@ -4180,7 +4109,7 @@ dependencies = [ [[package]] name = "openfang-wire" -version = "0.3.23" +version = "0.3.24" dependencies = [ "async-trait", "chrono", @@ -4225,6 +4154,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + [[package]] name = "openssl-probe" version = "0.2.1" @@ -4555,9 +4490,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -4567,9 +4502,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" dependencies = [ "atomic-waker", "fastrand", @@ -4592,6 +4527,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "plist" version = "1.8.0" @@ -4641,7 +4582,7 @@ dependencies = [ "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix 1.1.3", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -4737,11 +4678,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.23.10+spec-1.0.0", + "toml_edit 0.25.4+spec-1.1.0", ] [[package]] @@ -4795,9 +4736,9 @@ dependencies = [ [[package]] name = "pulley-interpreter" -version = "41.0.3" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01051a5b172e07f9197b85060e6583b942aec679dac08416647bf7e7dc916b65" +checksum = "e9812652c1feb63cf39f8780cecac154a32b22b3665806c733cd4072547233a4" dependencies = [ "cranelift-bitset", "log", @@ -4807,9 +4748,9 @@ dependencies = [ [[package]] name = "pulley-macros" -version = "41.0.3" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cf194f5b1a415ef3a44ee35056f4009092cc4038a9f7e3c7c1e392f48ee7dbb" +checksum = "56000349b6896e3d44286eb9c330891237f40b27fd43c1ccc84547d0b463cb40" dependencies = [ "proc-macro2", "quote", @@ -4818,12 +4759,9 @@ dependencies = [ [[package]] name = "pxfm" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" -dependencies = [ - "num-traits", -] +checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" [[package]] name = "quanta" @@ -4870,8 +4808,8 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls", - "socket2 0.6.2", + "rustls 0.23.37", + "socket2 0.6.3", "thiserror 2.0.18", "tokio", "tracing", @@ -4890,7 +4828,7 @@ dependencies = [ "rand 0.9.2", "ring", "rustc-hash", - "rustls", + "rustls 0.23.37", "rustls-pki-types", "slab", "thiserror 2.0.18", @@ -4908,16 +4846,16 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.2", + "socket2 0.6.3", "tracing", "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -4934,6 +4872,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.7.3" @@ -5120,9 +5064,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" dependencies = [ "bitflags 2.11.0", ] @@ -5214,9 +5158,9 @@ checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" @@ -5241,14 +5185,14 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls", + "rustls 0.23.37", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", "tokio-util", "tower", "tower-http", @@ -5281,14 +5225,14 @@ dependencies = [ "log", "percent-encoding", "pin-project-lite", - "rustls", + "rustls 0.23.37", "rustls-pki-types", "rustls-platform-verifier", "serde", "serde_json", "sync_wrapper", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", "tokio-util", "tower", "tower-http", @@ -5357,6 +5301,24 @@ dependencies = [ "serde", ] +[[package]] +name = "rumqttc" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1568e15fab2d546f940ed3a21f48bbbd1c494c90c99c4481339364a497f94a9" +dependencies = [ + "bytes", + "flume", + "futures-util", + "log", + "rustls-native-certs 0.7.3", + "rustls-pemfile", + "rustls-webpki 0.102.8", + "thiserror 1.0.69", + "tokio", + "tokio-rustls 0.25.0", +] + [[package]] name = "rusqlite" version = "0.31.0" @@ -5408,42 +5370,78 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags 2.11.0", "errno", "libc", - "linux-raw-sys 0.11.0", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.36" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "log", + "ring", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.103.9", "subtle", "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" +dependencies = [ + "openssl-probe 0.1.6", + "rustls-pemfile", + "rustls-pki-types", + "schannel", + "security-framework 2.11.1", +] + [[package]] name = "rustls-native-certs" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "openssl-probe", + "openssl-probe 0.2.1", "rustls-pki-types", "schannel", - "security-framework", + "security-framework 3.7.0", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", ] [[package]] @@ -5462,16 +5460,16 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" dependencies = [ - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "jni", "log", "once_cell", - "rustls", - "rustls-native-certs", + "rustls 0.23.37", + "rustls-native-certs 0.8.3", "rustls-platform-verifier-android", - "rustls-webpki", - "security-framework", + "rustls-webpki 0.103.9", + "security-framework 3.7.0", "security-framework-sys", "webpki-root-certs", "windows-sys 0.61.2", @@ -5483,6 +5481,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustls-webpki" version = "0.103.9" @@ -5581,6 +5590,19 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + [[package]] name = "security-framework" version = "3.7.0" @@ -5588,7 +5610,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags 2.11.0", - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -5752,9 +5774,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.16.1" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9" dependencies = [ "base64 0.22.1", "chrono", @@ -5771,9 +5793,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.16.1" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0" dependencies = [ "darling 0.21.3", "proc-macro2", @@ -5980,12 +6002,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -6036,6 +6058,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + [[package]] name = "spinning_top" version = "0.3.0" @@ -6207,23 +6238,22 @@ dependencies = [ [[package]] name = "tao" -version = "0.34.5" +version = "0.34.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" +checksum = "6e06d52c379e63da659a483a958110bbde891695a0ecb53e48cc7786d5eda7bb" dependencies = [ "bitflags 2.11.0", "block2", - "core-foundation", + "core-foundation 0.10.1", "core-graphics", "crossbeam-channel", - "dispatch", + "dispatch2", "dlopen2", "dpi", "gdkwayland-sys", "gdkx11-sys", "gtk", "jni", - "lazy_static", "libc", "log", "ndk", @@ -6235,7 +6265,6 @@ dependencies = [ "once_cell", "parking_lot", "raw-window-handle", - "scopeguard", "tao-macros", "unicode-segmentation", "url", @@ -6281,9 +6310,9 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "tauri" -version = "2.10.2" +version = "2.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "463ae8677aa6d0f063a900b9c41ecd4ac2b7ca82f0b058cc4491540e55b20129" +checksum = "da77cc00fb9028caf5b5d4650f75e31f1ef3693459dfca7f7e506d1ecef0ba2d" dependencies = [ "anyhow", "bytes", @@ -6333,9 +6362,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.5.5" +version = "2.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca7bd893329425df750813e95bd2b643d5369d929438da96d5bbb7cc2c918f74" +checksum = "4bbc990d1dbf57a8e1c7fa2327f2a614d8b757805603c1b9ba5c81bade09fd4d" dependencies = [ "anyhow", "cargo_toml", @@ -6355,9 +6384,9 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "2.5.4" +version = "2.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aac423e5859d9f9ccdd32e3cf6a5866a15bedbf25aa6630bcb2acde9468f6ae3" +checksum = "d4a24476afd977c5d5d169f72425868613d82747916dd29e0a357c84c4bd6d29" dependencies = [ "base64 0.22.1", "brotli", @@ -6382,9 +6411,9 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.5.4" +version = "2.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b6a1bd2861ff0c8766b1d38b32a6a410f6dc6532d4ef534c47cfb2236092f59" +checksum = "d39b349a98dadaffebb73f0a40dcd1f23c999211e5a2e744403db384d0c33de7" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -6396,9 +6425,9 @@ dependencies = [ [[package]] name = "tauri-plugin" -version = "2.5.3" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692a77abd8b8773e107a42ec0e05b767b8d2b7ece76ab36c6c3947e34df9f53f" +checksum = "ddde7d51c907b940fb573006cdda9a642d6a7c8153657e88f8a5c3c9290cd4aa" dependencies = [ "anyhow", "glob", @@ -6552,7 +6581,7 @@ dependencies = [ "osakit", "percent-encoding", "reqwest 0.13.2", - "rustls", + "rustls 0.23.37", "semver", "serde", "serde_json", @@ -6570,9 +6599,9 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.10.0" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b885ffeac82b00f1f6fd292b6e5aabfa7435d537cef57d11e38a489956535651" +checksum = "2826d79a3297ed08cd6ea7f412644ef58e32969504bc4fbd8d7dbeabc4445ea2" dependencies = [ "cookie", "dpi", @@ -6595,9 +6624,9 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.10.0" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5204682391625e867d16584fedc83fc292fb998814c9f7918605c789cd876314" +checksum = "e11ea2e6f801d275fdd890d6c9603736012742a1c33b96d0db788c9cdebf7f9e" dependencies = [ "gtk", "http", @@ -6605,7 +6634,6 @@ dependencies = [ "log", "objc2", "objc2-app-kit", - "objc2-foundation", "once_cell", "percent-encoding", "raw-window-handle", @@ -6622,9 +6650,9 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcd169fccdff05eff2c1033210b9b94acd07a47e6fa9a3431cf09cfd4f01c87e" +checksum = "219a1f983a2af3653f75b5747f76733b0da7ff03069c7a41901a5eb3ace4557d" dependencies = [ "anyhow", "brotli", @@ -6683,14 +6711,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.25.0" +version = "3.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" dependencies = [ "fastrand", - "getrandom 0.4.1", + "getrandom 0.4.2", "once_cell", - "rustix 1.1.3", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -6765,9 +6793,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.47" +version = "0.3.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" dependencies = [ "deranged", "itoa", @@ -6780,15 +6808,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" dependencies = [ "num-conv", "time-core", @@ -6821,9 +6849,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -6831,29 +6859,40 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.2", + "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", "syn 2.0.117", ] +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls 0.22.4", + "rustls-pki-types", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls", + "rustls 0.23.37", "tokio", ] @@ -6887,11 +6926,11 @@ checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" dependencies = [ "futures-util", "log", - "rustls", - "rustls-native-certs", + "rustls 0.23.37", + "rustls-native-certs 0.8.3", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", "tungstenite 0.24.0", ] @@ -6944,7 +6983,7 @@ dependencies = [ "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", - "winnow 0.7.14", + "winnow 0.7.15", ] [[package]] @@ -6965,6 +7004,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_datetime" +version = "1.0.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.19.15" @@ -6991,14 +7039,14 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.10+spec-1.0.0" +version = "0.25.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" dependencies = [ "indexmap 2.13.0", - "toml_datetime 0.7.5+spec-1.1.0", + "toml_datetime 1.0.0+spec-1.1.0", "toml_parser", - "winnow 0.7.14", + "winnow 0.7.15", ] [[package]] @@ -7007,7 +7055,7 @@ version = "1.0.9+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" dependencies = [ - "winnow 0.7.14", + "winnow 0.7.15", ] [[package]] @@ -7183,7 +7231,7 @@ dependencies = [ "httparse", "log", "rand 0.8.5", - "rustls", + "rustls 0.23.37", "rustls-pki-types", "sha1", "thiserror 1.0.69", @@ -7227,13 +7275,13 @@ checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "uds_windows" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +checksum = "51b70b87d15e91f553711b40df3048faf27a7a04e01e0ddc0cf9309f0af7c2ca" dependencies = [ "memoffset", "tempfile", - "winapi", + "windows-sys 0.61.2", ] [[package]] @@ -7391,11 +7439,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.21.0" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" dependencies = [ - "getrandom 0.4.1", + "getrandom 0.4.2", "js-sys", "serde_core", "wasm-bindgen", @@ -7496,9 +7544,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.110" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1de241cdc66a9d91bd84f097039eb140cdc6eec47e0cdbaf9d932a1dd6c35866" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -7509,9 +7557,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.60" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a42e96ea38f49b191e08a1bab66c7ffdba24b06f9995b39a9dd60222e5b6f1da" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", "futures-util", @@ -7523,9 +7571,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.110" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e12fdf6649048f2e3de6d7d5ff3ced779cdedee0e0baffd7dff5cdfa3abc8a52" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7533,9 +7581,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.110" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e63d1795c565ac3462334c1e396fd46dbf481c40f51f5072c310717bc4fb309" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", @@ -7546,9 +7594,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.110" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f9cdac23a5ce71f6bf9f8824898a501e511892791ea2a0c6b8568c68b9cb53" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] @@ -7691,9 +7739,9 @@ dependencies = [ [[package]] name = "wasmtime" -version = "41.0.3" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19f56cece843fa95dd929f5568ff8739c7e3873b530ceea9eda2aa02a0b4142" +checksum = "e2a83182bf04af87571b4c642300479501684f26bab5597f68f68cded5b098fd" dependencies = [ "addr2line", "anyhow", @@ -7718,7 +7766,7 @@ dependencies = [ "postcard", "pulley-interpreter", "rayon", - "rustix 1.1.3", + "rustix 1.1.4", "semver", "serde", "serde_derive", @@ -7748,9 +7796,9 @@ dependencies = [ [[package]] name = "wasmtime-environ" -version = "41.0.3" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bf9dff572c950258548cbbaf39033f68f8dcd0b43b22e80def9fe12d532d3e5" +checksum = "cb201c41aa23a3642365cfb2e4a183573d85127a3c9d528f56b9997c984541ab" dependencies = [ "anyhow", "cpp_demangle", @@ -7775,15 +7823,15 @@ dependencies = [ [[package]] name = "wasmtime-internal-cache" -version = "41.0.3" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f52a985f5b5dae53147fc596f3a313c334e2c24fd1ba708634e1382f6ecd727" +checksum = "fb5b3069d1a67ba5969d0eb1ccd7e141367d4e713f4649aa90356c98e8f19bea" dependencies = [ "base64 0.22.1", "directories-next", "log", "postcard", - "rustix 1.1.3", + "rustix 1.1.4", "serde", "serde_derive", "sha2", @@ -7795,9 +7843,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-component-macro" -version = "41.0.3" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7920dc7dcb608352f5fe93c52582e65075b7643efc5dac3fc717c1645a8d29a0" +checksum = "0c924400db7b6ca996fef1b23beb0f41d5c809836b1ec60fc25b4057e2d25d9b" dependencies = [ "anyhow", "proc-macro2", @@ -7810,15 +7858,15 @@ dependencies = [ [[package]] name = "wasmtime-internal-component-util" -version = "41.0.3" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "066f5aed35aa60580a2ac0df145c0f0d4b04319862fee1d6036693e1cca43a12" +checksum = "7d3f65daf4bf3d74ca2fbbe20af0589c42e2b398a073486451425d94fd4afef4" [[package]] name = "wasmtime-internal-cranelift" -version = "41.0.3" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afb8002dc415b7773d7949ee360c05ee8f91627ec25a7a0b01ee03831bdfdda1" +checksum = "633e889cdae76829738db0114ab3b02fce51ea4a1cd9675a67a65fce92e8b418" dependencies = [ "cfg-if", "cranelift-codegen", @@ -7843,14 +7891,14 @@ dependencies = [ [[package]] name = "wasmtime-internal-fiber" -version = "41.0.3" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9c562c5a272bc9f615d8f0c085a4360bafa28eef9aa5947e63d204b1129b22" +checksum = "deb126adc5d0c72695cfb77260b357f1b81705a0f8fa30b3944e7c2219c17341" dependencies = [ "cc", "cfg-if", "libc", - "rustix 1.1.3", + "rustix 1.1.4", "wasmtime-environ", "wasmtime-internal-versioned-export-macros", "windows-sys 0.61.2", @@ -7858,21 +7906,21 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-debug" -version = "41.0.3" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db673148f26e1211db3913c12c75594be9e3858a71fa297561e9162b1a49cfb0" +checksum = "8e66ff7f90a8002187691ff6237ffd09f954a0ebb9de8b2ff7f5c62632134120" dependencies = [ "cc", "object", - "rustix 1.1.3", + "rustix 1.1.4", "wasmtime-internal-versioned-export-macros", ] [[package]] name = "wasmtime-internal-jit-icache-coherence" -version = "41.0.3" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bada5ca1cc47df7d14100e2254e187c2486b426df813cea2dd2553a7469f7674" +checksum = "4b96df23179ae16d54fb3a420f84ffe4383ec9dd06fad3e5bc782f85f66e8e08" dependencies = [ "anyhow", "cfg-if", @@ -7882,24 +7930,24 @@ dependencies = [ [[package]] name = "wasmtime-internal-math" -version = "41.0.3" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf6f615d528eda9adc6eefb062135f831b5215c348f4c3ec3e143690c730605b" +checksum = "86d1380926682b44c383e9a67f47e7a95e60c6d3fa8c072294dab2c7de6168a0" dependencies = [ "libm", ] [[package]] name = "wasmtime-internal-slab" -version = "41.0.3" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da169d4f789b586e1b2612ba8399c653ed5763edf3e678884ba785bb151d018f" +checksum = "9b63cbea1c0192c7feb7c0dfb35f47166988a3742f29f46b585ef57246c65764" [[package]] name = "wasmtime-internal-unwinder" -version = "41.0.3" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4888301f3393e4e8c75c938cce427293fade300fee3fc8fd466fdf3e54ae068e" +checksum = "f25c392c7e5fb891a7416e3c34cfbd148849271e8c58744fda875dde4bec4d6a" dependencies = [ "cfg-if", "cranelift-codegen", @@ -7910,9 +7958,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-versioned-export-macros" -version = "41.0.3" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63ba3124cc2cbcd362672f9f077303ccc4cd61daa908f73447b7fdaece75ff9f" +checksum = "70f8b9796a3f0451a7b702508b303d654de640271ac80287176de222f187a237" dependencies = [ "proc-macro2", "quote", @@ -7921,9 +7969,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-winch" -version = "41.0.3" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90a4182515dabba776656de4ebd62efad03399e261cf937ecccb838ce8823534" +checksum = "c0063e61f1d0b2c20e9cfc58361a6513d074a23c80b417aac3033724f51648a0" dependencies = [ "cranelift-codegen", "gimli", @@ -7938,9 +7986,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-wit-bindgen" -version = "41.0.3" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87acbd416227cdd279565ba49e57cf7f08d112657c3b3f39b70250acdfd094fe" +checksum = "587699ca7cae16b4a234ffcc834f37e75675933d533809919b52975f5609e2ef" dependencies = [ "anyhow", "bitflags 2.11.0", @@ -7973,9 +8021,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.87" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2c7c5718134e770ee62af3b6b4a84518ec10101aad610c024b64d6ff29bb1ff" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -8122,9 +8170,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "winch-codegen" -version = "41.0.3" +version = "41.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4f31dcfdfaf9d6df9e1124d7c8ee6fc29af5b99b89d11ae731c138e0f5bd77b" +checksum = "c55de3ac5b8bd71e5f6c87a9e511dd3ceb194bdb58183c6a7bf21cd8c0e46fbc" dependencies = [ "anyhow", "cranelift-assembler-x64", @@ -8563,9 +8611,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] @@ -8774,7 +8822,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" dependencies = [ "gethostname", - "rustix 1.1.3", + "rustix 1.1.4", "x11rb-protocol", ] @@ -8791,7 +8839,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix 1.1.3", + "rustix 1.1.4", ] [[package]] @@ -8802,7 +8850,7 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" [[package]] name = "xtask" -version = "0.3.23" +version = "0.3.24" [[package]] name = "yoke" @@ -8849,14 +8897,14 @@ dependencies = [ "hex", "libc", "ordered-stream", - "rustix 1.1.3", + "rustix 1.1.4", "serde", "serde_repr", "tracing", "uds_windows", "uuid", "windows-sys 0.61.2", - "winnow 0.7.14", + "winnow 0.7.15", "zbus_macros", "zbus_names", "zvariant", @@ -8868,7 +8916,7 @@ version = "5.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", "syn 2.0.117", @@ -8884,24 +8932,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" dependencies = [ "serde", - "winnow 0.7.14", + "winnow 0.7.15", "zvariant", ] [[package]] name = "zerocopy" -version = "0.8.39" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.39" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" dependencies = [ "proc-macro2", "quote", @@ -9066,7 +9114,7 @@ dependencies = [ "endi", "enumflags2", "serde", - "winnow 0.7.14", + "winnow 0.7.15", "zvariant_derive", "zvariant_utils", ] @@ -9077,7 +9125,7 @@ version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", "syn 2.0.117", @@ -9094,5 +9142,5 @@ dependencies = [ "quote", "serde", "syn 2.0.117", - "winnow 0.7.14", + "winnow 0.7.15", ] diff --git a/Cargo.toml b/Cargo.toml index e9c080e6a..fa283731f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -134,6 +134,9 @@ imap = "2" native-tls = "0.2" mailparse = "0.15" +# MQTT +rumqttc = "0.24" + # Testing tokio-test = "0.4" tempfile = "3" diff --git a/README.md b/README.md index 746b84277..ba0bd410f 100644 --- a/README.md +++ b/README.md @@ -260,10 +260,40 @@ Connect your agents to every platform your users are on. **Social:** LINE, Viber, Facebook Messenger, Mastodon, Bluesky, Reddit, LinkedIn, Twitch **Community:** IRC, XMPP, Guilded, Revolt, Keybase, Discourse, Gitter **Privacy:** Threema, Nostr, Mumble, Nextcloud Talk, Rocket.Chat, Ntfy, Gotify +**IoT/Edge:** MQTT **Workplace:** Pumble, Flock, Twist, DingTalk, Zalo, Webhooks Each adapter supports per-channel model overrides, DM/group policies, rate limiting, and output formatting. +### MQTT Configuration + +MQTT is ideal for IoT devices, edge computing, and low-bandwidth environments. Configure it in `~/.openfang/config.toml`: + +```toml +[channels.mqtt] +broker_url = "tcp://broker.hivemq.com:1883" # or "ssl://..." for TLS +client_id = "" # empty = auto-generate +subscribe_topic = "openfang/inbox" +publish_topic_prefix = "openfang/reply" +default_agent = "assistant" +qos = 1 # 0=at most once, 1=at least once, 2=exactly once +use_tls = false +keep_alive_secs = 60 + +# Optional authentication +username_env = "MQTT_USERNAME" +password_env = "MQTT_PASSWORD" +``` + +Test with any MQTT client: +```bash +# Send a message +mosquitto_pub -h broker.hivemq.com -t "openfang/inbox" -m "What's the weather?" + +# Subscribe to replies +mosquitto_sub -h broker.hivemq.com -t "openfang/reply/#" +``` + --- ## 27 LLM Providers — 123+ Models diff --git a/crates/openfang-api/src/channel_bridge.rs b/crates/openfang-api/src/channel_bridge.rs index b72180670..d3b1c5a26 100644 --- a/crates/openfang-api/src/channel_bridge.rs +++ b/crates/openfang-api/src/channel_bridge.rs @@ -48,6 +48,7 @@ use openfang_channels::gitter::GitterAdapter; use openfang_channels::gotify::GotifyAdapter; use openfang_channels::linkedin::LinkedInAdapter; use openfang_channels::mumble::MumbleAdapter; +use openfang_channels::mqtt::MqttAdapter; use openfang_channels::ntfy::NtfyAdapter; use openfang_channels::webhook::WebhookAdapter; use openfang_kernel::OpenFangKernel; @@ -733,6 +734,7 @@ impl ChannelBridgeHandle for KernelBridgeAdapter { "gotify" => channels.gotify.as_ref().map(|c| c.overrides.clone()), "webhook" => channels.webhook.as_ref().map(|c| c.overrides.clone()), "linkedin" => channels.linkedin.as_ref().map(|c| c.overrides.clone()), + "mqtt" => channels.mqtt.as_ref().map(|c| c.overrides.clone()), _ => None, } } @@ -1008,7 +1010,8 @@ pub async fn start_channel_bridge_with_config( || config.ntfy.is_some() || config.gotify.is_some() || config.webhook.is_some() - || config.linkedin.is_some(); + || config.linkedin.is_some() + || config.mqtt.is_some(); if !has_any { return (None, Vec::new()); @@ -1518,6 +1521,18 @@ pub async fn start_channel_bridge_with_config( } } + // MQTT + if let Some(ref mqtt_config) = config.mqtt { + if !mqtt_config.broker_url.is_empty() && !mqtt_config.subscribe_topic.is_empty() { + let adapter = Arc::new(MqttAdapter::from_config(mqtt_config.clone())); + adapters.push((adapter, mqtt_config.default_agent.clone())); + info!( + "MQTT adapter configured: {} -> {}", + mqtt_config.broker_url, mqtt_config.subscribe_topic + ); + } + } + if adapters.is_empty() { return (None, Vec::new()); } diff --git a/crates/openfang-channels/Cargo.toml b/crates/openfang-channels/Cargo.toml index 58f0a5df5..23efc2d3a 100644 --- a/crates/openfang-channels/Cargo.toml +++ b/crates/openfang-channels/Cargo.toml @@ -32,5 +32,8 @@ imap = { workspace = true } native-tls = { workspace = true } mailparse = { workspace = true } +# MQTT support +rumqttc = { workspace = true } + [dev-dependencies] tokio-test = { workspace = true } diff --git a/crates/openfang-channels/src/lib.rs b/crates/openfang-channels/src/lib.rs index 978c202e7..5657c2426 100644 --- a/crates/openfang-channels/src/lib.rs +++ b/crates/openfang-channels/src/lib.rs @@ -48,5 +48,6 @@ pub mod gitter; pub mod gotify; pub mod linkedin; pub mod mumble; +pub mod mqtt; pub mod ntfy; pub mod webhook; diff --git a/crates/openfang-channels/src/mqtt.rs b/crates/openfang-channels/src/mqtt.rs new file mode 100644 index 000000000..35881f436 --- /dev/null +++ b/crates/openfang-channels/src/mqtt.rs @@ -0,0 +1,563 @@ +//! MQTT pub/sub channel adapter for the OpenFang channel bridge. +//! +//! MQTT is a lightweight messaging protocol ideal for: +//! - IoT devices and sensors +//! - Low-bandwidth/high-latency networks +//! - Real-time pub/sub messaging +//! - Integration with existing MQTT infrastructure +//! +//! This adapter subscribes to a topic for incoming messages and publishes +//! responses to a reply topic. + +use crate::types::{ + split_message, ChannelAdapter, ChannelContent, ChannelMessage, ChannelType, ChannelUser, +}; +use async_trait::async_trait; +use chrono::Utc; +use futures::Stream; +use openfang_types::config::MqttConfig; +use std::collections::HashMap; +use std::pin::Pin; +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::{mpsc, watch}; +use tracing::{debug, error, info, warn}; +use zeroize::Zeroizing; + +/// Maximum backoff duration on connection failures. +const MAX_BACKOFF: Duration = Duration::from_secs(60); +/// Initial backoff duration on connection failures. +const INITIAL_BACKOFF: Duration = Duration::from_secs(1); +/// Maximum message size for MQTT (most brokers have ~256KB limit). +const MAX_MQTT_MESSAGE_SIZE: usize = 250 * 1024; + +/// MQTT channel adapter using rumqttc. +pub struct MqttAdapter { + config: MqttConfig, + /// SECURITY: Password is zeroized on drop. + username: Option, + /// SECURITY: Password is zeroized on drop. + password: Option>, + shutdown_tx: Arc>, + shutdown_rx: watch::Receiver, +} + +impl MqttAdapter { + /// Create a new MQTT adapter from configuration. + /// + /// Reads credentials from environment variables specified in the config. + pub fn from_config(config: MqttConfig) -> Self { + let (shutdown_tx, shutdown_rx) = watch::channel(false); + + // Read credentials from env (both optional) + let username = if config.username_env.is_empty() { + None + } else { + std::env::var(&config.username_env) + .ok() + .filter(|s| !s.is_empty()) + }; + + let password = if config.password_env.is_empty() { + None + } else { + std::env::var(&config.password_env) + .ok() + .filter(|s| !s.is_empty()) + .map(Zeroizing::new) + }; + + Self { + config, + username, + password, + shutdown_tx: Arc::new(shutdown_tx), + shutdown_rx, + } + } + + /// Generate a unique client ID if not specified. + fn effective_client_id(&self) -> String { + if self.config.client_id.is_empty() { + format!("openfang-{}", uuid::Uuid::new_v4().simple()) + } else { + self.config.client_id.clone() + } + } + + /// Parse broker URL into host and port. + fn parse_broker_url(&self) -> Result<(String, u16), Box> { + let url = self.config.broker_url.trim(); + + // Handle various URL formats + if url.starts_with("tcp://") { + let addr = url.trim_start_matches("tcp://"); + parse_host_port(addr, 1883) + } else if url.starts_with("ssl://") || url.starts_with("mqtts://") { + let addr = url + .trim_start_matches("ssl://") + .trim_start_matches("mqtts://"); + parse_host_port(addr, 8883) + } else if url.starts_with("mqtt://") { + let addr = url.trim_start_matches("mqtt://"); + parse_host_port(addr, 1883) + } else { + // Plain host:port or just host + parse_host_port(url, 1883) + } + } + + /// Build rumqttc MqttOptions from configuration. + fn build_mqtt_options(&self) -> Result> { + let (host, port) = self.parse_broker_url()?; + let client_id = self.effective_client_id(); + + let mut options = rumqttc::MqttOptions::new(client_id, host, port); + options.set_keep_alive(Duration::from_secs(self.config.keep_alive_secs as u64)); + + // Set credentials if provided + if let Some(ref username) = self.username { + if let Some(ref password) = self.password { + options.set_credentials(username, password.as_str()); + } else { + options.set_credentials(username, ""); + } + } + + // Configure TLS if enabled + if self.config.use_tls { + // Use TLS with default configuration (no client auth, no ALPN) + // ca: empty vec means use system's default CA certificates + let transport = rumqttc::Transport::Tls(rumqttc::TlsConfiguration::Simple { + ca: Vec::new(), + alpn: None, + client_auth: None, + }); + options.set_transport(transport); + } + + Ok(options) + } + + /// Convert QoS config value to rumqttc QoS. + fn qos(&self) -> rumqttc::QoS { + match self.config.qos { + 0 => rumqttc::QoS::AtMostOnce, + 1 => rumqttc::QoS::AtLeastOnce, + 2 => rumqttc::QoS::ExactlyOnce, + _ => rumqttc::QoS::AtLeastOnce, // Default to QoS 1 + } + } +} + +/// Parse host:port string with default port fallback. +fn parse_host_port( + s: &str, + default_port: u16, +) -> Result<(String, u16), Box> { + let s = s.trim(); + + // Handle [IPv6]:port format + if s.starts_with('[') { + if let Some(close_bracket) = s.find(']') { + let host = s[1..close_bracket].to_string(); + let rest = &s[close_bracket + 1..]; + let port = if let Some(stripped) = rest.strip_prefix(':') { + stripped.parse()? + } else { + default_port + }; + return Ok((host, port)); + } + } + + // Handle host:port format + if let Some(colon_pos) = s.rfind(':') { + let host = s[..colon_pos].to_string(); + let port = s[colon_pos + 1..].parse()?; + Ok((host, port)) + } else { + Ok((s.to_string(), default_port)) + } +} + +#[async_trait] +impl ChannelAdapter for MqttAdapter { + fn name(&self) -> &str { + "mqtt" + } + + fn channel_type(&self) -> ChannelType { + ChannelType::Custom("mqtt".to_string()) + } + + async fn start( + &self, + ) -> Result + Send>>, Box> + { + let options = self.build_mqtt_options()?; + let subscribe_topic = self.config.subscribe_topic.clone(); + let qos = self.qos(); + + info!( + "MQTT connecting to {} with client_id={}", + self.config.broker_url, + self.effective_client_id() + ); + + let (tx, rx) = mpsc::channel::(256); + + let mut shutdown = self.shutdown_rx.clone(); + let publish_topic_prefix = self.config.publish_topic_prefix.clone(); + + tokio::spawn(async move { + let mut backoff = INITIAL_BACKOFF; + + loop { + // Check shutdown before attempting connection + if *shutdown.borrow() { + info!("MQTT adapter shutting down"); + break; + } + + // Create new client for each connection attempt + let (client, mut eventloop) = rumqttc::AsyncClient::new(options.clone(), 10); + + // Subscribe to the topic + if let Err(e) = client.subscribe(&subscribe_topic, qos).await { + error!("MQTT subscribe failed: {e}"); + tokio::time::sleep(backoff).await; + backoff = (backoff * 2).min(MAX_BACKOFF); + continue; + } + + info!("MQTT subscribed to topic: {}", subscribe_topic); + backoff = INITIAL_BACKOFF; + + // Event loop + loop { + tokio::select! { + // Check for shutdown + _ = shutdown.changed() => { + if *shutdown.borrow() { + let _ = client.disconnect().await; + info!("MQTT disconnected"); + return; + } + } + + // Handle MQTT events + notification = eventloop.poll() => { + match notification { + Ok(rumqttc::Event::Incoming(rumqttc::Incoming::Publish(publish))) => { + // Parse incoming message + if let Some(msg) = parse_mqtt_message(&publish, &publish_topic_prefix) { + debug!("MQTT message from topic {}: {:?}", publish.topic, msg.content); + + if tx.send(msg).await.is_err() { + info!("MQTT receiver dropped, stopping"); + return; + } + } + } + Ok(rumqttc::Event::Incoming(rumqttc::Incoming::Disconnect)) => { + warn!("MQTT broker sent disconnect"); + break; + } + Err(e) => { + error!("MQTT connection error: {e}, reconnecting in {backoff:?}"); + tokio::time::sleep(backoff).await; + backoff = (backoff * 2).min(MAX_BACKOFF); + break; + } + _ => {} + } + } + } + } + } + }); + + let stream = tokio_stream::wrappers::ReceiverStream::new(rx); + Ok(Box::pin(stream)) + } + + async fn send( + &self, + user: &ChannelUser, + content: ChannelContent, + ) -> Result<(), Box> { + // For MQTT, we publish to a topic based on the user's platform_id + // The platform_id in this case is the reply topic + + let reply_topic = if user.platform_id.starts_with("topic:") { + // Direct topic reference + user.platform_id[6..].to_string() + } else { + // Use the publish topic prefix + client identifier + format!("{}/{}", self.config.publish_topic_prefix, user.platform_id) + }; + + let payload = match content { + ChannelContent::Text(text) => { + // Split large messages + let chunks = split_message(&text, MAX_MQTT_MESSAGE_SIZE); + chunks.join("\n---\n") + } + ChannelContent::Image { url, caption } => { + let cap = caption.unwrap_or_default(); + format!("📷 Image: {url}\n{cap}") + } + ChannelContent::File { url, filename } => { + format!("📎 File: {filename}\n{url}") + } + ChannelContent::Voice { + url, + duration_seconds, + } => { + format!("🎙️ Voice ({duration_seconds}s): {url}") + } + ChannelContent::Location { lat, lon } => { + format!("📍 Location: {lat}, {lon}") + } + ChannelContent::Command { name, args } => { + format!("/{name} {}", args.join(" ")) + } + }; + + // Create a temporary client for publishing + let options = self.build_mqtt_options()?; + let (client, mut eventloop) = rumqttc::AsyncClient::new(options, 10); + + // Publish the message + client + .publish(&reply_topic, self.qos(), false, payload.as_bytes()) + .await?; + + // Wait for confirmation (briefly) + let _ = tokio::time::timeout(Duration::from_secs(5), async { + loop { + match eventloop.poll().await { + Ok(rumqttc::Event::Incoming(rumqttc::Incoming::PubAck(_))) => break, + Ok(rumqttc::Event::Incoming(rumqttc::Incoming::PubComp(_))) => break, + Err(_) => break, + _ => {} + } + } + }) + .await; + + Ok(()) + } + + async fn stop(&self) -> Result<(), Box> { + let _ = self.shutdown_tx.send(true); + info!("MQTT adapter stop signal sent"); + Ok(()) + } +} + +/// Parse an MQTT publish message into a ChannelMessage. +fn parse_mqtt_message( + publish: &rumqttc::Publish, + _publish_topic_prefix: &str, +) -> Option { + // Extract payload as string + let payload = String::from_utf8_lossy(&publish.payload); + + if payload.is_empty() { + return None; + } + + // Use the topic as the platform_id (for routing responses) + // Format: topic:{original_topic} so we can reply to the same topic + let platform_id = format!("topic:{}", publish.topic); + + // Try to extract sender info from topic or payload + // Common patterns: + // - device/{device_id}/status -> sender is device_id + // - {prefix}/{client_id}/request -> sender is client_id + let display_name = + extract_sender_from_topic(&publish.topic).unwrap_or_else(|| "mqtt-client".to_string()); + + // Parse payload - could be JSON with metadata or plain text + let (content, metadata) = parse_payload(&payload); + + Some(ChannelMessage { + channel: ChannelType::Custom("mqtt".to_string()), + platform_message_id: format!("{}-{}", publish.pkid, chrono::Utc::now().timestamp_millis()), + sender: ChannelUser { + platform_id, + display_name, + openfang_user: None, + }, + content, + target_agent: None, + timestamp: Utc::now(), + is_group: false, // MQTT is inherently point-to-point + thread_id: Some(publish.topic.clone()), + metadata, + }) +} + +/// Extract sender identifier from MQTT topic path. +fn extract_sender_from_topic(topic: &str) -> Option { + // Try common patterns + let parts: Vec<&str> = topic.split('/').collect(); + + // Pattern: device/{device_id}/... + if parts.len() >= 2 && parts[0] == "device" { + return Some(parts[1].to_string()); + } + + // Pattern: .../client/{client_id} + for i in 0..parts.len().saturating_sub(1) { + if parts[i] == "client" || parts[i] == "user" || parts[i] == "device" { + return Some(parts[i + 1].to_string()); + } + } + + // Use last segment as fallback + parts.last().map(|s| s.to_string()) +} + +/// Parse payload into content and metadata. +fn parse_payload(payload: &str) -> (ChannelContent, HashMap) { + // Try to parse as JSON first + if let Ok(json) = serde_json::from_str::(payload) { + // If it's a JSON object, extract fields + if let Some(obj) = json.as_object() { + // Extract message text + let text = obj + .get("message") + .or_else(|| obj.get("text")) + .or_else(|| obj.get("payload")) + .and_then(|v| v.as_str()) + .unwrap_or(payload); + + // Extract metadata + let metadata: HashMap = obj + .iter() + .filter(|(k, _)| !["message", "text", "payload"].contains(&k.as_str())) + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + + return (ChannelContent::Text(text.to_string()), metadata); + } + } + + // Plain text + (ChannelContent::Text(payload.to_string()), HashMap::new()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_host_port() { + assert_eq!( + parse_host_port("localhost:1883", 1883).unwrap(), + ("localhost".to_string(), 1883) + ); + assert_eq!( + parse_host_port("broker.hivemq.com", 1883).unwrap(), + ("broker.hivemq.com".to_string(), 1883) + ); + assert_eq!( + parse_host_port("192.168.1.1:8883", 1883).unwrap(), + ("192.168.1.1".to_string(), 8883) + ); + } + + #[test] + fn test_parse_host_port_ipv6() { + assert_eq!( + parse_host_port("[::1]:1883", 1883).unwrap(), + ("::1".to_string(), 1883) + ); + assert_eq!( + parse_host_port("[2001:db8::1]:8883", 1883).unwrap(), + ("2001:db8::1".to_string(), 8883) + ); + } + + #[test] + fn test_extract_sender_from_topic() { + assert_eq!( + extract_sender_from_topic("device/sensor-01/status"), + Some("sensor-01".to_string()) + ); + assert_eq!( + extract_sender_from_topic("home/livingroom/temperature"), + Some("temperature".to_string()) + ); + assert_eq!( + extract_sender_from_topic("client/robot-123/command"), + Some("robot-123".to_string()) + ); + } + + #[test] + fn test_parse_payload_plain_text() { + let (content, metadata) = parse_payload("Hello, world!"); + assert!(matches!(content, ChannelContent::Text(ref t) if t == "Hello, world!")); + assert!(metadata.is_empty()); + } + + #[test] + fn test_parse_payload_json() { + let json = r#"{"message": "Hello", "device_id": "sensor-01", "temperature": 25.5}"#; + let (content, metadata) = parse_payload(json); + + assert!(matches!(content, ChannelContent::Text(ref t) if t == "Hello")); + assert_eq!( + metadata.get("device_id").and_then(|v| v.as_str()), + Some("sensor-01") + ); + assert_eq!( + metadata.get("temperature").and_then(|v| v.as_f64()), + Some(25.5) + ); + } + + #[test] + fn test_mqtt_config_default() { + let config = MqttConfig::default(); + assert_eq!(config.broker_url, "tcp://localhost:1883"); + assert_eq!(config.subscribe_topic, "openfang/inbox"); + assert_eq!(config.qos, 1); + assert!(!config.use_tls); + } + + #[test] + fn test_qos_conversion() { + let config = MqttConfig { + qos: 0, + ..Default::default() + }; + let adapter = MqttAdapter::from_config(config); + assert!(matches!(adapter.qos(), rumqttc::QoS::AtMostOnce)); + + let config = MqttConfig { + qos: 1, + ..Default::default() + }; + let adapter = MqttAdapter::from_config(config); + assert!(matches!(adapter.qos(), rumqttc::QoS::AtLeastOnce)); + + let config = MqttConfig { + qos: 2, + ..Default::default() + }; + let adapter = MqttAdapter::from_config(config); + assert!(matches!(adapter.qos(), rumqttc::QoS::ExactlyOnce)); + + let config = MqttConfig { + qos: 99, + ..Default::default() + }; + let adapter = MqttAdapter::from_config(config); + assert!(matches!(adapter.qos(), rumqttc::QoS::AtLeastOnce)); + } +} diff --git a/crates/openfang-types/src/config.rs b/crates/openfang-types/src/config.rs index 617c09bfc..fda05bb67 100644 --- a/crates/openfang-types/src/config.rs +++ b/crates/openfang-types/src/config.rs @@ -1521,6 +1521,8 @@ pub struct ChannelsConfig { pub webhook: Option, /// LinkedIn messaging configuration (None = disabled). pub linkedin: Option, + /// MQTT pub/sub configuration (None = disabled). + pub mqtt: Option, } /// Telegram channel adapter configuration. @@ -2737,6 +2739,58 @@ impl Default for LinkedInConfig { } } +/// MQTT channel adapter configuration. +/// +/// MQTT is a lightweight pub/sub protocol ideal for IoT scenarios, +/// low-bandwidth environments, and real-time messaging. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] +pub struct MqttConfig { + /// MQTT broker URL (e.g., "tcp://localhost:1883" or "ssl://broker.hivemq.com:8883"). + pub broker_url: String, + /// Client identifier (must be unique per connection). + /// If empty, a random ID will be generated. + pub client_id: String, + /// Env var name holding the username (optional, for authentication). + pub username_env: String, + /// Env var name holding the password (optional, for authentication). + pub password_env: String, + /// Topic to subscribe for incoming messages (e.g., "openfang/inbox"). + pub subscribe_topic: String, + /// Topic prefix for outgoing responses (e.g., "openfang/reply"). + /// The full topic will be `{publish_topic_prefix}/{client_id}`. + pub publish_topic_prefix: String, + /// Default agent name to route messages to. + pub default_agent: Option, + /// Quality of Service level (0, 1, or 2). + pub qos: u8, + /// Use TLS/SSL for secure connection. + pub use_tls: bool, + /// Keep alive interval in seconds. + pub keep_alive_secs: u16, + /// Per-channel behavior overrides. + #[serde(default)] + pub overrides: ChannelOverrides, +} + +impl Default for MqttConfig { + fn default() -> Self { + Self { + broker_url: "tcp://localhost:1883".to_string(), + client_id: String::new(), + username_env: "MQTT_USERNAME".to_string(), + password_env: "MQTT_PASSWORD".to_string(), + subscribe_topic: "openfang/inbox".to_string(), + publish_topic_prefix: "openfang/reply".to_string(), + default_agent: None, + qos: 1, + use_tls: false, + keep_alive_secs: 60, + overrides: ChannelOverrides::default(), + } + } +} + impl KernelConfig { /// Validate the configuration, returning a list of warnings. /// @@ -3134,6 +3188,15 @@ impl KernelConfig { )); } } + // MQTT - username/password are optional, only warn if broker_url is empty + if let Some(ref mq) = self.channels.mqtt { + if mq.broker_url.is_empty() { + warnings.push("MQTT configured but broker_url is empty".to_string()); + } + if mq.subscribe_topic.is_empty() { + warnings.push("MQTT configured but subscribe_topic is empty".to_string()); + } + } // Web search provider validation match self.web.search_provider {