diff --git a/Cargo.lock b/Cargo.lock index 36777253..6ae8e4bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -145,6 +145,15 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +[[package]] +name = "ar_archive_writer" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73cd58deff2140a0a8eae87e417bd01db68a33e148aa93d1e8cd837e55e312b6" +dependencies = [ + "object", +] + [[package]] name = "arbitrary" version = "1.4.2" @@ -572,6 +581,15 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -843,7 +861,7 @@ dependencies = [ "futures", "futures-timer", "pin-project-lite", - "reqwest", + "reqwest 0.13.2", "serde", "serde_json", "thiserror 2.0.18", @@ -1050,7 +1068,7 @@ dependencies = [ "async-trait", "futures-util", "inventory", - "reqwest", + "reqwest 0.13.2", "secrecy", "serde", "serde_json", @@ -1146,7 +1164,7 @@ version = "0.1.0" dependencies = [ "async-trait", "inventory", - "reqwest", + "reqwest 0.13.2", "secrecy", "serde", "serde_json", @@ -1184,7 +1202,7 @@ dependencies = [ "chrono", "inventory", "rand 0.8.5", - "reqwest", + "reqwest 0.13.2", "ring", "rustls-pemfile", "secrecy", @@ -1224,7 +1242,7 @@ version = "0.1.0" dependencies = [ "async-trait", "inventory", - "reqwest", + "reqwest 0.13.2", "secrecy", "serde", "serde_json", @@ -1242,7 +1260,7 @@ version = "0.1.0" dependencies = [ "async-trait", "inventory", - "reqwest", + "reqwest 0.13.2", "secrecy", "serde", "serde_json", @@ -1277,7 +1295,7 @@ dependencies = [ "futures-util", "inventory", "rand 0.8.5", - "reqwest", + "reqwest 0.13.2", "secrecy", "serde", "serde_json", @@ -1296,7 +1314,7 @@ dependencies = [ "futures-util", "inventory", "rand 0.8.5", - "reqwest", + "reqwest 0.13.2", "secrecy", "serde", "serde_json", @@ -1315,17 +1333,17 @@ version = "0.1.0" dependencies = [ "async-trait", "inventory", - "reqwest", "secrecy", "serde", "serde_json", "springtale-connector", "springtale-crypto", - "springtale-transport", "subtle", + "teloxide-core", "thiserror 2.0.18", "tokio", "tracing", + "url", ] [[package]] @@ -1784,6 +1802,37 @@ dependencies = [ "uuid", ] +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "der" version = "0.7.10" @@ -1801,6 +1850,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl 1.0.0", ] [[package]] @@ -1809,7 +1868,19 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" dependencies = [ - "derive_more-impl", + "derive_more-impl 2.1.1", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "unicode-xid", ] [[package]] @@ -2029,6 +2100,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "erasable" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "437cfb75878119ed8265685c41a115724eae43fb7cc5a0bf0e4ecc3b803af1c4" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "errno" version = "0.3.14" @@ -2465,7 +2546,7 @@ checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" dependencies = [ "fnv", "hashbrown 0.16.1", - "indexmap", + "indexmap 2.14.0", "stable_deref_trait", ] @@ -2522,7 +2603,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", @@ -2558,6 +2639,12 @@ dependencies = [ "byteorder", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -3001,6 +3088,17 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -3225,6 +3323,59 @@ dependencies = [ "cc", ] +[[package]] +name = "jiff" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link", +] + +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + +[[package]] +name = "jiff-static" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + [[package]] name = "jni" version = "0.21.1" @@ -3870,7 +4021,7 @@ checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" dependencies = [ "crc32fast", "hashbrown 0.17.1", - "indexmap", + "indexmap 2.14.0", "memchr", ] @@ -4075,7 +4226,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset 0.4.2", - "indexmap", + "indexmap 2.14.0", ] [[package]] @@ -4086,7 +4237,7 @@ checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ "fixedbitset 0.5.7", "hashbrown 0.15.5", - "indexmap", + "indexmap 2.14.0", "serde", ] @@ -4274,6 +4425,15 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + [[package]] name = "postcard" version = "1.1.3" @@ -4390,6 +4550,16 @@ dependencies = [ "unarray", ] +[[package]] +name = "psm" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd034599e63b970727f70d79e02d62390a4a84f7c6b827c27c46d5ac3fa622" +dependencies = [ + "ar_archive_writer", + "cc", +] + [[package]] name = "pulley-interpreter" version = "47.0.4" @@ -4704,6 +4874,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rc-box" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897fecc9fac6febd4408f9e935e86df739b0023b625e610e0357535b9c8adad0" +dependencies = [ + "erasable", +] + [[package]] name = "rcgen" version = "0.13.2" @@ -4845,6 +5024,48 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.4.2", + "web-sys", + "webpki-roots 1.0.6", +] + [[package]] name = "reqwest" version = "0.13.2" @@ -4882,10 +5103,19 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", + "wasm-streams 0.5.0", "web-sys", ] +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" +dependencies = [ + "bytemuck", +] + [[package]] name = "ring" version = "0.17.14" @@ -4913,7 +5143,7 @@ dependencies = [ "pastey", "pin-project-lite", "rmcp-macros", - "schemars", + "schemars 1.2.1", "serde", "serde_json", "thiserror 2.0.18", @@ -5201,6 +5431,18 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "schemars" version = "1.2.1" @@ -5310,7 +5552,7 @@ checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" dependencies = [ "bitflags 2.11.0", "cssparser 0.36.0", - "derive_more", + "derive_more 2.1.1", "log", "new_debug_unreachable", "phf 0.13.1", @@ -5329,7 +5571,7 @@ checksum = "8adfa1c298912827b8a28b223b3b874357397ae706e6190acd9bf28cee99114d" dependencies = [ "bitflags 2.11.0", "cssparser 0.37.0", - "derive_more", + "derive_more 2.1.1", "log", "new_debug_unreachable", "phf 0.13.1", @@ -5516,13 +5758,46 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee78f1fbe43ac4a0e47aadb3dbd357b69eb0d3793e948624cd03dd2750ab1c0a" +dependencies = [ + "base64", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "jiff", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8705578779c2b6bd90d84d66eb2e206b708b1a4d7b9f17641b293545bf1c7e46" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "serde_yaml" version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap", + "indexmap 2.14.0", "itoa", "ryu", "serde", @@ -5748,7 +6023,7 @@ dependencies = [ "futures-core", "futures-util", "regex", - "reqwest", + "reqwest 0.13.2", "secrecy", "serde", "serde_json", @@ -5802,7 +6077,7 @@ dependencies = [ "futures-util", "hex", "indicatif", - "reqwest", + "reqwest 0.13.2", "rpassword", "secrecy", "serde", @@ -5837,8 +6112,8 @@ dependencies = [ "garde", "hex", "inventory", - "reqwest", - "schemars", + "reqwest 0.13.2", + "schemars 1.2.1", "secrecy", "serde", "serde_json", @@ -5896,7 +6171,7 @@ dependencies = [ "chrono", "garde", "regex", - "schemars", + "schemars 1.2.1", "serde", "serde_json", "specta", @@ -5921,7 +6196,7 @@ dependencies = [ "libc", "memsec", "rand 0.8.5", - "schemars", + "schemars 1.2.1", "secrecy", "serde", "serde_json", @@ -6097,7 +6372,7 @@ dependencies = [ "hex", "rand 0.8.5", "rcgen", - "reqwest", + "reqwest 0.13.2", "rustls", "rustls-pemfile", "rustls-post-quantum", @@ -6192,6 +6467,19 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "stacker" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.61.2", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -6331,12 +6619,56 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "take_mut" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" + +[[package]] +name = "takecell" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20f34339676cdcab560c9a82300c4c2581f68b9369aedf0fae86f2ff9565ff3e" + [[package]] name = "target-lexicon" version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" +[[package]] +name = "teloxide-core" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7a34ca8e971fa892e633858c07547fe138ef4a02e4a4eaa1d35e517d6e0bc4" +dependencies = [ + "bitflags 2.11.0", + "bytes", + "chrono", + "derive_more 1.0.0", + "either", + "futures", + "log", + "mime", + "once_cell", + "pin-project", + "rc-box", + "reqwest 0.12.28", + "rgb", + "serde", + "serde_json", + "serde_with", + "stacker", + "take_mut", + "takecell", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "url", + "uuid", +] + [[package]] name = "tempfile" version = "3.27.0" @@ -6653,7 +6985,7 @@ version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ - "indexmap", + "indexmap 2.14.0", "serde_core", "serde_spanned 1.1.0", "toml_datetime 0.7.5+spec-1.1.0", @@ -6686,7 +7018,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap", + "indexmap 2.14.0", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", @@ -6699,7 +7031,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap", + "indexmap 2.14.0", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", @@ -7337,7 +7669,7 @@ checksum = "d59b710751a35d54732a63851cdfacbfb2266b7160ce174faf269d3f4e84e31b" dependencies = [ "anyhow", "heck 0.5.0", - "indexmap", + "indexmap 2.14.0", "log", "petgraph 0.6.5", "smallvec", @@ -7383,11 +7715,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap", + "indexmap 2.14.0", "wasm-encoder 0.244.0", "wasmparser 0.244.0", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasm-streams" version = "0.5.0" @@ -7409,7 +7754,7 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags 2.11.0", "hashbrown 0.15.5", - "indexmap", + "indexmap 2.14.0", "semver", ] @@ -7421,7 +7766,7 @@ checksum = "d3eb099dcadcde5be9eef55e3a337128efd4e44b4c93122487e4d2e4e1c6627c" dependencies = [ "bitflags 2.11.0", "hashbrown 0.17.1", - "indexmap", + "indexmap 2.14.0", "semver", "serde", ] @@ -7433,7 +7778,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9a61719f93a87b16d325921e251800c4833f8fab50fa21c7de73aed50086313" dependencies = [ "bitflags 2.11.0", - "indexmap", + "indexmap 2.14.0", "semver", ] @@ -7514,7 +7859,7 @@ dependencies = [ "cranelift-entity", "gimli", "hashbrown 0.17.1", - "indexmap", + "indexmap 2.14.0", "log", "object", "postcard", @@ -7684,7 +8029,7 @@ dependencies = [ "anyhow", "bitflags 2.11.0", "heck 0.5.0", - "indexmap", + "indexmap 2.14.0", "wit-parser 0.252.0", ] @@ -8261,7 +8606,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck 0.5.0", - "indexmap", + "indexmap 2.14.0", "prettyplease", "syn 2.0.117", "wasm-metadata", @@ -8292,7 +8637,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags 2.11.0", - "indexmap", + "indexmap 2.14.0", "log", "serde", "serde_derive", @@ -8311,7 +8656,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap", + "indexmap 2.14.0", "log", "semver", "serde", @@ -8330,7 +8675,7 @@ dependencies = [ "anyhow", "hashbrown 0.17.1", "id-arena", - "indexmap", + "indexmap 2.14.0", "log", "semver", "serde", diff --git a/Cargo.toml b/Cargo.toml index cd04235a..a5ec1c99 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -138,6 +138,8 @@ rust-embed = { version = "8", features = ["interpolate-folder-path"] } # ── HTTP Client ──────────────────────────────────────────────────────────────── reqwest = { version = "0.13", default-features = false, features = ["json", "multipart", "rustls", "stream"] } octocrab = { version = "0.54", default-features = false, features = ["default-client", "follow-redirect", "retry", "rustls", "rustls-ring", "timeout", "jwt-aws-lc-rs"] } # rustls+ring only; `opentls` never enabled. JWT backend is aws-lc-rs (already in graph) because jwt-rust-crypto pulls the banned `rsa` crate +teloxide-core = { version = "0.13", default-features = false, features = ["rustls"] } # Telegram Bot API client; default `native-tls` feature never enabled (plan 5.1) +url = { version = "2", default-features = false } # teloxide-core takes url::Url for api_url / set_webhook / InputFile::url # ── Database (local-first) ───────────────────────────────────────────────────── rusqlite = { version = "0.39", features = ["bundled", "vtab"] } diff --git a/connectors/connector-telegram/Cargo.toml b/connectors/connector-telegram/Cargo.toml index 026aefb1..a633ff23 100644 --- a/connectors/connector-telegram/Cargo.toml +++ b/connectors/connector-telegram/Cargo.toml @@ -14,10 +14,10 @@ thiserror = { workspace = true } tracing = { workspace = true } async-trait = { workspace = true } tokio = { workspace = true } -reqwest = { workspace = true } +teloxide-core = { workspace = true } +url = { workspace = true } subtle = { workspace = true } springtale-connector = { workspace = true } -springtale-transport = { workspace = true } # secret_use helpers — bearer_header, with_str, clone_into_box, secret_eq_str. springtale-crypto = { workspace = true } inventory = { workspace = true } diff --git a/connectors/connector-telegram/src/actions/discover_destinations.rs b/connectors/connector-telegram/src/actions/discover_destinations.rs index 616fa2ce..233fc9a3 100644 --- a/connectors/connector-telegram/src/actions/discover_destinations.rs +++ b/connectors/connector-telegram/src/actions/discover_destinations.rs @@ -199,10 +199,10 @@ mod tests { use super::*; use crate::client::test_helpers::MockTelegramApi; - /// The real `TelegramClient.get_updates` runs the response through - /// `handle_telegram_response`, which unwraps `{ok, result}` and - /// returns the inner `result` value directly. Mirror that — the - /// mock surfaces the unwrapped array straight back to the action. + /// The real `TelegramClient.get_updates` goes through teloxide-core, + /// which unwraps `{ok, result}` and hands back the inner `result` + /// (re-encoded as JSON). Mirror that — the mock surfaces the + /// unwrapped array straight back to the action. fn mock_with_updates(updates: serde_json::Value) -> MockTelegramApi { MockTelegramApi { response: updates } } diff --git a/connectors/connector-telegram/src/client/mod.rs b/connectors/connector-telegram/src/client/mod.rs index 4486e022..8d6cae49 100644 --- a/connectors/connector-telegram/src/client/mod.rs +++ b/connectors/connector-telegram/src/client/mod.rs @@ -1,5 +1,16 @@ +use std::time::Duration; + use async_trait::async_trait; use secrecy::SecretBox; +use serde::Serialize; +use teloxide_core::Bot; +use teloxide_core::payloads::setters::*; +use teloxide_core::requests::Requester; +use teloxide_core::types::{ + AllowedUpdate, CallbackQueryId, ChatId, FileId, InlineKeyboardMarkup, InputFile, MessageId, + ParseMode, Recipient, ReplyParameters, +}; +use url::Url; use crate::error::TelegramError; @@ -75,83 +86,99 @@ pub trait TelegramApi: Send + Sync { ) -> Result; } -/// Telegram Bot API REST client. -/// All network calls to Telegram go through this client. +/// Telegram Bot API client backed by [`teloxide_core::Bot`]. +/// +/// The SDK owns URL building, JSON/multipart encoding, `{ok, result}` +/// unwrapping and `retry_after` parsing. This type only adapts the +/// connector's string-and-JSON [`TelegramApi`] surface onto the SDK's +/// typed requests so the action modules and their mock tests stay +/// untouched. pub struct TelegramClient { - inner: reqwest::Client, - api_base: String, - bot_token: SecretBox, + bot: Bot, } +/// Per-request wall-clock timeout. Must exceed the long-poll timeout +/// (`poll_timeout`, default 30s) or every `getUpdates` call would abort +/// before Telegram answers. Same value the hand-rolled client used. +const REQUEST_TIMEOUT: Duration = Duration::from_secs(60); + impl TelegramClient { pub fn new(api_base: &str, bot_token: SecretBox) -> Result { - let inner = springtale_transport::safe_http::builder() - .timeout(std::time::Duration::from_secs(60)) + let api_url = Url::parse(api_base) + .map_err(|e| TelegramError::InvalidConfig(format!("invalid api_base: {e}")))?; + + // teloxide-core 0.13 pins reqwest 0.12 while the workspace's + // `safe_http` builder is reqwest 0.13, so the two `Client` types are + // distinct and the shared factory cannot be used here. Mirror its + // policy on the SDK's builder instead: rustls only (the `rustls` + // feature is the sole TLS backend enabled), bounded connect and + // request timeouts, no proxy picked up from `TELOXIDE_PROXY`. + let http = teloxide_core::net::default_reqwest_settings() + .timeout(REQUEST_TIMEOUT) .build() .map_err(|e| TelegramError::RequestFailed(format!("failed to build client: {e}")))?; - Ok(Self { - inner, - api_base: api_base.to_owned(), - bot_token, - }) - } + // SECURITY: expose needed once to hand the token to the SDK, which + // places it in the request path per Telegram's Bot API design. + let bot = + springtale_crypto::secret_use::with_str(&bot_token, |tok| Bot::with_client(tok, http)) + .set_api_url(api_url); - /// Build the full Bot API URL for a method. The token sits in the - /// URL path per Telegram's Bot API; exposed only inside the closure. - fn method_url(&self, method: &str) -> String { - springtale_crypto::secret_use::with_str(&self.bot_token, |tok| { - format!("{}/bot{}/{}", self.api_base, tok, method) - }) + Ok(Self { bot }) } } -/// Parse Telegram Bot API response, extracting the "result" field. -/// Telegram returns `{"ok": true, "result": ...}` or `{"ok": false, "description": "..."}`. -async fn handle_telegram_response( - response: reqwest::Response, -) -> Result { - let status = response.status().as_u16(); - let body = response - .text() - .await - .map_err(|e| TelegramError::RequestFailed(format!("failed to read response: {e}")))?; - - let json: serde_json::Value = serde_json::from_str(&body) - .map_err(|e| TelegramError::RequestFailed(format!("invalid JSON: {e}")))?; - - if status == 429 { - let retry_after = json - .get("parameters") - .and_then(|p| p.get("retry_after")) - .and_then(|r| r.as_u64()) - .unwrap_or(5); - return Err(TelegramError::RateLimited { retry_after }); +/// Telegram accepts a numeric chat id or an `@channelusername`. +fn recipient(chat_id: &str) -> Result { + if let Ok(id) = chat_id.parse::() { + return Ok(Recipient::Id(ChatId(id))); } - - if status >= 400 { - let desc = json - .get("description") - .and_then(|d| d.as_str()) - .unwrap_or("unknown error"); - return Err(TelegramError::RequestFailed(format!( - "API returned {status}: {desc}" - ))); + if chat_id.starts_with('@') { + return Ok(Recipient::ChannelUsername(chat_id.to_owned())); } + Err(TelegramError::InvalidInput( + "chat_id must be a numeric id or an @username".to_owned(), + )) +} + +fn message_id(id: i64) -> Result { + i32::try_from(id) + .map(MessageId) + .map_err(|_| TelegramError::InvalidInput(format!("message_id out of range: {id}"))) +} + +/// "Markdown" | "MarkdownV2" | "HTML" — the Bot API's own spellings. +fn parse_mode(mode: &str) -> Result { + serde_json::from_value(serde_json::Value::String(mode.to_owned())) + .map_err(|_| TelegramError::InvalidInput(format!("unknown parse_mode: {mode}"))) +} - // Telegram can return HTTP 200 with ok:false in the JSON body. - let ok = json.get("ok").and_then(|v| v.as_bool()).unwrap_or(false); - if !ok { - let desc = json - .get("description") - .and_then(|d| d.as_str()) - .unwrap_or("unknown error"); - return Err(TelegramError::RequestFailed(format!("API error: {desc}"))); +/// Update-type names as the Bot API spells them (`message`, `callback_query`, ...). +fn allowed_updates(names: &[String]) -> Result, TelegramError> { + names + .iter() + .map(|n| { + serde_json::from_value(serde_json::Value::String(n.clone())) + .map_err(|_| TelegramError::InvalidInput(format!("unknown update type: {n}"))) + }) + .collect() +} + +/// A photo is either an HTTP(S) URL Telegram fetches itself or a +/// previously uploaded `file_id`. +fn photo_input(photo: &str) -> Result { + if photo.starts_with("https://") || photo.starts_with("http://") { + let url = Url::parse(photo) + .map_err(|e| TelegramError::InvalidInput(format!("invalid photo URL: {e}")))?; + return Ok(InputFile::url(url)); } + Ok(InputFile::file_id(FileId(photo.to_owned()))) +} - json.get("result") - .cloned() - .ok_or_else(|| TelegramError::RequestFailed("missing 'result' in response".to_owned())) +/// Re-encode an SDK model as the JSON the action modules already consume. +fn encode(value: &T) -> Result { + serde_json::to_value(value) + .map_err(|e| TelegramError::RequestFailed(format!("failed to encode response: {e}"))) } #[async_trait] @@ -163,24 +190,14 @@ impl TelegramApi for TelegramClient { parse_mode: Option<&str>, reply_to_message_id: Option, ) -> Result { - let mut body = serde_json::json!({ - "chat_id": chat_id, - "text": text, - }); + let mut req = self.bot.send_message(recipient(chat_id)?, text); if let Some(pm) = parse_mode { - body["parse_mode"] = serde_json::Value::String(pm.to_owned()); + req = req.parse_mode(self::parse_mode(pm)?); } if let Some(reply_id) = reply_to_message_id { - body["reply_to_message_id"] = serde_json::Value::Number(reply_id.into()); + req = req.reply_parameters(ReplyParameters::new(message_id(reply_id)?)); } - - let response = self - .inner - .post(self.method_url("sendMessage")) - .json(&body) - .send() - .await?; - handle_telegram_response(response).await + encode(&req.await?) } async fn send_photo( @@ -189,21 +206,13 @@ impl TelegramApi for TelegramClient { photo: &str, caption: Option<&str>, ) -> Result { - let mut body = serde_json::json!({ - "chat_id": chat_id, - "photo": photo, - }); + let mut req = self + .bot + .send_photo(recipient(chat_id)?, photo_input(photo)?); if let Some(cap) = caption { - body["caption"] = serde_json::Value::String(cap.to_owned()); + req = req.caption(cap); } - - let response = self - .inner - .post(self.method_url("sendPhoto")) - .json(&body) - .send() - .await?; - handle_telegram_response(response).await + encode(&req.await?) } async fn edit_message_text( @@ -213,22 +222,13 @@ impl TelegramApi for TelegramClient { text: &str, parse_mode: Option<&str>, ) -> Result { - let mut body = serde_json::json!({ - "chat_id": chat_id, - "message_id": message_id, - "text": text, - }); + let mut req = + self.bot + .edit_message_text(recipient(chat_id)?, self::message_id(message_id)?, text); if let Some(pm) = parse_mode { - body["parse_mode"] = serde_json::Value::String(pm.to_owned()); + req = req.parse_mode(self::parse_mode(pm)?); } - - let response = self - .inner - .post(self.method_url("editMessageText")) - .json(&body) - .send() - .await?; - handle_telegram_response(response).await + encode(&req.await?) } async fn delete_message( @@ -236,18 +236,10 @@ impl TelegramApi for TelegramClient { chat_id: &str, message_id: i64, ) -> Result { - let body = serde_json::json!({ - "chat_id": chat_id, - "message_id": message_id, - }); - - let response = self - .inner - .post(self.method_url("deleteMessage")) - .json(&body) - .send() - .await?; - handle_telegram_response(response).await + let req = self + .bot + .delete_message(recipient(chat_id)?, self::message_id(message_id)?); + encode(&req.await?) } async fn send_inline_keyboard( @@ -256,21 +248,16 @@ impl TelegramApi for TelegramClient { text: &str, inline_keyboard: serde_json::Value, ) -> Result { - let body = serde_json::json!({ - "chat_id": chat_id, - "text": text, - "reply_markup": { - "inline_keyboard": inline_keyboard, - }, - }); - - let response = self - .inner - .post(self.method_url("sendMessage")) - .json(&body) - .send() - .await?; - handle_telegram_response(response).await + let markup: InlineKeyboardMarkup = + serde_json::from_value(serde_json::json!({ "inline_keyboard": inline_keyboard })) + .map_err(|e| { + TelegramError::InvalidInput(format!("invalid inline_keyboard: {e}")) + })?; + let req = self + .bot + .send_message(recipient(chat_id)?, text) + .reply_markup(markup); + encode(&req.await?) } async fn get_updates( @@ -279,24 +266,20 @@ impl TelegramApi for TelegramClient { timeout: u64, allowed_updates: &[String], ) -> Result { - let mut body = serde_json::json!({ - "timeout": timeout, - }); + let timeout = u32::try_from(timeout).map_err(|_| { + TelegramError::InvalidConfig(format!("poll_timeout too large: {timeout}")) + })?; + let mut req = self.bot.get_updates().timeout(timeout); if let Some(off) = offset { - body["offset"] = serde_json::Value::Number(off.into()); + let off = i32::try_from(off).map_err(|_| { + TelegramError::PollingFailed(format!("update offset out of range: {off}")) + })?; + req = req.offset(off); } if !allowed_updates.is_empty() { - body["allowed_updates"] = serde_json::to_value(allowed_updates) - .unwrap_or_else(|_| serde_json::Value::Array(vec![])); + req = req.allowed_updates(self::allowed_updates(allowed_updates)?); } - - let response = self - .inner - .post(self.method_url("getUpdates")) - .json(&body) - .send() - .await?; - handle_telegram_response(response).await + encode(&req.await?) } async fn set_webhook( @@ -305,38 +288,24 @@ impl TelegramApi for TelegramClient { secret_token: Option<&str>, allowed_updates: &[String], ) -> Result { - let mut body = serde_json::json!({ - "url": url, - }); + let url = Url::parse(url) + .map_err(|e| TelegramError::InvalidConfig(format!("invalid webhook_url: {e}")))?; + let mut req = self.bot.set_webhook(url); if let Some(token) = secret_token { - body["secret_token"] = serde_json::Value::String(token.to_owned()); + req = req.secret_token(token); } if !allowed_updates.is_empty() { - body["allowed_updates"] = serde_json::to_value(allowed_updates) - .unwrap_or_else(|_| serde_json::Value::Array(vec![])); + req = req.allowed_updates(self::allowed_updates(allowed_updates)?); } - - let response = self - .inner - .post(self.method_url("setWebhook")) - .json(&body) - .send() - .await?; - handle_telegram_response(response).await + encode(&req.await?) } async fn delete_webhook(&self) -> Result { - let response = self - .inner - .post(self.method_url("deleteWebhook")) - .send() - .await?; - handle_telegram_response(response).await + encode(&self.bot.delete_webhook().await?) } async fn get_me(&self) -> Result { - let response = self.inner.get(self.method_url("getMe")).send().await?; - handle_telegram_response(response).await + encode(&self.bot.get_me().await?) } async fn answer_callback_query( @@ -345,20 +314,14 @@ impl TelegramApi for TelegramClient { text: Option<&str>, show_alert: bool, ) -> Result { - let mut body = serde_json::json!({ - "callback_query_id": callback_query_id, - "show_alert": show_alert, - }); + let mut req = self + .bot + .answer_callback_query(CallbackQueryId(callback_query_id.to_owned())) + .show_alert(show_alert); if let Some(t) = text { - body["text"] = serde_json::Value::String(t.to_owned()); + req = req.text(t); } - let response = self - .inner - .post(self.method_url("answerCallbackQuery")) - .json(&body) - .send() - .await?; - handle_telegram_response(response).await + encode(&req.await?) } } diff --git a/connectors/connector-telegram/src/error.rs b/connectors/connector-telegram/src/error.rs index 1064d1f3..02203d88 100644 --- a/connectors/connector-telegram/src/error.rs +++ b/connectors/connector-telegram/src/error.rs @@ -17,9 +17,6 @@ pub enum TelegramError { #[error("unknown trigger: {0}")] UnknownTrigger(String), - #[error("HTTP error: {0}")] - Reqwest(#[from] reqwest::Error), - #[error("invalid configuration: {0}")] InvalidConfig(String), @@ -33,6 +30,23 @@ pub enum TelegramError { RateLimited { retry_after: u64 }, } +/// Map teloxide-core's request errors onto the connector's typed errors. +/// +/// `RetryAfter` becomes [`TelegramError::RateLimited`] so the polling loop +/// keeps its back-off behaviour. teloxide-core strips the bot token from +/// `Network` errors (`hide_token`) before they reach us, so `Display` is +/// safe to surface. +impl From for TelegramError { + fn from(e: teloxide_core::RequestError) -> Self { + match e { + teloxide_core::RequestError::RetryAfter(secs) => TelegramError::RateLimited { + retry_after: u64::from(secs.seconds()), + }, + other => TelegramError::RequestFailed(other.to_string()), + } + } +} + impl From for springtale_connector::error::ConnectorError { fn from(e: TelegramError) -> Self { springtale_connector::error::ConnectorError::ExecutionFailed(e.to_string())