diff --git a/.gitignore b/.gitignore index d9d2376..1b18f11 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ # already existing elements were commented out #/target -/Cargo.lock .vscode @@ -14,4 +13,4 @@ # ignore vim temp files *~ *.swp -*.swo \ No newline at end of file +*.swo diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..57d68db --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2332 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e1496f8fb1fbf272686b8d37f523dab3e4a7443300055e74cdaa449f3114356" + +[[package]] +name = "async-compression" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fec134f64e2bc57411226dfc4e52dec859ddfc7e711fc5e07b612584f000e4aa" +dependencies = [ + "flate2", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "async-trait" +version = "0.1.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "axum" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +dependencies = [ + "async-trait", + "axum-core", + "bitflags 1.3.2", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" + +[[package]] +name = "cc" +version = "1.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets 0.52.6", +] + +[[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-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.77", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + +[[package]] +name = "firebase-messaging-rs" +version = "0.8.5" +dependencies = [ + "async-trait", + "chrono", + "gcloud-sdk", + "http", + "hyper", + "hyper-rustls 0.25.0", + "hyper-tls", + "log", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "flate2" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "gcemeta" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d460327b24cc34c86d53d60a90e9e6044817f7906ebd9baa5c3d0ee13e1ecf" +dependencies = [ + "bytes", + "hyper", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "gcloud-sdk" +version = "0.24.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1afe2a62202f8f8eb624638f7e5b8f0988a540dd8dbb69e098daeb277273b2ab" +dependencies = [ + "async-trait", + "bytes", + "chrono", + "futures", + "gcemeta", + "hyper", + "jsonwebtoken", + "once_cell", + "prost 0.12.6", + "prost-types 0.12.6", + "reqwest", + "secret-vault-value", + "serde", + "serde_json", + "serde_with", + "tokio", + "tonic", + "tower", + "tower-layer", + "tower-util", + "tracing", + "url", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 2.5.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http", + "hyper", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "399c78f9338483cb7e630c8474b07268983c6bd5acee012e4211f9f7bb21b070" +dependencies = [ + "futures-util", + "http", + "hyper", + "log", + "rustls 0.22.4", + "rustls-native-certs 0.7.3", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.25.0", +] + +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper", + "pin-project-lite", + "tokio", + "tokio-io-timeout", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[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.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +dependencies = [ + "equivalent", + "hashbrown 0.14.5", + "serde", +] + +[[package]] +name = "ipnet" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "9.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" +dependencies = [ + "base64 0.21.7", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "libc" +version = "0.2.158" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "openssl" +version = "0.10.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-src" +version = "300.3.2+3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a211a18d945ef7e648cc6e0058f4c548ee46aab922ea203e0d30e966ea23647b" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64 0.22.1", + "serde", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ef0f924a5ee7ea9cbcea77529dba45f8a9ba9f622419fe3386ca581a3ae9d5a" +dependencies = [ + "pin-project-internal 0.4.30", +] + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal 1.1.5", +] + +[[package]] +name = "pin-project-internal" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "851c8d0ce9bebe43790dedfc86614c23494ac9f423dd618d3a61fc693eafe61e" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" +dependencies = [ + "bytes", + "prost-derive 0.12.6", +] + +[[package]] +name = "prost" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2ecbe40f08db5c006b5764a2645f7f3f141ce756412ac9e1dd6087e6d32995" +dependencies = [ + "bytes", + "prost-derive 0.13.2", +] + +[[package]] +name = "prost-derive" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" +dependencies = [ + "anyhow", + "itertools 0.12.1", + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "prost-derive" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acf0c195eebb4af52c752bec4f52f645da98b6e92077a04110c7f349477ae5ac" +dependencies = [ + "anyhow", + "itertools 0.13.0", + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "prost-types" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" +dependencies = [ + "prost 0.12.6", +] + +[[package]] +name = "prost-types" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60caa6738c7369b940c3d49246a8d1749323674c65cb13010134f5c9bad5b519" +dependencies = [ + "prost 0.13.2", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "async-compression", + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls 0.24.2", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls 0.21.12", + "rustls-native-certs 0.6.3", + "rustls-pemfile 1.0.4", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-rustls 0.24.1", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "winreg", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustix" +version = "0.38.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "log", + "ring", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile 1.0.4", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-native-certs" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" +dependencies = [ + "openssl-probe", + "rustls-pemfile 2.1.3", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-pemfile" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" +dependencies = [ + "base64 0.22.1", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[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 = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "schannel" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9aaafd5a2b6e3d657ff009d82fbd630b6bd54dd4eb06f21693925cdf80f9b8b" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "secret-vault-value" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc32a777b53b3433b974c9c26b6d502a50037f8da94e46cb8ce2ced2cfdfaea0" +dependencies = [ + "prost 0.13.2", + "prost-types 0.13.2", + "serde", + "serde_json", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "serde_json" +version = "1.0.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.5.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-io-timeout" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + +[[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-stream" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tonic" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76c4eb7a4e9ef9d4763600161f12f5070b92a578e1b634db88a6887844c91a13" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64 0.21.7", + "bytes", + "h2", + "http", + "http-body", + "hyper", + "hyper-timeout", + "percent-encoding", + "pin-project 1.1.5", + "prost 0.12.6", + "rustls-native-certs 0.7.3", + "rustls-pemfile 2.1.3", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.25.0", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project 1.1.5", + "pin-project-lite", + "rand", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tower-util" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1093c19826d33807c72511e68f73b4a0469a3f22c2bd5f7d5212178b4b89674" +dependencies = [ + "futures-core", + "futures-util", + "pin-project 0.4.30", + "tower-service", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.77", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" + +[[package]] +name = "wasm-streams" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] diff --git a/Cargo.toml b/Cargo.toml index 9176413..9d68328 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "firebase-messaging-rs" -version = "0.8.4" +version = "0.8.5" authors = [ "Yoichiro ITO " ] @@ -17,19 +17,20 @@ readme = "README.md" [badges] maintenance = { status = "actively-developed" } - [features] -default = ["native-tls"] +default = ["topic-management", "fcm", "native-tls"] +topic-management = [] +fcm = ["serde_json"] native-tls = ["hyper-tls"] rustls = ["hyper-rustls"] vendored-tls = ["hyper-tls/vendored"] [dependencies] serde = { version = "1", features = ["derive"] } -serde_json = "1" +serde_json = { version = "1", optional = true } chrono = "0.4" log = "0.4" -gcloud-sdk = {version = "0.24", features = ["rest"]} +gcloud-sdk = { version = "0.24", features = ["rest"] } hyper = { version = "0.14", features = ["client", "http1"] } hyper-tls = { version = "0.5", optional = true } hyper-rustls = { version = "0.25", optional = true, features = ["rustls-native-certs"] } diff --git a/README.md b/README.md index 7f98531..f9332c6 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ You can choose tls backend from native-tls or rustls. ```toml firebase-messaging-rs = {git = "ssh://git@github.com/i10416/firebase-messaging-rs.git", branch = "main", version = "0.4", features = ["rustls"] } -# wip: firebase-messaging-rs = { version = "", features = ["rustls"] } +# firebase-messaging-rs = { version = "", features = ["rustls"] } ``` ## required GCP roles @@ -51,7 +51,11 @@ If you need fine-grained permissions, see the table bellow and grant required ro ```rust use firebase_messaging_rs::FCMClient; -use firebase_messaging_rs::topic::TopicManagementSupport; +use firebase_messaging_rs::fcm::*; +use firebase_messaging_rs::topic::*; +use firebase_messaging_rs::fcm::android::*; +use firebase_messaging_rs::fcm::ios::*; +use firebase_messaging_rs::fcm::webpush::*; // you need to have application_default_credentials.json at $HOME/.config/gcloud directory // or export GOOGLE_APPLICATION_CREDENTIALS env to authenticate to Firebase. @@ -89,7 +93,37 @@ let res = client.unregister_token_from_topic( ).await.unwrap(); // => Ok(TopicManagementResponse { results: [{}] }) - +let message = Message::Topic { + topic: "example".to_string(), + fcm_options: Some(FcmOptions::new("example")) + notification: Some(Notification { + title: Some("example".to_string()), + body: Some("example".to_string()), + ..Default::default() + }), + android: Some(AndroidConfig { + priority: Some(AndroidMessagePriority::High), + ..Default::default() + }), + webpush: None, + apns: Some(ApnsConfig::new( + &Aps { + content_available: Some(ContentAvailable::On), + ..Default::default() + }, + HashMap::from_iter([ + ("foo".to_string(),"bar".to_string()) + ]), + Some( + ApnsHeaders { + apns_push_type: Some(ApnsPushType::Alert), + ..Default::default() + } + ) + )) +} +let res = client.validate(&message).await +// => Ok(MessageOutput { name: "projects/{project-id}/messages/{id}" }) ``` diff --git a/src/fcm.rs b/src/fcm.rs new file mode 100644 index 0000000..89db31f --- /dev/null +++ b/src/fcm.rs @@ -0,0 +1,234 @@ +use std::{collections::HashMap, time::Duration}; + +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; + +pub mod android; +pub mod ios; +pub mod webpush; +use crate::{GenericGoogleRestAPISupport, RPCError}; + +use android::AndroidConfig; +use ios::ApnsConfig; +use webpush::WebPushConfig; + +#[async_trait] + +pub trait FCMApi: GenericGoogleRestAPISupport { + fn post_endpoint(project_id: &str) -> String { + format!("https://fcm.googleapis.com/v1/projects/{project_id}/messages:send") + } + async fn send(&self, message: &Message) -> Result { + let payload = MessagePayload { + validate_only: false, + message, + }; + self.post_request(&Self::post_endpoint(&self.project_id()), &payload) + .await + } + async fn validate(&self, message: &Message) -> Result { + let payload = MessagePayload { + validate_only: true, + message, + }; + self.post_request(&Self::post_endpoint(&self.project_id()), &payload) + .await + } +} + +#[derive(Debug, Serialize)] +pub struct MessagePayload<'a> { + validate_only: bool, + message: &'a Message, +} + +#[derive(Debug, Deserialize, Clone)] +pub enum FCMError { + InternalRequestError { reason: String }, + InternalResponseError { reason: String }, + Unauthorized(String), + InvalidRequestDescriptive { reason: String }, + InvalidRequest, + RetryableInternal { retry_after: Duration }, + Internal, + Unknown { code: u16, hint: Option }, +} + +impl From for FCMError { + fn from(value: RPCError) -> Self { + match value { + RPCError::BuildRequestFailure(reason) => Self::InternalRequestError { reason }, + RPCError::Unauthorized(reason) => Self::Unauthorized(reason), + RPCError::HttpRequestFailure => Self::InternalRequestError { + reason: "unable to process http request".to_string(), + }, + RPCError::DecodeFailure => Self::InternalResponseError { + reason: "unable to decode response body bytes".to_string(), + }, + RPCError::DeserializeFailure { reason, source } => Self::InternalResponseError { + reason: format!("unable to deserialize response body to type: {reason}: {source}"), + }, + RPCError::InvalidRequest { + details: Some(details), + } => Self::InvalidRequestDescriptive { reason: details }, + RPCError::InvalidRequest { details: None } => Self::InvalidRequest, + RPCError::Internal { + retry_after: Some(retry_after), + } => Self::RetryableInternal { retry_after }, + RPCError::Internal { retry_after: None } => Self::Internal, + RPCError::Unknown(code) => Self::Unknown { code, hint: None }, + } + } +} +/// Low-level type representing FCM Message type. +/// See https://fcm.googleapis.com/$discovery/rest?version=v1 for details. +#[derive(Debug, Serialize)] +#[serde(untagged)] +pub enum Message { + Token { + #[serde(skip_serializing_if = "Option::is_none")] + name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + data: Option>, + /// Registration token to send a message to. + token: String, + /// Template for FCM SDK feature options to use across all platforms. + #[serde(skip_serializing_if = "Option::is_none")] + fcm_options: Option, + /// Basic notification template to use across all platforms. + #[serde(skip_serializing_if = "Option::is_none")] + notification: Option, + /// Android specific options for messages sent through [FCM connection server](https://goo.gl/4GLdUl). + #[serde(skip_serializing_if = "Option::is_none")] + android: Option, + /// [Webpush protocol](https://tools.ietf.org/html/rfc8030) options. + #[serde(skip_serializing_if = "Option::is_none")] + webpush: Option, + + /// [Apple Push Notification Service](https://goo.gl/MXRTPa) specific options. + #[serde(skip_serializing_if = "Option::is_none")] + apns: Option, + }, + Topic { + /// Topic name to send a message to, e.g. "weather". Note: "/topics/" prefix should not be provided. + topic: String, + /// Template for FCM SDK feature options to use across all platforms. + #[serde(skip_serializing_if = "Option::is_none")] + fcm_options: Option, + /// Basic notification template to use across all platforms. + #[serde(skip_serializing_if = "Option::is_none")] + notification: Option, + /// Android specific options for messages sent through [FCM connection server](https://goo.gl/4GLdUl). + #[serde(skip_serializing_if = "Option::is_none")] + android: Option, + + /// [Webpush protocol](https://tools.ietf.org/html/rfc8030) options. + #[serde(skip_serializing_if = "Option::is_none")] + webpush: Option, + + /// [Apple Push Notification Service](https://goo.gl/MXRTPa) specific options. + #[serde(skip_serializing_if = "Option::is_none")] + apns: Option, + }, + Condition { + /// "Condition to send a message to, e.g. "'foo' in topics && 'bar' in topics". + condition: String, + /// Template for FCM SDK feature options to use across all platforms. + #[serde(skip_serializing_if = "Option::is_none")] + fcm_options: Option, + /// Basic notification template to use across all platforms. + #[serde(skip_serializing_if = "Option::is_none")] + notification: Option, + /// Android specific options for messages sent through [FCM connection server](https://goo.gl/4GLdUl). + #[serde(skip_serializing_if = "Option::is_none")] + android: Option, + /// [Webpush protocol](https://tools.ietf.org/html/rfc8030) options. + #[serde(skip_serializing_if = "Option::is_none")] + webpush: Option, + + /// [Apple Push Notification Service](https://goo.gl/MXRTPa) specific options. + #[serde(skip_serializing_if = "Option::is_none")] + apns: Option, + }, +} + +#[derive(Debug, Serialize, Default)] +pub struct FcmOptions { + /// Label associated with the message's analytics data. + #[serde(skip_serializing_if = "Option::is_none")] + analytics_label: Option, +} +impl FcmOptions { + pub fn new(analytics_label: &str) -> Self { + Self { + analytics_label: Some(analytics_label.to_string()), + } + } +} + +#[derive(Debug, Serialize, Default)] +pub struct Notification { + /// The notification title. + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + /// The notification's body text. + #[serde(skip_serializing_if = "Option::is_none")] + pub body: Option, + /// Contains the URL of an image that is going to be downloaded on the device + /// and displayed in a notification. JPEG, PNG, BMP have full support across platforms. + /// Animated GIF and video only work on iOS. WebP and HEIF have varying levels of + /// support across platforms and platform versions. Android has 1MB image size limit. + /// Quota usage and implications/costs for hosting image on Firebase Storage: https://firebase.google.com/pricing + #[serde(skip_serializing_if = "Option::is_none")] + pub image: Option, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct MessageOutput { + /// "Output Only. The identifier of the message sent, in the format of `projects/*/messages/{message_id}`." + pub name: String, +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use super::{Message, Notification}; + use crate::fcm::ApnsConfig; + #[test] + pub fn ios_background_notification() { + let background_notification = Message::Topic { + topic: "background_channel".to_string(), + fcm_options: None, + notification: Some(Notification { + title: Some("example".to_string()), + ..Default::default() + }), + android: None, + webpush: None, + apns: Some(ApnsConfig::ios_background_notification(HashMap::from_iter( + [("message".to_string(), "Hello, World!".to_string())], + ))), + }; + let result = serde_json::to_value(&background_notification).expect("should always succeed"); + let expected = serde_json::json!({ + "topic": "background_channel", + "notification": { + "title": "example" + }, + "apns": { + "payload": { + "aps": { + "content-available": 1 + }, + "message": "Hello, World!" + }, + "headers": { + "apns-push-type": "background", + "apns-priority": "5" + } + } + }); + assert_eq!(result, expected) + } +} diff --git a/src/fcm/android.rs b/src/fcm/android.rs new file mode 100644 index 0000000..1f2238d --- /dev/null +++ b/src/fcm/android.rs @@ -0,0 +1,420 @@ +use std::collections::HashMap; + +use serde::Serialize; +/// In JSON format, the Duration type is encoded as a string rather than an object, +/// where the string ends in the suffix "s" (indicating seconds) and is preceded by +/// the number of seconds, with nanoseconds expressed as fractional seconds. +/// For example, 3 seconds with 0 nanoseconds should be encoded in JSON format as "3s", +/// while 3 seconds and 1 nanosecond should be expressed in JSON format as "3.000000001s". +/// Resolution defined by [proto.Duration](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#google.protobuf.Duration) +#[derive(Debug, PartialEq, Clone, Copy)] +pub struct Duration(f32); +impl Duration { + pub fn from_secs(secs: f32) -> Self { + Self(secs) + } +} +impl From for Duration { + fn from(value: f32) -> Self { + Self(value) + } +} +impl Serialize for Duration { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + format!("{}s", self.0).serialize(serializer) + } +} + +/// Android specific options for messages sent through [FCM connection server](https://goo.gl/4GLdUl). +#[derive(Debug, Serialize, Default)] +pub struct AndroidConfig { + #[serde(skip_serializing_if = "Option::is_none")] + /// Options for features provided by the FCM SDK for Android. + pub fcm_options: Option, + + /// Message priority. Can take "normal" and "high" values. For more information, see [Setting the priority of a message](https://goo.gl/GjONJv). + #[serde(skip_serializing_if = "Option::is_none")] + pub priority: Option, + + /// Notification to send to android devices. + #[serde(skip_serializing_if = "Option::is_none")] + pub notification: Option, + + /// Arbitrary key/value payload. If present, it will override google.firebase.fcm.v1.Message.data. + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option>, + + /// Package name of the application where the registration token must match in order to receive the message. + #[serde(skip_serializing_if = "Option::is_none")] + pub restricted_package_name: Option, + + /// How long (in seconds) the message should be kept in FCM storage if the device is offline. + /// The maximum time to live supported is 4 weeks, and the default value is 4 weeks if not set. + /// Set it to 0 if want to send the message immediately. In JSON format, the Duration type is + /// encoded as a string rather than an object, where the string ends in the suffix "s" (indicating seconds) + /// and is preceded by the number of seconds, with nanoseconds expressed as fractional seconds. + /// For example, 3 seconds with 0 nanoseconds should be encoded in JSON format as "3s", + /// while 3 seconds and 1 nanosecond should be expressed in JSON format as "3.000000001s". + /// The ttl will be rounded down to the nearest second. + #[serde(skip_serializing_if = "Option::is_none")] + pub ttl: Option, + + /// If set to true, messages will be allowed to be delivered to the app while the device is in direct boot mode. See [Support Direct Boot mode](https://developer.android.com/training/articles/direct-boot). + #[serde(skip_serializing_if = "Option::is_none")] + pub direct_boot_ok: Option, + + /// An identifier of a group of messages that can be collapsed, so that only the last message gets sent when delivery can be resumed. A maximum of 4 different collapse keys is allowed at any given time. + #[serde(skip_serializing_if = "Option::is_none")] + pub collapse_key: Option, +} + +/// Notification to send to android devices. +#[derive(Debug, Serialize, Default)] +pub struct AndroidNotification { + /// Set whether or not this notification is relevant only to the current device. + /// Some notifications can be bridged to other devices for remote display, + /// such as a Wear OS watch. This hint can be set to recommend this notification + /// not be bridged. + /// See [Wear OS guides](https://developer.android.com/training/wearables/notifications/bridger#existing-method-of-preventing-bridging) + #[serde(skip_serializing_if = "Option::is_none")] + pub local_only: Option, + + /// If set to true, use the Android framework's default LED light settings for the + /// notification. Default values are specified in + /// [config.xml](https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml). + /// If `default_light_settings` is set to true and `light_settings` is also set, the user-specified + /// `light_settings` is used instead of the default value. + #[serde(skip_serializing_if = "Option::is_none")] + pub default_light_settings: Option, + + /// If set to true, use the Android framework's default sound for the notification. + /// Default values are specified in + /// [config.xml](https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml). + #[serde(skip_serializing_if = "Option::is_none")] + pub default_sound: Option, + + /// Contains the URL of an image that is going to be displayed in a notification. + /// If present, it will override google.firebase.fcm.v1.Notification.image. + #[serde(skip_serializing_if = "Option::is_none")] + pub image: Option, + + /// Identifier used to replace existing notifications in the notification drawer. + /// If not specified, each request creates a new notification. + /// If specified and a notification with the same tag is already being shown, the new notification + /// replaces the existing one in the notification drawer. + #[serde(skip_serializing_if = "Option::is_none")] + pub tag: Option, + + /// If set to true, use the Android framework's default vibrate pattern for the notification. + /// Default values are specified in + /// [config.xml](https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml). + /// If `default_vibrate_timings` is set to true and `vibrate_timings` is also set, + /// the default value is used instead of the user-specified `vibrate_timings`. + #[serde(skip_serializing_if = "Option::is_none")] + pub default_vibrate_timings: Option, + + /// Sets the number of items this notification represents. + /// May be displayed as a badge count for launchers that support badging. + /// See [Notification Badge](https://developer.android.com/training/notify-user/badges). + /// For example, this might be useful if you're using just one notification to represent + /// multiple new messages but you want the count here to represent the number of total new messages. + /// If zero or unspecified, systems that support badging use the default, which is to increment a number + /// displayed on the long-press menu each time a new notification arrives. + #[serde(skip_serializing_if = "Option::is_none")] + pub notification_count: Option, + + /// The key to the title string in the app's string resources to use + /// to localize the title text to the user's current localization. + /// See [String Resources](https://goo.gl/NdFZGI) for more information. + #[serde(skip_serializing_if = "Option::is_none")] + pub title_loc_key: Option, + + /// If set, display notifications delivered to the device will be + /// handled by the app instead of the proxy. + #[serde(skip_serializing_if = "Option::is_none")] + #[deprecated(since = "0.8.4")] + pub bypass_proxy_notification: Option, + + /// The action associated with a user click on the notification. + /// If specified, an activity with a matching intent filter is + /// launched when a user clicks on the notification. + #[serde(skip_serializing_if = "Option::is_none")] + pub click_action: Option, + + /// The sound to play when the device receives the notification. + /// Supports "default" or the filename of a sound resource bundled + /// in the app. Sound files must reside in /res/raw/. + #[serde(skip_serializing_if = "Option::is_none")] + pub sound: Option, + + /// Set the time that the event in the notification occurred. + /// Notifications in the panel are sorted by this time. + /// A point in time is represented using + /// [protobuf.Timestamp](https://developers.google.com/protocol-buffers/docs/reference/java/com/google/protobuf/Timestamp). + /// + /// Example: "2014-10-02T15:01:23Z", "2014-10-02T15:01:23.045123456Z" + #[serde(skip_serializing_if = "Option::is_none")] + pub event_time: Option, + + /// The notification's title. If present, it will override + /// google.firebase.fcm.v1.Notification.title. + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + + /// Set the vibration pattern to use. Pass in an array of + /// [protobuf.Duration](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#google.protobuf.Duration) + /// to turn on or off the vibrator. + /// The first value indicates the `Duration` to wait before turning the vibrator on. + /// The next value indicates the `Duration` to keep the vibrator on. + /// Subsequent values alternate between `Duration` to turn the vibrator off and to turn the vibrator on. + /// If `vibrate_timings` is set and `default_vibrate_timings` is set to `true`, + /// the default value is used instead of the user-specified `vibrate_timings`. + #[serde(skip_serializing_if = "Option::is_none")] + pub vibrate_timings: Option>, + + /// The key to the body string in the app's string resources to use + /// to localize the body text to the user's current localization. + /// See [String Resources](https://goo.gl/NdFZGI) for more information. + #[serde(skip_serializing_if = "Option::is_none")] + pub body_loc_key: Option, + + /// The notification's body text. If present, it will override + /// google.firebase.fcm.v1.Notification.body. + #[serde(skip_serializing_if = "Option::is_none")] + pub body: Option, + + /// The notification's icon. Sets the notification icon to myicon + /// for drawable resource myicon. If you don't send this key in the request, + /// FCM displays the launcher icon specified in your app manifest. + #[serde(skip_serializing_if = "Option::is_none")] + pub icon: Option, + + /// Variable string values to be used in place of the format + /// specifiers in title_loc_key to use to localize the title text to + /// the user's current localization. + /// See [Formatting and Styling](https://goo.gl/MalYE3) for more information. + #[serde(skip_serializing_if = "Option::is_none")] + pub title_loc_args: Option>, + + /// The notification's icon color, expressed in #rrggbb format. + #[serde(skip_serializing_if = "Option::is_none")] + pub color: Option, + + /// Variable string values to be used in place of the format + /// specifiers in body_loc_key to use to localize the body text + /// to the user's current localization. + /// See [Formatting and Styling](https://goo.gl/MalYE3) for more information. + #[serde(skip_serializing_if = "Option::is_none")] + pub body_loc_args: Option>, + + /// When set to false or unset, the notification is + /// automatically dismissed when the user clicks it in the panel. + /// When set to true, the notification persists even when the user clicks it. + #[serde(skip_serializing_if = "Option::is_none")] + pub sticky: Option, + + /// Setting to control when a notification may be proxied. + #[serde(skip_serializing_if = "Option::is_none")] + pub proxy: Option, + + /// Sets the "ticker" text, which is sent to accessibility services. + /// Prior to API level 21 (`Lollipop`), sets the text that is displayed + /// in the status bar when the notification first arrives. + #[serde(skip_serializing_if = "Option::is_none")] + pub ticker: Option, + + /// Set the relative priority for this notification. + /// Priority is an indication of how much of the user's attention + /// should be consumed by this notification. + /// Low-priority notifications may be hidden from the user in certain situations, + /// while the user might be interrupted for a higher-priority notification. + /// The effect of setting the same priorities may differ slightly on different platforms. + /// Note this priority differs from `AndroidMessagePriority`. + /// This priority is processed by the client after the message has been delivered, whereas [AndroidMessagePriority](https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#androidmessagepriority) is an FCM concept that controls when the message is delivered. + #[serde(skip_serializing_if = "Option::is_none")] + pub notification_priority: Option, + + /// Set the [Notification.visibility](https://developer.android.com/reference/android/app/Notification.html#visibility) + /// of the notification. + #[serde(skip_serializing_if = "Option::is_none")] + pub visibility: Option, + + /// The [notification's channel id](https://developer.android.com/guide/topics/ui/notifiers/notifications#ManageChannels)(new in Android O). + /// The app must create a channel with this channel ID before any notification with this channel ID is received. + /// If you don't send this channel ID in the request, or if the channel ID provided has not yet been created by the app, + /// FCM uses the channel ID specified in the app manifest. + #[serde(skip_serializing_if = "Option::is_none")] + pub channel_id: Option, + + /// Settings to control the notification's LED blinking rate and color if LED is available on the device. + /// The total blinking time is controlled by the OS. + #[serde(skip_serializing_if = "Option::is_none")] + pub light_settings: Option, +} + +/// Settings to control notification LED. +#[derive(Debug, Serialize, Default)] +pub struct LightSettings { + pub color: Color, + /// Along with `light_off_duration`, define the blink rate of LED flashes. + /// Resolution defined by [proto.Duration](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#google.protobuf.Duration) + #[serde(skip_serializing_if = "Option::is_none")] + pub light_on_duration: Option, + /// Along with `light_on_duration `, define the blink rate of LED flashes. + /// Resolution defined by [proto.Duration](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#google.protobuf.Duration) + #[serde(skip_serializing_if = "Option::is_none")] + pub light_off_duration: Option, +} + +/// Set `color` of the LED with [google.type.Color](https://github.com/googleapis/googleapis/blob/master/google/type/color.proto). +#[derive(Debug, Serialize, Default)] +pub struct Color { + /// The amount of red in the color as a value in the interval [0, 1]. + pub red: f32, + /// The amount of green in the color as a value in the interval [0, 1]. + pub green: f32, + /// The amount of blue in the color as a value in the interval [0, 1]. + pub blue: f32, + /// The fraction of this color that should be applied to the pixel. That is, the final pixel color is defined + /// by the equation: `pixel color = alpha * (this color) + (1.0 - alpha) * (background color)` + /// This means that a value of 1.0 corresponds to a solid color, whereas a + /// value of 0.0 corresponds to a completely transparent color. + /// This uses a wrapper message rather than a simple float scalar so that + /// it is possible to distinguish between a default value and the value being unset. + /// If omitted, this color object is rendered as a solid color + /// (as if the alpha value had been explicitly given a value of 1.0). + pub alpha: f32, +} + +#[derive(Debug, Serialize)] +pub enum Proxy { + #[serde(rename = "PROXY_UNSPECIFIED")] + ProxyUnspecified, + + /// Try to proxy this notification. + #[serde(rename = "ALLOW")] + Allow, + + /// Do not proxy this notification. + #[serde(rename = "DENY")] + Deny, + + /// Only try to proxy this notification if its `AndroidMessagePriority` was lowered from `HIGH` to `NORMAL` on the device. + #[serde(rename = "IF_PRIORITY_LOWERED")] + IfPriorityLowered, +} + +impl Default for Proxy { + fn default() -> Self { + Self::IfPriorityLowered + } +} + +#[derive(Debug, Serialize)] +pub enum NotificationPriority { + /// If priority is unspecified, notification priority is set to `PRIORITY_DEFAULT`. + #[serde(rename = "PRIORITY_UNSPECIFIED")] + PriorityUnspecified, + + /// Lowest notification priority. Notifications with this `PRIORITY_MIN` might not be shown to the user except under special circumstances, such as detailed notification logs. + #[serde(rename = "PRIORITY_MIN")] + PriorityMin, + + /// Lower notification priority. The UI may choose to show the notifications smaller, or at a different position in the list, compared with notifications with `PRIORITY_DEFAULT`. + #[serde(rename = "PRIORITY_LOW")] + PriorityLow, + + /// Default notification priority. If the application does not prioritize its own notifications, use this value for all notifications. + #[serde(rename = "PRIORITY_DEFAULT")] + PriorityDefault, + + /// Higher notification priority. Use this for more important notifications or alerts. The UI may choose to show these notifications larger, or at a different position in the notification lists, compared with notifications with `PRIORITY_DEFAULT`. + #[serde(rename = "PRIORITY_HIGH")] + PriorityHigh, + + /// Highest notification priority. Use this for the application's most important items that require the user's prompt attention or input. + #[serde(rename = "PRIORITY_MAX")] + PriorityMax, +} + +impl Default for NotificationPriority { + fn default() -> Self { + Self::PriorityDefault + } +} + +#[derive(Debug, Serialize)] +pub enum Visibility { + /// If unspecified, default to `Visibility.PRIVATE`. + #[serde(rename = "VISIBILITY_UNSPECIFIED")] + VisibilityUnspecified, + + /// Show this notification on all lockscreens, but conceal sensitive or private information on secure lockscreens. + #[serde(rename = "PRIVATE")] + Private, + + /// Show this notification in its entirety on all lockscreens. + #[serde(rename = "PUBLIC")] + Public, + + /// Do not reveal any part of this notification on a secure lockscreen. + #[serde(rename = "SECRET")] + Secret, +} + +impl Default for Visibility { + fn default() -> Self { + Self::Private + } +} + +/// Message priority. Can take "normal" and "high" values. +/// For more information, see [Setting the priority of a message](https://goo.gl/GjONJv). +#[derive(Debug, Serialize)] +pub enum AndroidMessagePriority { + /// Default priority for notification messages. + /// FCM attempts to deliver high priority messages immediately, + /// allowing the FCM service to wake a sleeping device when possible + /// and open a network connection to your app server. + /// Apps with instant messaging, chat, or voice call alerts, + /// for example, generally need to open a network connection and make + /// sure FCM delivers the message to the device without delay. + /// Set high priority if the message is time-critical and requires + /// the user's immediate interaction, but beware that setting your + /// messages to high priority contributes more to battery drain compared + /// with normal priority messages. + #[serde(rename = "HIGH")] + High, + /// Default priority for data messages. Normal priority messages won't + /// open network connections on a sleeping device, and their delivery + /// may be delayed to conserve the battery. For less time-sensitive messages, + /// such as notifications of new email or other data to sync, choose normal delivery + /// priority. + #[serde(rename = "NORMAL")] + Normal, +} + +impl Default for AndroidMessagePriority { + fn default() -> Self { + Self::High + } +} + +/// Options for features provided by the FCM SDK for Android. +#[derive(Debug, Serialize, Default)] +pub struct AndroidFcmOptions { + /// Label associated with the message's analytics data. + #[serde(skip_serializing_if = "Option::is_none")] + analytics_label: Option, +} + +impl AndroidFcmOptions { + pub fn new(analytics_label: &str) -> Self { + Self { + analytics_label: Some(analytics_label.to_string()), + } + } +} diff --git a/src/fcm/ios.rs b/src/fcm/ios.rs new file mode 100644 index 0000000..4ee46d1 --- /dev/null +++ b/src/fcm/ios.rs @@ -0,0 +1,477 @@ +use std::collections::HashMap; + +use serde::Serialize; + +#[derive(Debug)] +pub struct Duration(std::time::Duration); +impl Duration { + pub fn from_secs(secs: u64) -> Self { + Self(std::time::Duration::from_secs(secs)) + } +} +impl Serialize for Duration { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.0.as_secs().to_string()) + } +} + +#[derive(Debug, Serialize, Default)] +pub struct APNSFcmOptions { + /// Label associated with the message's analytics data. + #[serde(skip_serializing_if = "Option::is_none")] + analytics_label: Option, + /// Contains the URL of an image that is going to be displayed in a notification. + /// If present, it will override [[MessageLike]]::fcmOptions. + #[serde(skip_serializing_if = "Option::is_none")] + image: Option, +} + +#[derive(Debug, Serialize, Default)] +/// APNs HTTP headers properties +/// See https://developer.apple.com/documentation/usernotifications/sending-notification-requests-to-apns +pub struct ApnsHeaders { + #[serde(skip_serializing_if = "Option::is_none")] + pub authorization: Option, + #[serde(rename = "apns-id")] + #[serde(skip_serializing_if = "Option::is_none")] + /// A canonical UUID that’s the unique ID for the notification. + /// If an error occurs when sending the notification, + /// APNs includes this value when reporting the error to your server. + /// Canonical UUIDs are 32 lowercase hexadecimal digits, displayed in five groups + /// separated by hyphens in the form 8-4-4-4-12. + /// For example: 123e4567-e89b-12d3-a456-4266554400a0. + /// + /// If you omit this header, APNs creates a UUID for you and returns it in its response. + pub apns_id: Option, + #[serde(rename = "apns-push-type")] + #[serde(skip_serializing_if = "Option::is_none")] + /// The value of this header must accurately reflect the contents of your notification’s payload. + /// If there’s a mismatch, or if the header is missing on required systems, APNs may return an error, + /// delay the delivery of the notification, or drop it altogether. + pub apns_push_type: Option, + /// The date at which the notification is no longer valid. This value is + /// a UNIX epoch expressed in seconds (UTC). + /// + /// If the value is nonzero, APNs stores the notification and tries to deliver it at least once, + /// repeating the attempt as needed until the specified date. + /// + /// If the value is 0, APNs attempts to deliver the notification only once and doesn’t store it. + /// + /// If you omit this header, APNs stores the push according to APNs storage policy. + #[serde(rename = "apns-expiration")] + #[serde(skip_serializing_if = "Option::is_none")] + pub apns_expiration: Option, + /// The priority of the notification. + /// + /// If you omit this header, APNs sets the notification priority to 10. + /// + /// - Specify 10 to send the notification immediately. + /// - Specify 5 to send the notification based on power considerations on the user’s device. + /// - Specify 1 to prioritize the device’s power considerations over all other + /// factors for delivery, and prevent awakening the device. + #[serde(rename = "apns-priority")] + #[serde(skip_serializing_if = "Option::is_none")] + pub apns_priority: Option, + /// The topic for the notification. In general, the topic is your app’s bundle ID/app ID. + /// It can have a suffix based on the type of push notification. + /// + /// If you’re using token-based authentication with APNs, you must include + /// this header with the correct bundle ID and suffix combination. + #[serde(rename = "apns-topic")] + #[serde(skip_serializing_if = "Option::is_none")] + pub apns_topic: Option, + /// An identifier you use to merge multiple notifications into a single notification for the user. + /// Typically, each notification request displays a new notification on the user’s device. + /// + /// When sending the same notification more than once, use the same value in this + /// header to merge the requests. + /// The value of this key must not exceed 64 bytes. + #[serde(rename = "apns-collapse-id")] + #[serde(skip_serializing_if = "Option::is_none")] + pub apns_collapse_id: Option, +} + +impl ApnsHeaders { + pub fn ios_background_notification() -> ApnsHeaders { + ApnsHeaders { + apns_push_type: Some(ApnsPushType::Background), + apns_priority: Some(ApnsPriority::RespectEnergySavingMode), + ..Default::default() + } + } +} + +#[derive(Debug, Serialize)] +pub enum ApnsPriority { + #[serde(rename = "10")] + SendImmediately, + #[serde(rename = "5")] + RespectEnergySavingMode, + #[serde(rename = "1")] + RespectEnergySavingModeNoAwaking, +} + +#[derive(Debug, Serialize, Default)] +pub struct ApnsConfig { + #[serde(skip_serializing_if = "Option::is_none")] + payload: Option, + #[serde(skip_serializing_if = "Option::is_none")] + headers: Option, +} + +impl ApnsConfig { + pub fn new( + aps: &Aps, + data: &HashMap, + headers: Option, + ) -> ApnsConfig { + let mut payload = serde_json::json!({ + "aps": aps, + }); + let data_payload = serde_json::json!(data); + ApnsConfig::merge(&mut payload, &data_payload); + ApnsConfig { + payload: Some(payload), + headers, + } + } + pub fn ios_background_notification(data_payload: HashMap) -> ApnsConfig { + let mut payload = serde_json::json!({ + "aps": Aps { + content_available: Some(ContentAvailable::On), + ..Default::default() + } + }); + let data_payload = serde_json::json!(data_payload); + ApnsConfig::merge(&mut payload, &data_payload); + + ApnsConfig { + payload: Some(payload), + headers: Some(ApnsHeaders::ios_background_notification()), + } + } + fn merge(a: &mut serde_json::Value, b: &serde_json::Value) { + match (a, b) { + (serde_json::Value::Object(a), serde_json::Value::Object(b)) => { + for (k, v) in b { + ApnsConfig::merge(a.entry(k.clone()).or_insert(serde_json::Value::Null), v); + } + } + (a, b) => *a = b.clone(), + } + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum ApnsPushType { + /// The push type for notifications that trigger a user interaction—for example, an alert, badge, or sound. + /// If you set this push type, the apns-topic header field must use your app’s bundle ID as the topic. + /// For more information, refer to + /// [Generating a remote notification](https://developer.apple.com/documentation/usernotifications/generating-a-remote-notification). + /// If the notification requires immediate action from the user, set notification priority to 10; otherwise use 5. + /// + /// You’re required to use the alert push type on watchOS 6 and later. It’s recommended on macOS, iOS, tvOS, and iPadOS. + Alert, + /// The push type for notifications that deliver content in the background, and don’t trigger any user interactions. + /// If you set this push type, the apns-topic header field must use your app’s bundle ID as the topic. Always use priority 5. + /// Using priority 10 is an error. For more information, refer to + /// [Pushing background updates to your App](https://developer.apple.com/documentation/usernotifications/pushing-background-updates-to-your-app). + /// + /// You’re required to use the background push type on watchOS 6 and later. It’s recommended on macOS, iOS, tvOS, and iPadOS. + Background, + /// The push type for notifications that request a user’s location. If you set this push type, the apns-topic + /// header field must use your app’s bundle ID with.location-query appended to the end. For more information, refer to + /// [Creating a location push service extension](https://developer.apple.com/documentation/CoreLocation/creating-a-location-push-service-extension). + /// + /// The location push type isn’t available on macOS, tvOS, and watchOS. It’s recommended for iOS and iPadOS. + /// + /// If the location query requires an immediate response from the Location Push Service Extension, + /// set notification apns-priority to 10; otherwise, use 5. The location push type supports only token-based authentication. + Location, + /// The push type for notifications that provide information about an incoming Voice-over-IP (VoIP) call. + /// For more information, refer to + /// [Responding to VoIP Notifications from PushKit](https://developer.apple.com/documentation/PushKit/responding-to-voip-notifications-from-pushkit). + /// If you set this push type, the apns-topic header field must use your app’s bundle ID with.voip appended to the end. + /// + /// If you’re using certificate-based authentication, you must also register the certificate for VoIP services. + /// The topic is then part of the 1.2.840.113635.100.6.3.4 or 1.2.840.113635.100.6.3.6 extension. + /// + /// The voip push type isn’t available on watchOS. It’s recommended on macOS, iOS, tvOS, and iPadOS. + VoiP, + /// The push type for notifications that contain update information for a watchOS app’s complications. + /// For more information, refer to + /// [Keeping your complications up to date](https://developer.apple.com/documentation/clockkit/deprecated_articles_and_symbols/keeping_your_complications_up_to_date). + /// + /// If you set this push type, the apns-topic header field must use your app’s bundle ID with.complication + /// appended to the end. If you’re using certificate-based authentication, you must also register + /// the certificate for WatchKit services. + /// + /// The topic is then part of the 1.2.840.113635.100.6.3.6 extension. + /// + /// The complication push type isn’t available on macOS, tvOS, and iPadOS. It’s recommended for watchOS and iOS. + Compilation, + /// The push type to signal changes to a File Provider extension. + /// + /// If you set this push type, the apns-topic header field must use your app’s bundle ID with.pushkit.fileprovider + /// appended to the end. + /// + /// For more information, refer to + /// [Using push notifications to signal changes](https://developer.apple.com/documentation/FileProvider/using-push-notifications-to-signal-changes). + /// + /// The fileprovider push type isn’t available on watchOS. It’s recommended on macOS, iOS, tvOS, and iPadOS. + FileProvider, + /// The push type for notifications that tell managed devices to contact the MDM server. + /// + /// If you set this push type, you must use the topic from the UID attribute in the subject + /// of your MDM push certificate. + /// + /// For more information, refer to + /// [Device Management](https://developer.apple.com/documentation/devicemanagement). + /// + /// The mdm push type isn’t available on watchOS. It’s recommended on macOS, iOS, tvOS, and iPadOS. + MDM, + /// The push type to signal changes to a live activity session. If you set this push type, + /// the apns-topic header field must use your app’s bundle ID with.push-type.liveactivity + /// appended to the end. For more information, refer to Updating and ending your Live Activity + /// with ActivityKit push notifications. + /// + /// The liveactivity push type isn’t available on watchOS, macOS, and tvOS. It’s recommended on iOS and iPadOS. + LiveActivity, + /// The push type for notifications that provide information about updates to your application’s + /// push to talk services. For more information, refer to [Push to Talk](https://developer.apple.com/documentation/PushToTalk). + /// + /// If you set this push type, the apns-topic header field must use your app’s bundle ID with.voip-ptt appended to the end. + /// + /// The pushtotalk push type isn’t available on watchOS, macOS, and tvOS. It’s recommended on iOS and iPadOS. + PushToTalk, +} + +/// See https://developer.apple.com/documentation/usernotifications/generating-a-remote-notification +#[derive(Debug, Serialize, Default)] +pub struct Aps { + #[serde(skip_serializing_if = "Option::is_none")] + pub alert: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub badge: Option, + #[serde(rename = "thread-id")] + #[serde(skip_serializing_if = "Option::is_none")] + pub thread_id: Option, + #[serde(rename = "content-available")] + #[serde(skip_serializing_if = "Option::is_none")] + pub content_available: Option, + #[serde(rename = "mutable-content")] + #[serde(skip_serializing_if = "Option::is_none")] + pub mutable_content: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub timestamp: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub event: Option, + #[serde(rename = "dismissal-date")] + #[serde(skip_serializing_if = "Option::is_none")] + pub dismissal_date: Option, + #[serde(rename = "attributes-type")] + #[serde(skip_serializing_if = "Option::is_none")] + pub attributes_type: Option, +} + +#[derive(Debug, Clone, Copy)] +pub enum MutableContent { + On, + Off, +} + +impl Serialize for MutableContent { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Self::On => 1.serialize(serializer), + Self::Off => 0.serialize(serializer), + } + } +} + +#[derive(Debug, Clone, Copy)] +pub enum ContentAvailable { + On, + Off, +} + +impl Serialize for ContentAvailable { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + ContentAvailable::On => 1.serialize(serializer), + ContentAvailable::Off => 0.serialize(serializer), + } + } +} + +#[derive(Debug)] +pub enum Alert { + Simple(String), + Structural(Box), +} + +impl Serialize for Alert { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Self::Simple(alert) => alert.serialize(serializer), + Self::Structural(alert) => alert.serialize(serializer), + } + } +} + +#[derive(Debug, Serialize, Default)] +pub struct RichAlert { + /// The title of the notification. Apple Watch displays this string in + /// the short look notification interface. Specify a string that’s quickly + /// understood by the user. + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + /// Additional information that explains the purpose of the notification. + #[serde(skip_serializing_if = "Option::is_none")] + pub subtitle: Option, + /// The content of the alert message. + #[serde(skip_serializing_if = "Option::is_none")] + pub body: Option, + /// The name of the launch image file to display. If the user chooses + /// to launch your app, the contents of the specified image or storyboard + /// file are displayed instead of your app’s normal launch image. + #[serde(rename = "launch-image")] + #[serde(skip_serializing_if = "Option::is_none")] + pub launch_image: Option, + /// The key for a localized title string. Specify this key instead of the + /// title key to retrieve the title from your app’s Localizable.strings files. + /// The value must contain the name of a key in your strings file. + #[serde(rename = "title-loc-key")] + #[serde(skip_serializing_if = "Option::is_none")] + pub title_loc_key: Option, + /// An array of strings containing replacement values for variables in your + /// title string. Each %@ character in the string specified by the title-loc-key + /// is replaced by a value from this array. + /// The first item in the array replaces the first instance of the %@ character + /// in the string, the second item replaces the second instance, and so on. + #[serde(rename = "title-loc-args")] + #[serde(skip_serializing_if = "Option::is_none")] + pub title_loc_args: Option>, + /// The key for a localized subtitle string. Use this key, instead of the subtitle key, + /// to retrieve the subtitle from your app’s Localizable.strings file. + /// The value must contain the name of a key in your strings file. + #[serde(rename = "subtitle-loc-key")] + #[serde(skip_serializing_if = "Option::is_none")] + pub subtitle_loc_key: Option, + /// An array of strings containing replacement values for variables in your title + /// string. Each %@ character in the string specified by subtitle-loc-key is + /// replaced by a value from this array. The first item in the array replaces + /// the first instance of the %@ character in the string, the second item + /// replaces the second instance, and so on. + #[serde(rename = "subtitle-loc-args")] + #[serde(skip_serializing_if = "Option::is_none")] + pub subtitle_loc_args: Option>, + /// The key for a localized message string. Use this key, instead of + /// the body key, to retrieve the message text from your app’s Localizable.strings + /// file. The value must contain the name of a key in your strings file. + #[serde(rename = "loc-key")] + #[serde(skip_serializing_if = "Option::is_none")] + pub loc_key: Option, + /// An array of strings containing replacement values for variables in your message text. + /// Each %@ character in the string specified by loc-key is replaced by a value from this array. + /// The first item in the array replaces the first instance of the %@ character in the string, + /// the second item replaces the second instance, and so on. + #[serde(rename = "loc-args")] + #[serde(skip_serializing_if = "Option::is_none")] + pub loc_args: Option>, +} + +#[derive(Debug)] +pub enum Sound { + Simple(String), + Structural { + critical: u8, + /// The name of a sound file in your app’s main bundle or in the Library/Sounds + /// folder of your app’s container directory. Specify the string “default” to play + /// the system sound. For information about how to prepare sounds, + /// see [UNNotificationSound](https://developer.apple.com/documentation/usernotifications/unnotificationsound). + name: String, + /// The volume for the critical alert’s sound. + /// Set this to a value between 0 (silent) and 1 (full volume). + volume: f32, + }, +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use crate::fcm::ios::RichAlert; + + use super::{Alert, ApnsConfig}; + + #[test] + fn check_serialization_for_union_like_type() { + let simple = Alert::Simple("bar".to_string()); + let json = serde_json::json!({ + "foo": simple + }); + assert_eq!( + json, + serde_json::json!({ + "foo": "bar" + }) + ); + let structural = Alert::Structural( + RichAlert { + title: Some("title".to_string()), + subtitle: Some("subtitle".to_string()), + body: Some("body".to_string()), + ..Default::default() + } + .into(), + ); + let json = serde_json::json!({ + "foo": structural + }); + assert_eq!( + json, + serde_json::json!({ + "foo": { + "title": "title", + "subtitle": "subtitle", + "body": "body" + } + }) + ) + } + #[test] + fn check_serialization_for_ios_background_type() { + let payload = ApnsConfig::ios_background_notification(HashMap::from_iter([( + "example".to_string(), + "example".to_string(), + )])); + let json = serde_json::json!(payload); + let expect = serde_json::json!({ + "headers": { + "apns-push-type": "background", + "apns-priority": "5" + }, + "payload": { + "aps": { + "content-available": 1 + }, + "example": "example" + } + }); + assert_eq!(json, expect) + } +} diff --git a/src/fcm/webpush.rs b/src/fcm/webpush.rs new file mode 100644 index 0000000..35be7a6 --- /dev/null +++ b/src/fcm/webpush.rs @@ -0,0 +1,32 @@ +use std::collections::HashMap; + +use serde::Serialize; + +/// [Webpush protocol](https://tools.ietf.org/html/rfc8030) options., +#[derive(Debug, Serialize, Default)] +pub struct WebPushConfig { + /// HTTP headers defined in webpush protocol. Refer to [Webpush protocol](https://tools.ietf.org/html/rfc8030#section-5) for supported headers, e.g. \"TTL\": \"15\". + #[serde(skip_serializing_if = "Option::is_none")] + pub headers: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option>, + /// Web Notification options as a JSON object. Supports Notification instance properties as defined in + /// [Web Notification API](https://developer.mozilla.org/en-US/docs/Web/API/Notification). + /// If present, "title" and "body" fields override [google.firebase.fcm.v1.Notification.title] and + /// [google.firebase.fcm.v1.Notification.body]. + #[serde(skip_serializing_if = "Option::is_none")] + pub notification: Option, + /// Options for features provided by the FCM SDK for Web + #[serde(skip_serializing_if = "Option::is_none")] + pub fcm_options: Option, +} + +#[derive(Debug, Serialize, Default)] +pub struct WebPushFcmOptions { + /// Label associated with the message's analytics data. + #[serde(skip_serializing_if = "Option::is_none")] + pub analytics_label: Option, + #[serde(skip_serializing_if = "Option::is_none")] + /// The link to open when the user clicks on the notification. For all URL values, HTTPS is required. + pub link: Option, +} diff --git a/src/lib.rs b/src/lib.rs index 63efdd3..08d9683 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,15 @@ +#[cfg(feature = "fcm")] +pub use serde_json; +#[cfg(feature = "fcm")] +pub mod fcm; +#[cfg(feature = "topic-management")] pub mod topic; use async_trait::async_trait; use gcloud_sdk::{GoogleAuthTokenGenerator, TokenSourceType, GCP_DEFAULT_SCOPES}; use http::{ header::{ACCEPT, AUTHORIZATION, CONTENT_LENGTH, CONTENT_TYPE}, - Request, Response, StatusCode, + HeaderName, Request, Response, StatusCode, }; use hyper::{client::HttpConnector, Body}; #[cfg(feature = "hyper-rustls")] @@ -12,8 +17,7 @@ use hyper_rustls::HttpsConnector; #[cfg(feature = "hyper-tls")] use hyper_tls::HttpsConnector; use serde::Deserialize; -use std::sync::Arc; -use topic::TopicManagementSupport; +use std::{env, sync::Arc, time::Duration}; /// [FCMClient] implements some wrapper functions for google FCM APIs and Instant ID APIs. /// @@ -22,24 +26,46 @@ use topic::TopicManagementSupport; /// For example, if you set `GOOGLE_APPLICATION_CREDENTIALS` environment variable pointing to your service account json file, /// FCMClient tries to read the file from there. /// -/// ```rust -/// let client = FCMClient::new().await.unwrap(); +/// For details, see https://google.aip.dev/auth/4110 /// -/// let res = client.register_tokens_to_topic( -/// "topic_name".into(), -/// vec![token_0,token_1,...] -/// ).await.unwrap(); -/// println!("{:?}",res); -/// // => TopicManagementResponse {results: [{}, {"error": "INVALID_ARGUMENT"}, ...] } +/// ```no_run +/// use firebase_messaging_rs::FCMClient; +/// use firebase_messaging_rs::fcm::*; +/// use firebase_messaging_rs::topic::*; +/// +/// #[tokio::main] +/// async fn main() { +/// let client = FCMClient::new().await.unwrap(); +/// let _ = client.register_tokens_to_topic( +/// "topic_name".into(), +/// vec!["token_0".to_string(),"token_1".to_string()] +/// ).await; +/// // => TopicManagementResponse {results: [{}, {"error": "INVALID_ARGUMENT"}, ...] } +/// } /// ``` #[derive(Clone)] pub struct FCMClient { http_client: hyper::Client>, token_gen: Arc, + project_id: String, } impl FCMClient { + fn google_cloud_project() -> Option { + env::var("GOOGLE_CLOUD_PROJECT") + .or_else(|_| env::var("GCP_PROJECT")) + .ok() + } pub async fn new() -> Result { + let project_id = Self::google_cloud_project().ok_or( + "cannot detect google project id from env. Provide project id by GOOGLE_CLOUD_PROJECT env var.".to_string(), + )?; + FCMClient::with_scope(&project_id, &GCP_DEFAULT_SCOPES).await + } + pub async fn new_with_project(project_id: &str) -> Result { + FCMClient::with_scope(project_id, &GCP_DEFAULT_SCOPES).await + } + pub async fn with_scope(project_id: &str, scopes: &[String]) -> Result { #[cfg(feature = "hyper-tls")] let connector = HttpsConnector::new(); @@ -51,24 +77,30 @@ impl FCMClient { .enable_http1() .build(); - let token_gen = - GoogleAuthTokenGenerator::new(TokenSourceType::Default, GCP_DEFAULT_SCOPES.clone()) - .await - .map_err(|_| "unable to initialize token generator")?; + let token_gen = GoogleAuthTokenGenerator::new(TokenSourceType::Default, scopes.to_vec()) + .await + .map_err(|_| "unable to initialize token generator")?; Ok(Self { token_gen: Arc::new(token_gen), http_client: hyper::Client::builder().build::<_, Body>(connector), + project_id: project_id.to_string(), }) } } -impl TopicManagementSupport for FCMClient {} +#[cfg(feature = "topic-management")] +impl crate::topic::TopicManagementSupport for FCMClient {} +#[cfg(feature = "fcm")] +impl crate::fcm::FCMApi for FCMClient {} #[async_trait] impl GenericGoogleRestAPISupport for FCMClient { fn get_http_client(&self) -> hyper::Client, Body> { self.http_client.clone() } + fn project_id(&self) -> String { + self.project_id.to_string() + } async fn get_header_token(&self) -> Result { let token = self.token_gen.create_token().await?; Ok(token.header_value()) @@ -78,6 +110,7 @@ impl GenericGoogleRestAPISupport for FCMClient { #[async_trait] pub trait GenericGoogleRestAPISupport { async fn get_header_token(&self) -> Result; + fn project_id(&self) -> String; fn get_http_client(&self) -> hyper::Client, Body>; async fn post_request< P: serde::Serialize + Send + Sync, @@ -87,6 +120,19 @@ pub trait GenericGoogleRestAPISupport { &self, endpoint: &str, payloadable: P, + ) -> Result { + self.post_request_with(endpoint, payloadable, &[]).await + } + + async fn post_request_with< + P: serde::Serialize + Send + Sync, + R: for<'a> Deserialize<'a> + Clone, + E: From, + >( + &self, + endpoint: &str, + payloadable: P, + extra_headers: &[(&str, &str)], ) -> Result { let auth_header_value = self .get_header_token() @@ -94,16 +140,17 @@ pub trait GenericGoogleRestAPISupport { .map_err(|_| RPCError::Unauthorized("unable to get header token".into())) .map_err(E::from)?; let payload = serde_json::to_vec(&payloadable).unwrap(); - let req = Request::builder() + let mut builder = Request::builder() .uri(endpoint) .method("POST") .header(CONTENT_TYPE, "application/json") .header(ACCEPT, "application/json") .header(AUTHORIZATION, auth_header_value) - .header("access_token_auth", "true") - // `access_token_auth` enables authorization based on oauth2 access_token. Without this, We must use unsafe serverKey. - // https://github.com/firebase/firebase-admin-go/blob/beaa6ae763d2fb57650760b9703cd91cc7c14b9b/messaging/topic_mgt.go#L69 - .header(CONTENT_LENGTH, format!("{}", payload.len() as u64)) + .header(CONTENT_LENGTH, format!("{}", payload.len() as u64)); + for (key, value) in extra_headers { + builder = builder.header(*key, *value) + } + let req = builder .body(Body::from(payload)) .map_err(|e| RPCError::BuildRequestFailure(format!("{e:?}"))) .map_err(E::from)?; @@ -131,9 +178,37 @@ pub trait GenericGoogleRestAPISupport { .header(CONTENT_TYPE, "application/json") .header(ACCEPT, "application/json") .header(AUTHORIZATION, auth_header_value) - .header("access_token_auth", "true") - // `access_token_auth` enables authorization based on oauth2 access_token. Without this, We must use unsafe serverKey. - // https://github.com/firebase/firebase-admin-go/blob/beaa6ae763d2fb57650760b9703cd91cc7c14b9b/messaging/topic_mgt.go#L69 + .body(Body::empty()) // NOTE: what is difference between Body::empty() and ()? + .map_err(|e| RPCError::BuildRequestFailure(format!("{e:?}"))) + .map_err(E::from)?; + let res = self + .get_http_client() + .request(req) + .await + .map_err(|_| RPCError::HttpRequestFailure) // FIXME: don't swallow error! propagate error info + .map_err(E::from)?; + Self::handle_response_body(res).await + } + async fn get_request_with Deserialize<'a> + Clone, E: From>( + &self, + endpoint: &str, + extra_headers: &[(&str, &str)], + ) -> Result { + let auth_header_value = self + .get_header_token() + .await + .map_err(|_| RPCError::Unauthorized("unable to get header token".into())) + .map_err(E::from)?; + let mut builder = Request::builder() + .uri(endpoint) + .method("GET") + .header(CONTENT_TYPE, "application/json") + .header(ACCEPT, "application/json") + .header(AUTHORIZATION, auth_header_value); + for (key, value) in extra_headers { + builder = builder.header(*key, *value) + } + let req = builder .body(Body::empty()) // NOTE: what is difference between Body::empty() and ()? .map_err(|e| RPCError::BuildRequestFailure(format!("{e:?}"))) .map_err(E::from)?; @@ -147,7 +222,7 @@ pub trait GenericGoogleRestAPISupport { } async fn handle_response_body Deserialize<'a> + Clone, E: From>( - res: Response, + mut res: Response, ) -> Result { match res.status() { StatusCode::OK => { @@ -169,8 +244,27 @@ pub trait GenericGoogleRestAPISupport { )) } .map_err(E::from), - e if e.is_client_error() => Err(E::from(RPCError::InvalidRequest)), - e if e.is_server_error() => Err(E::from(RPCError::Internal)), + StatusCode::BAD_REQUEST => { + let data = hyper::body::to_bytes(res.body_mut()) + .await + .map_err(|_| RPCError::DecodeFailure)?; + let data = String::from_utf8(data.to_vec()).ok(); + Err(E::from(RPCError::InvalidRequest { details: data })) + } + e if e.is_client_error() => Err(E::from(RPCError::invalid_request())), + e if e.is_server_error() => { + if let Some(retry_after_sec) = res + .headers() + .get(HeaderName::from_static("Retry-After")) + .and_then(|h| h.to_str().ok().and_then(|s| s.parse::().ok())) + { + Err(E::from(RPCError::retryable_internal(Duration::from_secs( + retry_after_sec, + )))) + } else { + Err(E::from(RPCError::internal())) + } + } e => Err(E::from(RPCError::Unknown(e.as_u16()))), } } @@ -184,39 +278,169 @@ pub enum RPCError { HttpRequestFailure, DecodeFailure, DeserializeFailure { reason: String, source: String }, - InvalidRequest, - Internal, + InvalidRequest { details: Option }, + Internal { retry_after: Option }, Unknown(u16), } +impl RPCError { + pub fn invalid_request() -> Self { + Self::InvalidRequest { details: None } + } + pub fn invalid_request_descriptive(data: &str) -> Self { + Self::InvalidRequest { + details: Some(data.to_string()), + } + } + pub fn internal() -> Self { + RPCError::Internal { retry_after: None } + } + pub fn retryable_internal(retry_after: Duration) -> Self { + RPCError::Internal { + retry_after: Some(retry_after), + } + } +} #[cfg(test)] mod tests { - use crate::{ - topic::{TopicManagementError, TopicManagementSupport}, - FCMClient, - }; + #[cfg(feature = "fcm")] + use crate::fcm::android::*; + #[cfg(feature = "fcm")] + use crate::fcm::ios::*; + #[cfg(feature = "fcm")] + use crate::fcm::webpush::*; + #[cfg(feature = "fcm")] + use crate::fcm::*; + #[cfg(feature = "topic-management")] + use crate::topic::*; + use crate::FCMClient; + use std::collections::HashMap; + #[cfg(feature = "fcm")] + #[tokio::test{flavor = "multi_thread"}] + async fn full_message_payload_should_pass_validation() { + let client = FCMClient::new().await.unwrap(); + let aps = Aps { + alert: Some(Alert::Structural( + RichAlert { + title: Some("example".to_string()), + subtitle: Some("example".to_string()), + body: Some("example".to_string()), + launch_image: Some("example".to_string()), + title_loc_key: Some("example".to_string()), + title_loc_args: Some(vec!["example".to_string()]), + subtitle_loc_key: Some("example".to_string()), + subtitle_loc_args: Some(vec!["example".to_string()]), + loc_key: Some("example".to_string()), + loc_args: Some(vec!["example".to_string()]), + } + .into(), + )), + badge: Some(42), + thread_id: Some("example".to_string()), + content_available: Some(ContentAvailable::On), + mutable_content: Some(MutableContent::On), + timestamp: Some(0), + event: Some("example".to_string()), + dismissal_date: Some(0), + attributes_type: Some("example".to_string()), + }; + let headers = ApnsHeaders { + authorization: Some("example".to_string()), + apns_id: Some("example".to_string()), + apns_push_type: Some(ApnsPushType::Alert), + apns_expiration: Some(ios::Duration::from_secs(3600)), + apns_priority: Some(ApnsPriority::RespectEnergySavingMode), + apns_topic: Some("example".to_string()), + apns_collapse_id: Some("example".to_string()), + }; + let msg = Message::Topic { + topic: "example".to_string(), + fcm_options: Some(FcmOptions::new("example")), + notification: Some(Notification { + title: Some("example".to_string()), + body: Some("example".to_string()), + image: Some("https://example.com/example.png".to_string()), + }), + android: Some(AndroidConfig { + fcm_options: Some(AndroidFcmOptions::new("example")), + priority: Some(AndroidMessagePriority::Normal), + notification: Some(AndroidNotification { + local_only: Some(true), + default_light_settings: Some(false), + default_sound: Some(false), + image: Some("https://example.com/example.png".to_string()), + tag: Some("example".to_string()), + default_vibrate_timings: Some(false), + notification_count: Some(1), + title_loc_key: Some("example".to_string()), + bypass_proxy_notification: Some(false), + click_action: Some("example".to_string()), + sound: Some("default".to_string()), + // FIXME + event_time: Some("1970-01-01T00:00:00Z".to_string()), + title: Some("example".to_string()), + vibrate_timings: Some(vec![android::Duration::from_secs(10.0)]), + body_loc_key: Some("example".to_string()), + body: Some("example".to_string()), + icon: Some("https://example.com/example.ico".to_string()), + title_loc_args: Some(vec!["example".to_string()]), + color: Some("#FFFFFF".to_string()), + body_loc_args: Some(vec!["example".to_string()]), + sticky: Some(true), + proxy: Some(Proxy::ProxyUnspecified), + ticker: Some("example".to_string()), + notification_priority: Some(NotificationPriority::PriorityDefault), + visibility: Some(android::Visibility::VisibilityUnspecified), + channel_id: Some("example".to_string()), + light_settings: Some(LightSettings { + color: Color { + red: 255.0, + green: 255.0, + blue: 255.0, + alpha: 1.0, + }, + light_on_duration: Some(android::Duration::from_secs(10.0)), + light_off_duration: Some(android::Duration::from_secs(10.0)), + }), + }), + data: Some(HashMap::from_iter([("foo".to_string(), "bar".to_string())])), + restricted_package_name: Some("com.example.app".to_string()), + ttl: Some(android::Duration::from_secs(3.5)), + direct_boot_ok: Some(true), + collapse_key: Some("example".to_string()), + }), + webpush: Some(WebPushConfig { + headers: Some(HashMap::from_iter([("foo".to_string(), "bar".to_string())])), + data: Some(HashMap::from_iter([("foo".to_string(), "bar".to_string())])), + notification: None, + fcm_options: Some(WebPushFcmOptions { + analytics_label: Some("example".to_string()), + link: Some("example".to_string()), + }), + }), + apns: Some(ApnsConfig::new(&aps, &HashMap::default(), Some(headers))), + }; + let res = client.send(&msg).await; + println!("{res:?}") + } + #[cfg(feature = "topic-management")] #[tokio::test{flavor = "multi_thread"}] - async fn it_returns_errors_for_invalid_token() { let res = FCMClient::new() .await - .expect( - "FCMClient initialization failed. Did you export GOOGLE_APPLICATION_CREDENTIALS?", - ) + .expect("FCMClient initialization failed. Did you set GOOGLE_APPLICATION_CREDENTIALS?") .register_token_to_topic("topic_name".into(), "") .await; assert!(matches!(res, Err(TopicManagementError::InvalidRequest))); } - + #[cfg(feature = "topic-management")] #[tokio::test{flavor = "multi_thread"}] async fn it_returns_errors_for_invalid_tokens() { let res = FCMClient::new() .await - .expect( - "FCMClient initialization failed. Did you export GOOGLE_APPLICATION_CREDENTIALS?", - ) + .expect("FCMClient initialization failed. Did you set GOOGLE_APPLICATION_CREDENTIALS?") .register_tokens_to_topic("topic_name".into(), vec!["".into(), "".into(), "".into()]) .await .expect("Request Failed Due to: "); @@ -229,15 +453,16 @@ mod tests { })); } #[allow(unused)] + #[cfg(feature = "topic-management")] #[tokio::test{ flavor = "multi_thread"}] - async fn testit() { + async fn test_for_interactive_debug() { let topic_name = std::env::var("TEST_FIREBASE_TOPIC_NAME").expect("TEST_FIREBASE_TOPIC_NAME not found."); let tkn = std::env::var("TEST_FIREBASE_IID_TOKEN").expect("TEST_FIREBASE_IID_TOKEN not found"); - let c = FCMClient::new().await.expect( - "FCMClient initialization failed. Did you export GOOGLE_APPLICATION_CREDENTIALS?", - ); + let c = FCMClient::new() + .await + .expect("FCMClient initialization failed. Did you set GOOGLE_APPLICATION_CREDENTIALS?"); let sts = c.get_info_by_iid_token(&tkn, true).await; let res = c.register_token_to_topic(&topic_name, &tkn).await; let res = c diff --git a/src/topic.rs b/src/topic.rs index c51a5a6..7521075 100644 --- a/src/topic.rs +++ b/src/topic.rs @@ -30,33 +30,47 @@ pub trait TopicManagementSupport: GenericGoogleRestAPISupport { topic: &str, token: &str, ) -> Result, TopicManagementError> { - self.post_request(&Self::put_endpoint(token, topic), ()) - .await + // `access_token_auth` enables authorization based on oauth2 access_token. Without this, We must use unsafe serverKey. + // https://github.com/firebase/firebase-admin-go/blob/beaa6ae763d2fb57650760b9703cd91cc7c14b9b/messaging/topic_mgt.go#L69 + self.post_request_with( + &Self::put_endpoint(token, topic), + (), + &[("access_token_auth", "true")], + ) + .await } /// [register_tokens_to_topic] registers tokens to topic. /// * topic - topic to follow. You don't need to add `/topics/` prefix. - /// * tokens - registration tokens to be associated with the topic. + /// * tokens - A non-empty list of device registration tokens to be associated with the topic. List may not have more than 1000 elements and any list element must not be empty. async fn register_tokens_to_topic( &self, topic: String, tokens: Vec, ) -> Result { let req = Request::subscribe(format!("/topics/{topic}"), tokens); - self.post_request(&format!("{BATCH_ENDPOINT}:batchAdd"), req) - .await + self.post_request_with( + &format!("{BATCH_ENDPOINT}:batchAdd"), + req, + &[("access_token_auth", "true")], + ) + .await } /// [unregister_tokens_from_topic] unregisters tokens from topic. /// * topic - topic to follow. You don't need to add `/topics/` prefix. - /// * tokens - registration tokens to be unregistered from the topic. + /// * tokens - A non-empty list of device registration tokens to be unregistered from the topic. List may not have more than 1000 elements. async fn unregister_tokens_from_topic( &self, topic: &str, tokens: Vec, ) -> Result { let req = Request::unsubscribe(format!("/topics/{topic}"), tokens); - self.post_request(&format!("{BATCH_ENDPOINT}:batchRemove"), req) - .await + self.post_request_with( + &format!("{BATCH_ENDPOINT}:batchRemove"), + req, + &[("access_token_auth", "true")], + ) + .await } /// [get_info_by_iid_token] get information about topics accosiated to the given token. Information may contain application id, authorized_entity, platform, etc. /// @@ -75,7 +89,8 @@ pub trait TopicManagementSupport: GenericGoogleRestAPISupport { } else { format!("{INFO_ENDPOINT}/{token}") }; - self.get_request(&request_url).await + self.get_request_with(&request_url, &[("access_token_auth", "true")]) + .await } } @@ -152,8 +167,8 @@ impl From for TopicManagementError { msg: format!("unable to deserialize response body to type: {reason}: {source}"), }, RPCError::Unauthorized(msg) => Self::Unauthorized(msg), - RPCError::InvalidRequest => Self::InvalidRequest, - RPCError::Internal => Self::ServerError, + RPCError::InvalidRequest { .. } => Self::InvalidRequest, + RPCError::Internal { .. } => Self::ServerError, RPCError::Unknown(_) => Self::Unknown, } } @@ -238,10 +253,10 @@ impl TopicInfoResponseKind { /// ```json /// { /// "topics":{ -/// "topicname1":{ "addDate":"2015-07-30"}, -/// "topicname2":{ "addDate":"2015-07-30"}, -/// "topicname3":{"addDate":"2015-07-30"}, -/// "topicname4":{"addDate":"2015-07-30"} +/// "topicname1": {"addDate":"2015-07-30"}, +/// "topicname2": {"addDate":"2015-07-30"}, +/// "topicname3": {"addDate":"2015-07-30"}, +/// "topicname4": {"addDate":"2015-07-30"} /// } /// } /// ```