From c3970e2a45e0759776e62551c58e88d78da817d6 Mon Sep 17 00:00:00 2001 From: "nicolas.borges" Date: Thu, 16 Apr 2026 09:58:54 -0300 Subject: [PATCH] commit inicial --- .dockerignore | 6 + .gitignore | 13 + Cargo.lock | 4166 +++++++++++++++++++++++++++ Cargo.toml | 33 + Containerfile | 19 + FILTER.txt | 1 + PROMPT.txt | 45 + PROMPT_DATA_SANITIZATION.txt | 102 + compose.yaml | 24 + crontab | 13 + src/.env.example | 7 + src/config.rs | 7 + src/fin_main.rs | 673 +++++ src/groupped_repport_monthly.rs | 544 ++++ src/groupped_repport_weekly.rs | 597 ++++ src/groupped_repport_weekly.rs.save | 493 ++++ src/main.rs | 749 +++++ src/main.rs.save | 744 +++++ src/send_mail_util.rs | 46 + src/zip_directory_util.rs | 69 + 20 files changed, 8351 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 Containerfile create mode 100644 FILTER.txt create mode 100644 PROMPT.txt create mode 100644 PROMPT_DATA_SANITIZATION.txt create mode 100644 compose.yaml create mode 100644 crontab create mode 100644 src/.env.example create mode 100644 src/config.rs create mode 100644 src/fin_main.rs create mode 100644 src/groupped_repport_monthly.rs create mode 100644 src/groupped_repport_weekly.rs create mode 100644 src/groupped_repport_weekly.rs.save create mode 100644 src/main.rs create mode 100644 src/main.rs.save create mode 100644 src/send_mail_util.rs create mode 100644 src/zip_directory_util.rs diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..362ad98 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +/target +src/.env +.env +log/ +.zip +evaluations \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4e46adf --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +/target +src/.env +.env +log/ +.zip +.vscode +evaluations +groupped +# Added by cargo +# +# already existing elements were commented out + +#/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..3b54114 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4166 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[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.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "argminmax" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70f13d10a41ac8d2ec79ee34178d61e6f47a29c2edfe7ef1721c7383b0359e65" +dependencies = [ + "num-traits", +] + +[[package]] +name = "array-init-cursor" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed51fe0f224d1d4ea768be38c51f9f831dee9d05c163c11fba0b8c44387b1fc3" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atoi_simd" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a49e05797ca52e312a0c658938b7d00693ef037799ef7187678f212d7684cf" +dependencies = [ + "debug_unsafe", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +dependencies = [ + "serde", +] + +[[package]] +name = "blake3" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "boxcar" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f64beae40a84da1b4b26ff2761a5b895c12adc41dc25aaee1c4f2bbfe97a6e" + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +dependencies = [ + "serde", +] + +[[package]] +name = "bzip2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bea8dcd42434048e4f7a304411d9273a411f647446c1234a65ce0554923f4cff" +dependencies = [ + "libbz2-rs-sys", +] + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cc" +version = "1.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "chrono-tz" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" +dependencies = [ + "chrono", + "phf", +] + +[[package]] +name = "chumsky" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" +dependencies = [ + "hashbrown 0.14.5", + "stacker", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "comfy-table" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b03b7db8e0b4b2fdad6c551e634134e99ec000e5c8c3b6856c65e8bbaded7a3b" +dependencies = [ + "crossterm", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "compact_str" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "serde", + "static_assertions", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9" +dependencies = [ + "cookie", + "document-features", + "idna", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +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 = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags", + "crossterm_winapi", + "document-features", + "parking_lot", + "rustix", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + +[[package]] +name = "debug_unsafe" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85d3cef41d236720ed453e102153a53e4cc3d2fde848c0078a50cf249e8e3e5b" + +[[package]] +name = "deflate64" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "document-features" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +dependencies = [ + "litrs", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "email-encoding" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9298e6504d9b9e780ed3f7dfd43a61be8cd0e09eb07f7706a945b0072b6670b6" +dependencies = [ + "base64", + "memchr", +] + +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "ethnum" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca81e6b4777c89fd810c25a4be2b1bd93ea034fbe58e6a75216a34c6b82c539b" + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fast-float2" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8eb564c5c7423d25c886fb561d1e4ee69f72354d16918afa32c08811f6b6a55" + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "libz-rs-sys", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.3.2" +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 = "fs4" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8640e34b88f7652208ce9e88b1a37a2ae95227d84abec377ccd3c5cfeb141ed4" +dependencies = [ + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "h2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", + "rayon", + "serde", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "hostname" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" +dependencies = [ + "cfg-if", + "libc", + "windows-link 0.1.1", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.5.10", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "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 = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +dependencies = [ + "equivalent", + "hashbrown 0.16.0", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipaddress" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957bb9f3645d6bb7f36df99d5105b4866aa79749819d7c176a170a27dc477cbf" +dependencies = [ + "lazy_static", + "libc", + "num", + "num-integer", + "num-traits", + "regex", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jobserver" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lettre" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e13e10e8818f8b2a60f52cb127041d388b89f3a96a62be9ceaffa22262fef7f" +dependencies = [ + "base64", + "chumsky", + "email-encoding", + "email_address", + "fastrand", + "futures-util", + "hostname", + "httpdate", + "idna", + "mime", + "native-tls", + "nom", + "percent-encoding", + "quoted_printable", + "socket2 0.6.0", + "tokio", + "url", +] + +[[package]] +name = "libbz2-rs-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775bf80d5878ab7c2b1080b5351a48b2f737d9f6f8b383574eebcc22be0dfccb" + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libz-rs-sys" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221" +dependencies = [ + "zlib-rs", +] + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "lz4" +version = "1.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a20b523e860d03443e98350ceaac5e71c6ba89aea7d960769ec3ce37f4de5af4" +dependencies = [ + "lz4-sys", +] + +[[package]] +name = "lz4-sys" +version = "1.11.1+lz4-1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bd8c0d6c6ed0cd30b3652886bb8711dc4bb01d637a68105a3d5158039b418e6" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "lzma-rust2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c60a23ffb90d527e23192f1246b14746e2f7f071cb84476dd879071696c18a4a" +dependencies = [ + "crc", + "sha2", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memmap2" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" +dependencies = [ + "libc", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework 2.11.1", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "now" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d89e9874397a1f0a52fc1f197a8effd9735223cb2390e9dcc83ac6cd02923d0" +dependencies = [ + "chrono", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[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-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "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-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "object_store" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c1be0c6c22ec0817cdc77d3842f721a17fd30ab6965001415b5402a74e6b740" +dependencies = [ + "async-trait", + "base64", + "bytes", + "chrono", + "form_urlencoded", + "futures", + "http", + "http-body-util", + "humantime", + "hyper", + "itertools", + "parking_lot", + "percent-encoding", + "quick-xml", + "rand", + "reqwest", + "ring", + "serde", + "serde_json", + "serde_urlencoded", + "thiserror", + "tokio", + "tracing", + "url", + "walkdir", + "wasm-bindgen-futures", + "web-time", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags", + "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", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "phf" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piperun-bot" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "csv", + "dotenv", + "http", + "ipaddress", + "itertools", + "lettre", + "polars", + "regex", + "reqwest", + "serde", + "serde_json", + "walkdir", + "zip", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "planus" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3daf8e3d4b712abe1d690838f6e29fb76b76ea19589c4afa39ec30e12f62af71" +dependencies = [ + "array-init-cursor", + "hashbrown 0.15.3", +] + +[[package]] +name = "polars" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bc9ea901050c1bb8747ee411bc7fbb390f3b399931e7484719512965132a248" +dependencies = [ + "getrandom 0.2.16", + "getrandom 0.3.3", + "polars-arrow", + "polars-compute", + "polars-core", + "polars-error", + "polars-io", + "polars-lazy", + "polars-ops", + "polars-parquet", + "polars-sql", + "polars-time", + "polars-utils", + "version_check", +] + +[[package]] +name = "polars-arrow" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d3fe43f8702cf7899ff3d516c2e5f7dc84ee6f6a3007e1a831a0ff87940704" +dependencies = [ + "atoi_simd", + "bitflags", + "bytemuck", + "chrono", + "chrono-tz", + "dyn-clone", + "either", + "ethnum", + "getrandom 0.2.16", + "getrandom 0.3.3", + "hashbrown 0.16.0", + "itoa", + "lz4", + "num-traits", + "polars-arrow-format", + "polars-error", + "polars-schema", + "polars-utils", + "serde", + "simdutf8", + "streaming-iterator", + "strum_macros", + "version_check", + "zstd", +] + +[[package]] +name = "polars-arrow-format" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a556ac0ee744e61e167f34c1eb0013ce740e0ee6cd8c158b2ec0b518f10e6675" +dependencies = [ + "planus", + "serde", +] + +[[package]] +name = "polars-compute" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29cc7497378dee3a002f117e0b4e16b7cbe6c8ed3da16a0229c89294af7c3bf" +dependencies = [ + "atoi_simd", + "bytemuck", + "chrono", + "either", + "fast-float2", + "hashbrown 0.16.0", + "itoa", + "num-traits", + "polars-arrow", + "polars-error", + "polars-utils", + "rand", + "ryu", + "serde", + "strength_reduce", + "strum_macros", + "version_check", +] + +[[package]] +name = "polars-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48409b7440cb1a4aa84953fe3a4189dfbfb300a3298266a92a37363476641e40" +dependencies = [ + "bitflags", + "boxcar", + "bytemuck", + "chrono", + "chrono-tz", + "comfy-table", + "either", + "hashbrown 0.16.0", + "indexmap", + "itoa", + "num-traits", + "polars-arrow", + "polars-compute", + "polars-dtype", + "polars-error", + "polars-row", + "polars-schema", + "polars-utils", + "rand", + "rand_distr", + "rayon", + "regex", + "serde", + "serde_json", + "strum_macros", + "uuid", + "version_check", + "xxhash-rust", +] + +[[package]] +name = "polars-dtype" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7007e9e8b7b657cbd339b65246af7e87f5756ee9a860119b9424ddffd2aaf133" +dependencies = [ + "boxcar", + "hashbrown 0.16.0", + "polars-arrow", + "polars-error", + "polars-utils", + "serde", + "uuid", +] + +[[package]] +name = "polars-error" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a6be22566c89f6405f553bfdb7c8a6cb20ec51b35f3172de9a25fa3e252d85" +dependencies = [ + "object_store", + "parking_lot", + "polars-arrow-format", + "regex", + "signal-hook", + "simdutf8", +] + +[[package]] +name = "polars-expr" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6199a50d3e1afd0674fb009e340cbfb0010682b2387187a36328c00f3f2ca87b" +dependencies = [ + "bitflags", + "hashbrown 0.16.0", + "num-traits", + "polars-arrow", + "polars-compute", + "polars-core", + "polars-io", + "polars-ops", + "polars-plan", + "polars-row", + "polars-time", + "polars-utils", + "rand", + "rayon", + "recursive", + "regex", + "version_check", +] + +[[package]] +name = "polars-io" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be3714acdff87170141880a07f5d9233490d3bd5531c41898f6969d440feee11" +dependencies = [ + "async-trait", + "atoi_simd", + "blake3", + "bytes", + "chrono", + "fast-float2", + "fs4", + "futures", + "glob", + "hashbrown 0.16.0", + "home", + "itoa", + "memchr", + "memmap2", + "num-traits", + "object_store", + "percent-encoding", + "polars-arrow", + "polars-compute", + "polars-core", + "polars-error", + "polars-parquet", + "polars-schema", + "polars-time", + "polars-utils", + "rayon", + "regex", + "reqwest", + "ryu", + "serde", + "serde_json", + "simdutf8", + "tokio", +] + +[[package]] +name = "polars-lazy" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea136c360d03aafe56e0233495e30044ce43639b8b0360a4a38e840233f048a1" +dependencies = [ + "bitflags", + "chrono", + "either", + "memchr", + "polars-arrow", + "polars-compute", + "polars-core", + "polars-expr", + "polars-io", + "polars-mem-engine", + "polars-ops", + "polars-plan", + "polars-stream", + "polars-time", + "polars-utils", + "rayon", + "version_check", +] + +[[package]] +name = "polars-mem-engine" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f6e455ceb6e5aee7ed7d5c8944104e66992173e03a9c42f9670226318672249" +dependencies = [ + "memmap2", + "polars-arrow", + "polars-core", + "polars-error", + "polars-expr", + "polars-io", + "polars-ops", + "polars-plan", + "polars-time", + "polars-utils", + "rayon", + "recursive", +] + +[[package]] +name = "polars-ops" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b59c80a019ef0e6f09b4416d2647076a52839305c9eb11919e8298ec667f853" +dependencies = [ + "argminmax", + "base64", + "bytemuck", + "chrono", + "chrono-tz", + "either", + "hashbrown 0.16.0", + "hex", + "indexmap", + "libm", + "memchr", + "num-traits", + "polars-arrow", + "polars-compute", + "polars-core", + "polars-error", + "polars-schema", + "polars-utils", + "rayon", + "regex", + "regex-syntax", + "strum_macros", + "unicode-normalization", + "unicode-reverse", + "version_check", +] + +[[package]] +name = "polars-parquet" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93c2439d127c59e6bfc9d698419bdb45210068a6f501d44e6096429ad72c2eaa" +dependencies = [ + "async-stream", + "base64", + "bytemuck", + "ethnum", + "futures", + "hashbrown 0.16.0", + "num-traits", + "polars-arrow", + "polars-compute", + "polars-error", + "polars-parquet-format", + "polars-utils", + "serde", + "simdutf8", + "streaming-decompression", +] + +[[package]] +name = "polars-parquet-format" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c025243dcfe8dbc57e94d9f82eb3bef10b565ab180d5b99bed87fd8aea319ce1" +dependencies = [ + "async-trait", + "futures", +] + +[[package]] +name = "polars-plan" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65b4619f5c7e9b91f18611c9ed82ebeee4b10052160825c1316ecf4dbd4d97e6" +dependencies = [ + "bitflags", + "bytemuck", + "bytes", + "chrono", + "chrono-tz", + "either", + "hashbrown 0.16.0", + "memmap2", + "num-traits", + "percent-encoding", + "polars-arrow", + "polars-compute", + "polars-core", + "polars-error", + "polars-io", + "polars-ops", + "polars-time", + "polars-utils", + "rayon", + "recursive", + "regex", + "sha2", + "slotmap", + "strum_macros", + "version_check", +] + +[[package]] +name = "polars-row" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18d232f25b83032e280a279a1f40beb8a6f8fc43907b13dc07b1c56f3b11eea" +dependencies = [ + "bitflags", + "bytemuck", + "polars-arrow", + "polars-compute", + "polars-dtype", + "polars-error", + "polars-utils", +] + +[[package]] +name = "polars-schema" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f73e21d429ae1c23f442b0220ccfe773a9734a44e997b5062a741842909d9441" +dependencies = [ + "indexmap", + "polars-error", + "polars-utils", + "serde", + "version_check", +] + +[[package]] +name = "polars-sql" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e67ac1cbb0c972a57af3be12f19aa9803898863fe95c33cdd39df05f5738a75" +dependencies = [ + "bitflags", + "hex", + "polars-core", + "polars-error", + "polars-lazy", + "polars-ops", + "polars-plan", + "polars-time", + "polars-utils", + "rand", + "regex", + "serde", + "sqlparser", +] + +[[package]] +name = "polars-stream" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ff19612074640a9d65e5928b7223db76ffee63e55b276f1e466d06719eb7362" +dependencies = [ + "async-channel", + "async-trait", + "atomic-waker", + "bitflags", + "chrono-tz", + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-queue", + "crossbeam-utils", + "futures", + "memmap2", + "parking_lot", + "percent-encoding", + "pin-project-lite", + "polars-arrow", + "polars-compute", + "polars-core", + "polars-error", + "polars-expr", + "polars-io", + "polars-mem-engine", + "polars-ops", + "polars-parquet", + "polars-plan", + "polars-time", + "polars-utils", + "rand", + "rayon", + "recursive", + "slotmap", + "tokio", + "version_check", +] + +[[package]] +name = "polars-time" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddce7a9f81d5f47d981bcee4a8db004f9596bb51f0f4d9d93667a1a00d88166c" +dependencies = [ + "atoi_simd", + "bytemuck", + "chrono", + "chrono-tz", + "now", + "num-traits", + "polars-arrow", + "polars-compute", + "polars-core", + "polars-error", + "polars-ops", + "polars-utils", + "rayon", + "regex", + "strum_macros", +] + +[[package]] +name = "polars-utils" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "667c1bc2d2313f934d711f6e3b58d8d9f80351d14ea60af936a26b7dfb06e309" +dependencies = [ + "bincode", + "bytemuck", + "bytes", + "compact_str", + "either", + "flate2", + "foldhash 0.2.0", + "hashbrown 0.16.0", + "indexmap", + "libc", + "memmap2", + "num-traits", + "polars-error", + "rand", + "raw-cpuid", + "rayon", + "regex", + "rmp-serde", + "serde", + "serde_json", + "serde_stacker", + "slotmap", + "stacker", + "uuid", + "version_check", +] + +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppmd-rust" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c834641d8ad1b348c9ee86dec3b9840d805acd5f24daa5f90c788951a52ff59b" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "psm" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f" +dependencies = [ + "cc", +] + +[[package]] +name = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna", + "psl-types", +] + +[[package]] +name = "quick-xml" +version = "0.38.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2 0.6.0", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.3", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.0", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "quoted_printable" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" + +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "rand_distr" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463" +dependencies = [ + "num-traits", + "rand", +] + +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "recursive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0786a43debb760f491b1bc0269fe5e84155353c67482b9e60d0cfb596054b43e" +dependencies = [ + "recursive-proc-macro-impl", + "stacker", +] + +[[package]] +name = "recursive-proc-macro-impl" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76009fbe0614077fc1a2ce255e3a1881a2e3a3527097d5dc6d8212c585e7e38b" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "reqwest" +version = "0.12.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +dependencies = [ + "base64", + "bytes", + "cookie", + "cookie_store", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rmp" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.23.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.3.0", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +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 = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_stacker" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4936375d50c4be7eff22293a9344f8e46f323ed2b3c243e52f89138d9bb0f4a" +dependencies = [ + "serde", + "serde_core", + "stacker", +] + +[[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 = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "sqlparser" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05a528114c392209b3264855ad491fcce534b94a38771b0a0b97a79379275ce8" +dependencies = [ + "log", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "stacker" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cddb07e32ddb770749da91081d8d0ac3a16f1a569a18b20348cd371f5dead06b" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.59.0", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "streaming-decompression" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf6cc3b19bfb128a8ad11026086e31d3ce9ad23f8ea37354b31383a187c44cf3" +dependencies = [ + "fallible-streaming-iterator", +] + +[[package]] +name = "streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" + +[[package]] +name = "strength_reduce" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +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.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2 0.5.10", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[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 = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +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 = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-reverse" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6f4888ebc23094adfb574fdca9fdc891826287a6397d2cd28802ffd6f20c76" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "serde", + "wasm-bindgen", +] + +[[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 = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[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 = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.1", + "windows-result", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result", + "windows-strings 0.3.1", + "windows-targets 0.53.0", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.1", +] + +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link 0.1.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.1", +] + +[[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.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 0.52.6", + "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-targets" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[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_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[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_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[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_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[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_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[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_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[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", +] + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zip" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a05c7c36fde6c09b08576c9f7fb4cda705990f73b58fe011abf7dfb24168b" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "deflate64", + "flate2", + "getrandom 0.3.3", + "hmac", + "indexmap", + "lzma-rust2", + "memchr", + "pbkdf2", + "ppmd-rust", + "sha1", + "time", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zlib-rs" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a" + +[[package]] +name = "zopfli" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.15+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3997496 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "piperun-bot" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "groupped-repport-weekly" +path = "src/groupped_repport_weekly.rs" + +[[bin]] +name = "groupped-repport-monthly" +path = "src/groupped_repport_monthly.rs" + +[[bin]] +name = "piperun-bot" +path = "src/main.rs" + +[dependencies] +http = {version = "1.3.1"} +dotenv = {version = "0.15.0"} +serde_json = "1.0.145" +reqwest = { version = "0.12.23", features = ["json", "cookies", "blocking"] } +chrono = { version = "0.4.42" } +itertools = {version = "0.14.0"} +ipaddress = {version = "0.1.3"} +zip = { version = "6.0.0"} +walkdir = { version = "2.5.0"} +lettre = {version = "0.11.19", features = ["builder"]} +anyhow = { version = "1.0.100"} +polars = { version = "0.52.0"} +serde = { version = "1.0.228" } +csv = {version = "1.4.0"} +regex = { version = "1.12.2" } \ No newline at end of file diff --git a/Containerfile b/Containerfile new file mode 100644 index 0000000..5756a2a --- /dev/null +++ b/Containerfile @@ -0,0 +1,19 @@ +FROM docker.io/rust:1.90 AS build +COPY . /app +WORKDIR /app +RUN git checkout main +RUN cargo build --release + +# FROM docker.io/alpine:3.22.1 AS production +FROM docker.io/debian:sid AS production +COPY --from=build /app/target/release/piperun-bot /app/ +COPY crontab /app/ + +WORKDIR /app +RUN ln -sf /bin/bash /bin/sh +RUN mkdir log +RUN chmod +x piperun-bot +RUN apt update && apt install cron -y +RUN /usr/bin/crontab crontab + +CMD ["cron", "-f"] \ No newline at end of file diff --git a/FILTER.txt b/FILTER.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/FILTER.txt @@ -0,0 +1 @@ + diff --git a/PROMPT.txt b/PROMPT.txt new file mode 100644 index 0000000..b281ccc --- /dev/null +++ b/PROMPT.txt @@ -0,0 +1,45 @@ +SEGUINDO OS CRITÉRIOS QUE VÃO ESTAR ABAIXO, AVALIE ATENDIMENTOS PRESTADOS VIA CHAT PELO SETOR FINANCEIRO. + +01 (APRESENTAÇÃO) - O AGENTE DEVE SE APRESENTAR DURANTE O ATENDIMENTO. + +02 (CONFIRMAÇÃO DE E-MAIL) - O AGENTE DEVE CITAR, SOLICITAR OU CONFIRMAR O E-MAIL DO CLIENTE DURANTE A CONVERSA. + +03 (CONFIRMAÇÃO DE TELEFONE) - O AGENTE DEVE CITAR, SOLICITAR OU CONFIRMAR O TELEFONE DO CLIENTE DURANTE A CONVERSA. + +04 (PROTOCOLO) - O AGENTE DEVE CITAR, INFORMAR OU RELEMBRAR O PROTOCOLO DE ATENDIMENTO DURANTE A CONVERSA. + +05 (USO DO PORTUGUÊS) – O AGENTE DEVE APENAS PONTUAR CORRETAMENTE, ACENTUAR CORRETAMENTE E INICIAR AS FRASES COM LETRA MAIÚSCULA. +AVALIE APENAS AS MENSAGENS ENVIADAS PELO AGENTE (type: "in",). +IGNORE COMPLETAMENTE QUALQUER MENSAGEM ENVIADA PELO CLIENTE (type: "out",) +É PERMITIDO O USO DE LINGUAGEM REGIONAL COMO “TU”, “TEU”, “TUA”, POIS ISSO FAZ PARTE DA IDENTIDADE DA EMPRESA. + +06 (PACIÊNCIA E EDUCAÇÃO) - O AGENTE DEVE SER PACIENTE E EDUCADO, UTILIZANDO O USO DE AGRADECIMENTOS, SAUDAÇÕES, PEDIDOS DE DESCULPAS E LINGUAGEM RESPEITOSA QUANDO NECESSÁRIO. +COMO POR EXEMPLO: "COMO POSSO TE AJUDAR?", "POSSO TE AJUDAR EM ALGO MAIS?", "OBRIGADO PELAS INFORMAÇÕES", "VOU VERIFICAR, AGUARDE UM MOMENTO POR GENTILEZA", "DESCULPE, MAS NÃO TE COMPREENDI", "PODERIA EXPLICAR DE NOVO?". + +07 (DISPONIBILIDADE) - O AGENTE DEVE SE COLOCAR À DISPOSIÇÃO DO CLIENTE, DEIXANDO CLARO QUE A EMPRESA ESTÁ SEMPRE DISPONÍVEL PARA AJUDAR. +COMO POR EXEMPLO "CASO TENHA ALGUMA DÚVIDA, NOS CONTATE. A EQUIPE DA NOVANET ESTÁ SEMPRE AQUI PARA TE AJUDAR." + +08 (ESCLARECIMENTO) - O AGENTE DEVE MANTER O CLIENTE INFORMADO DURANTE TODO O ATENDIMENTO, DEIXANDO CLARO O QUE ESTÁ SENDO FEITO, QUAL É A SITUAÇÃO ATUAL E O QUE ACONTECERÁ EM SEGUIDA. +ELE DEVE TRANSMITIR SEGURANÇA, MOSTRANDO QUE ESTÁ ACOMPANHANDO O CASO E EXPLICANDO AS ETAPAS DO PROCESSO. + +----------------------------------- + +As mensagens do chat estão estruturadas no formato JSON com os campos: +- **message**: conteúdo da mensagem +- **sent_at**: horário de envio +- **type**: tipo da mensagem ('IN', 'OUT' ou 'SYSTEM'). As mensagens do agente são sempre do tipo 'OUT'. +- **user_name**: nome do usuário que enviou a mensagem. Não considere respostas do PipeBot como do agente + +O fluxo de mensagens inicia-se com o cliente interagindo com o BOT, e então a mensagem é transferida para o atendente. + +Em cada categoria, atribua 0 quando o agente não tiver atendido o critétio e 1 caso tenha atendido. Não atribua nenhum outro valor além destes dois . + +Devem ser avaliados somente os agentes que o nome de usuario inicie com "NOC -" . + +A sua resposta deve ser apenas uma tabela CSV e nada mais, utilizando como separador o caracter ';' com as colunas: CATEGORIA;PONTOS;MOTIVO;EVIDENCIA; on> + +Portanto crie essas colunas para todo agente que for avaliado. + +Na saída CSV, na coluna categoria, utilize o nome correspondente ao invés do número + +A seguir estão as mensagens do atendimento, em JSON, avalie e retorne apenas um CSV. diff --git a/PROMPT_DATA_SANITIZATION.txt b/PROMPT_DATA_SANITIZATION.txt new file mode 100644 index 0000000..6176d4f --- /dev/null +++ b/PROMPT_DATA_SANITIZATION.txt @@ -0,0 +1,102 @@ +Abaixo está a avaliação de um atendimento que foi realizado. Eu preciso que a formatação fique consistente e padronizada. +Padronize o arquivo CSV da seguinte forma, deixando apenas as colunas listadas. +Título: CATEGORIA;PONTOS +A sua resposta deve ser apenas o CSV com a formatação corrigida, nada mais deve ser incluído na sua resposta, nem mesmo notas sobre a resposta. +Se não for possível padronizar o arquivo de entrada de acordo com as instruções fornecidas a resposta deve ser o CSV com o campo de pontuação vazio. +As categorias são: APRESENTAÇÃO, CONFIRMAÇÃO DE E-MAIL, CONFIRMAÇÃO DE TELEFONE, PROTOCOLO, USO DO PORTUGUÊS, PACIÊNCIA E EDUCAÇÃO, DISPONIBILIDADE, ESCLARECIMENTO +A coluna pontos deve ter apenas os valores 0, 1 ou vazio, se no arquivo de entrada não houver a avaliação da categoria (somente neste caso), a coluna de pontos deve ser vazio. + +Aqui estão alguns exemplos de formatação de como deve ser a sua resposta: +Exemplo 01: +Dado o seguinte arquivo de entrada: +APRESENTAÇÃO;1;O agente se apresentou ao cliente.;Boa noite, me chamo Ander.;Certo, um bom final de semana! 😊 +CONFIRMAÇÃO DE E-MAIL;1;O agente pediu confirmação do e-mail.;Para manter o cadastro atualizado, poderia me confirmar se o e-mail continua sendo janainads.sls@gmail.com e se o telefone do titular do cadastro permanece (53) 98446-2208?;Obrigado pela confirmação. +CONFIRMAÇÃO DE TELEFONE;1;O agente pediu confirmação do telefone.;Para manter o cadastro atualizado, poderia me confirmar se o e-mail continua sendo janainads.sls@gmail.com e se o telefone do titular do cadastro permanece (53) 98446-2208?;Obrigado pela confirmação. +PROTOCOLO;1;O agente informou o protocolo.;Aqui está o protocolo do teu atendimento: 2510.3624;Boa noite, me chamo Ander. +USO DO PORTUGUÊS;1;O agente utilizou português correto.;Aqui está o protocolo do teu atendimento: 2510.3624;Para manter o cadastro atualizado, poderia me confirmar se o e-mail continua sendo janainads.sls@gmail.com e se o telefone do titular do cadastro permanece (53) 98446-2208? +PACIÊNCIA E EDUCAÇÃO;1;O agente foi paciente e educado.;Obrigado pela confirmação.;Certo, um bom final de semana! 😊 +DISPONIBILIDADE;1;O agente demonstrou disponibilidade.;Caso tenha alguma dúvida, nos contate. A equipe da NovaNet está sempre aqui para te ajudar.😊;Certo, um bom final de semana! 😊 +ESCLARECIMENTO;1;O agente manteve o cliente informado durante todo o processo.; Esclareceu situaçãoo de instabilidade e envio de fatura, mantendo cliente informado. Atualizou cliente sobre o andamento e próximos passos. + +100% + +A resposta sua deve ser: +```csv +CATEGORIA;PONTOS +APRESENTAÇÃO;1 +CONFIRMAÇÃO DE E-MAIL;1 +CONFIRMAÇÃO DE TELEFONE;1 +PROTOCOLO;1 +USO DO PORTUGUÊS;1 +PACIÊNCIA E EDUCAÇÃO;1 +DISPONIBILIDADE;1 +ESCLARECIMENTO;1 +``` + +Exemplo 02: +Dado o seguinte arquivo de entrada: +01,1,Apresentação,"Boa tarde, me chamo Ander. (12:10:05)" +02,1,Confirmou e‑mail,"Para manter o cadastro atualizado, poderia me confirmar se o e‑mail continua sendo mmaicomvoss@gmail.com? (13:01:40)" +03,1,Confirmou telefone,"para manter o cadastro atualizado, poderia me confirmar se o telefone continua (53) 98414‑3027? (13:01:40)" +04,1,Informa protocolo,"Aqui está o protocolo do teu atendimento: 2510.2749 (12:10:06)" +05,1,Uso correto do português,"Todas as mensagens foram escritas em português formal e correto, inclusive com 'tu' permitido. (12:10:05‑13:03:08)" +06,1,Uso de linguagem paciente e educada,"Aguarde meu retorno, por gentileza; me informe, por gentileza; obrigado pela confirmação. (12:10:13‑13:03:07)" +07,1,Disponibilidade expressa,"Caso tenha alguma dúvida, nos contate. A equipe da NovaNet está sempre aqui para te ajudar. (13:03:08)" +08,1,Esclarecimento,"Um momento irei verificar os valores.Andressa, tem valores em aberto conosco desde 2021, em caso de pagamento hoje o valor é de R$358,95. (12:25:56‑12:26:06)" +09,2,Tempo de espera excedido, 2 ocorrências,"Intervalos superiores a 5 min: 12:55:17‑13:01:15 (5 min 58 s) e 12:27:51‑12:54:11 (26 min 20 s)" + +A resposta sua deve ser: +```csv +CATEGORIA;PONTOS +APRESENTAÇÃO;1 +CONFIRMAÇÃO DE E-MAIL;1 +CONFIRMAÇÃO DE TELEFONE;1 +PROTOCOLO;1 +USO DO PORTUGUÊS;1 +PACIÊNCIA E EDUCAÇÃO;1 +DISPONIBILIDADE;1 +ESCLARECIMENTO;1 +``` + +Exemplo 03: +Dado o seguinte arquivo de entrada: +Identificação e abertura do atendimento,1, +Confirmação de dados do cliente,1, +Verificação do histórico e do plano do cliente,1, +Escala de serviço,1, +Encerramento de atendimento,1, +Follow-up,1, +Comunicação com o cliente,1, +Tempo de Resposta,1 + +A sua resposta deve ser vazia neste caso, pois a entrada não fornece a pontuação adequada para os critérios. +Ou seja o retorno deve ser o seguinte +```csv +CATEGORIA;PONTOS +APRESENTAÇÃO; +CONFIRMAÇÃO DE E-MAIL; +CONFIRMAÇÃO DE TELEFONE; +PROTOCOLO; +USO DO PORTUGUÊS; +PACIÊNCIA E EDUCAÇÃO; +DISPONIBILIDADE; +ESCLARECIMENTO; +``` + + +Aqui um exemplo de formatação de como não deve ser sua resposta +Erro 01: Não utilizar o formato estritamente como fornecido nas instruções e copiar da entrada que está sendo avaliada +```csv +CATEGORIA;PONTOS +APRESENTAÇÃO;1 +Confirmação de e-mail;1 +CONFIRMAÇÃO DE TELEFONE;1 +PROTOCOLO;1 +USO DO PORTUGUÊS;1 +PACIÊNCIA E EDUCAÇÃO;1 +DISPONIBILIDADE;1 +ESCLARECIMENTO;1 +``` + +Abaixo está a avaliação que deve ser processada +-------------------------------- diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..c6510ad --- /dev/null +++ b/compose.yaml @@ -0,0 +1,24 @@ +version: "3.9" + +networks: + piperun_bot_default: + external: true + ipv6_enabled: true + +services: + piperun-bot: + name: piperun-bot + restart: always + build: + context: . + dockerfile: Containerfile + volumes: + - ./crontab:/app/crontab:ro + - ./PROMPT.txt:/app/PROMPT.txt + - ./FILTER.txt:/app/FILTER.txt + - ./evaluations:/app/evaluations + - ./.env:/app/.env + - ./log/container:/app/log + - /etc/localtime:/etc/localtime:ro # sync time between machine and container + networks: + - piperun_bot_default diff --git a/crontab b/crontab new file mode 100644 index 0000000..477592a --- /dev/null +++ b/crontab @@ -0,0 +1,13 @@ +# +# Output of the crontab jobs (including errors) is sent through +# email to the user the crontab file belongs to (unless redirected). +# +# For example, you can run a backup of all your user accounts +# at 5 a.m every week with: +# 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/ +# +# For more information see the manual pages of crontab(5) and cron(8) +# +# m h dom mon dow command +#0 1 * * * piperun-bot # Execute the piperun-bot everyday at 01h00min +25 0 * * * cd /app; ./piperun-bot >/app/log/stdout.log 2>/app/log/stderr.log # Execute the piperun-bot every minute diff --git a/src/.env.example b/src/.env.example new file mode 100644 index 0000000..1e36132 --- /dev/null +++ b/src/.env.example @@ -0,0 +1,7 @@ +OLLAMA_URL=localhost +OLLAMA_PORT=11432 +PIPERUN_API_URL=novanet.cxm.pipe.run +PIPERUN_CLIENT_ID=0 +PIPERUN_CLIENT_SECRET= +PIPERUN_BOT_USERNAME= +PIPERUN_BOT_PASSWORD= \ No newline at end of file diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..cd3edca --- /dev/null +++ b/src/config.rs @@ -0,0 +1,7 @@ +const OLLAMA_URL: str = "localhost"; +const OLLAMA_PORT: str = 11434; +const PIPERUN_API_URL: str = "novanet.cxm.pipe.run"; +const PIPERUN_CLIENT_ID: i32 = 0; +const PIPERUN_CLIENT_SECRET: str = ""; +const PIPERUN_BOT_USERNAME: str = ""; +const PIPERUN_BOT_PASSWORD: str = ""; diff --git a/src/fin_main.rs b/src/fin_main.rs new file mode 100644 index 0000000..e4ed571 --- /dev/null +++ b/src/fin_main.rs @@ -0,0 +1,673 @@ +use std::{any::Any, env, fmt::format, iter, time::Duration}; + +use chrono::{self, Timelike}; +use dotenv; +use ipaddress; +use itertools::{self, Itertools}; +use lettre::{self, message}; +use reqwest; +use serde_json::{self, json}; + +use std::io::prelude::*; + +pub mod send_mail_util; +pub mod zip_directory_util; + +fn main() -> anyhow::Result<()> { + match dotenv::dotenv().ok() { + Some(_) => println!("Environment variables loaded from .env file"), + None => eprintln!("Failed to load .env file, using defaults"), + } + + // Read environment variables + let OLLAMA_URL = env::var("OLLAMA_URL").unwrap_or("localhost".to_string()); + let OLLAMA_PORT = env::var("OLLAMA_PORT") + .unwrap_or("11432".to_string()) + .parse::() + .unwrap_or(11432); + let PIPERUN_API_URL = env::var("PIPERUN_API_URL").expect("PIPERUN_API_URL has not been set!"); + let PIPERUN_CLIENT_ID = env::var("PIPERUN_CLIENT_ID") + .expect("PIPERUN_CLIENT_ID has not been set!") + .parse::() + .unwrap_or(0); + let PIPERUN_CLIENT_SECRET = + env::var("PIPERUN_CLIENT_SECRET").expect("PIPERUN_CLIENT_SECRET has not been set!"); + let PIPERUN_BOT_USERNAME = + env::var("PIPERUN_BOT_USERNAME").expect("PIPERUN_BOT_USERNAME has not been set!"); + let PIPERUN_BOT_PASSWORD = + env::var("PIPERUN_BOT_PASSWORD").expect("PIPERUN_BOT_PASSWORD has not been set!"); + let OLLAMA_AI_MODEL = env::var("OLLAMA_AI_MODEL").expect("OLLAMA_AI_MODEL has not been set!"); + let MINIMUM_NUMBER_OF_MESSAGES_TO_EVALUATE = env::var("MINIMUM_NUMBER_OF_MESSAGES_TO_EVALUATE") + .expect("MINIMUM_NUMBER_OF_MESSAGES_TO_EVALUATE has not been set!") + .parse::() + .unwrap_or(10); + let MINIMUM_NUMBER_OF_MESSAGES_WITH_AGENT_TO_EVALUATE = + env::var("MINIMUM_NUMBER_OF_MESSAGES_WITH_AGENT_TO_EVALUATE") + .expect("MINIMUM_NUMBER_OF_MESSAGES_WITH_AGENT_TO_EVALUATE has not been set!") + .parse::() + .unwrap_or(12); + let BOT_EMAIL = env::var("BOT_EMAIL").expect("BOT_EMAIL has not been set!"); + let BOT_EMAIL_PASSWORD = + env::var("BOT_EMAIL_PASSWORD").expect("BOT_EMAIL_PASSWORD has not been set!"); + + // Print the configuration + println!("OLLAMA_URL: {}", OLLAMA_URL); + println!("OLLAMA_PORT: {}", OLLAMA_PORT); + println!("OLLAMA_AI_MODEL: {}", OLLAMA_AI_MODEL); + println!("PIPERUN_API_URL: {}", PIPERUN_API_URL); + println!("PIPERUN_CLIENT_ID: {}", PIPERUN_CLIENT_ID); + println!("PIPERUN_CLIENT_SECRET: {}", PIPERUN_CLIENT_SECRET); + println!("PIPERUN_BOT_USERNAME: {}", PIPERUN_BOT_USERNAME); + println!("PIPERUN_BOT_PASSWORD: {}", PIPERUN_BOT_PASSWORD); + + let ip_address = ipaddress::IPAddress::parse(OLLAMA_URL.to_string()); + let OLLAMA_SANITIZED_IP = match ip_address { + Ok(ip) => { + if ip.is_ipv4() { + OLLAMA_URL.clone() + } else { + format!("[{}]", OLLAMA_URL.clone()) + } + } + Err(e) => OLLAMA_URL.clone(), + }; + + // Send the authentication request + let client = reqwest::blocking::Client::new(); + + let auth_request = client + .post(format!("https://{}/oauth/token", PIPERUN_API_URL)) + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .body( + serde_json::json!({ + "grant_type": "password", + "client_id": PIPERUN_CLIENT_ID, + "client_secret": PIPERUN_CLIENT_SECRET, + "username": PIPERUN_BOT_USERNAME, + "password": PIPERUN_BOT_PASSWORD, + }) + .to_string(), + ); + + println!("Sending authentication request to Piperun API..."); + println!("{:?}", auth_request); + + let response = auth_request.send(); + let access_token = match response { + Ok(resp) => { + if resp.status().is_success() { + let json: serde_json::Value = resp.json()?; + // println!("Authentication successful: {:?}", json); + + // Extract the access token + if let Some(access_token) = json.get("access_token") { + println!("Access Token: {}", access_token); + access_token + .as_str() + .expect("Failed to get token") + .to_string() + } else { + eprintln!("Access token not found in response"); + panic!("Failed to retrieve access token from Piperun API"); + } + } else { + eprintln!("Authentication failed: {}", resp.status()); + let json: serde_json::Value = resp.json()?; + eprintln!("Response body: {:?}", json); + + panic!("Failed to authenticate with Piperun API"); + } + } + Err(e) => { + eprintln!("Error sending authentication request: {}", e); + panic!("Failed to send authentication request to Piperun API"); + } + }; + + // Get the current day in the format YYYY-MM-DD + let current_date = chrono::Local::now(); + let formatted_date = current_date.format("%Y-%m-%d").to_string(); + + println!("Current date: {}", formatted_date); + + // Get the day before the current date + let day_before = current_date + .checked_sub_signed(chrono::Duration::days(1)) + .expect("Failed to get the day before"); + let formatted_day_before = day_before.format("%Y-%m-%d").to_string(); + + println!("Day before: {}", formatted_day_before); + + let day_before_at_midnight = day_before + .with_hour(0) + .unwrap() + .with_minute(0) + .unwrap() + .with_second(0) + .unwrap(); + let formatted_day_before_at_midnight = + day_before_at_midnight.format("%Y-%m-%d %H:%M").to_string(); + + let day_before_at_23_59_59 = day_before + .with_hour(23) + .unwrap() + .with_minute(59) + .unwrap() + .with_second(59) + .unwrap(); + let formatted_day_before_at_23_59_59 = + day_before_at_23_59_59.format("%Y-%m-%d %H:%M").to_string(); + + println!( + "Day before at midnight: {}, Day before at 23:59:59: {}", + formatted_day_before_at_midnight, formatted_day_before_at_23_59_59 + ); + + let formatted_day_before = day_before_at_midnight.format("%Y-%m-%d").to_string(); + + // Create a folder named with the day_before + if !std::fs::exists(format!("./evaluations/{formatted_day_before}")).unwrap() { + std::fs::create_dir(format!("./evaluations/{formatted_day_before}")) + .expect("Failed to create directory") + } + + // Create the response time folder + if !std::fs::exists(format!( + "./evaluations/{formatted_day_before}/response_time.csv" + )) + .unwrap() + { + let mut response_time_file = std::fs::File::create_new(format!( + "./evaluations/{formatted_day_before}/response_time.csv" + )) + .expect("Failed to response_time.csv"); + } + + // Read system prompt + let prompt = std::fs::read_to_string("PROMPT.txt").unwrap(); + let filter_file_contents = std::fs::read_to_string("FILTER.txt").unwrap_or(String::new()); + let filter_keywords = filter_file_contents + .split("\n") + .filter(|keyword| !keyword.is_empty()) + .collect::>(); + + let talks_array = get_piperun_chats_on_date( + &PIPERUN_API_URL, + &client, + &access_token, + formatted_day_before_at_midnight, + formatted_day_before_at_23_59_59, + ); + + println!("Number of consolidated talks: {}", talks_array.len()); + + let talk_ids = talks_array + .iter() + .cloned() + .map(|value| { + serde_json::from_value::(value).expect("Failed to parse the JSON") + ["id"] + .clone() + .to_string() + }) + .collect::>(); + + println!("IDS {:?}", talk_ids); + + // Gather messages and apply filtering + let filtered_chats = talk_ids + .iter() + .cloned() + .map(|talk_id| { + let talk_id_get_request = client + .get(format!("https://{}/api/talk_histories", PIPERUN_API_URL)) + .bearer_auth(&access_token) + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .query(&[ + ("talk_id", talk_id), + ("type", "a".to_string()), + ("only_view", "1".to_string()), + ]); + + let talk_id_get_result = talk_id_get_request.send(); + + return talk_id_get_result; + }) + .filter_map_ok(|result| { + let json = result + .json::() + .expect("Failed to deserialize response to JSON") + .to_owned(); + let talk_histories = &json["talk_histories"]; + let data = &talk_histories["data"]; + + // Filter chats that have very few messages + let talk_lenght = talk_histories + .as_array() + .expect("Wrong message type received from talk histories") + .len(); + if talk_lenght < MINIMUM_NUMBER_OF_MESSAGES_TO_EVALUATE { + return None; + } + + // Filter chats that have less that specified ammount of talks with support agent form the last queue transfer + let found = talk_histories + .as_array() + .expect("Wrong message type received from talk histories") + .into_iter() + .enumerate() + .find(|(pos, message_object)| { + let message = message_object["message"] + .as_str() + .expect("Failed to decode message as string"); + let found = message.find( + "Atendimento transferido para a fila [NovaNet -> Atendimento -> Financeiro NVL2]", + ); + found.is_some() + }); + + match found { + None => { + return None; + } + Some(pos) => { + let pos_found = pos.0; + if pos_found < MINIMUM_NUMBER_OF_MESSAGES_WITH_AGENT_TO_EVALUATE { + return None; + } + } + }; + + // Filter Bot finished chats + if json["agent"]["user"]["name"] + .as_str() + .unwrap_or("unknown_user") + == "PipeBot" + { + return None; + } + + // Apply keyword based filtering + let filter_keywords_found = talk_histories + .as_array() + .expect("Wrong message type received from talk histories") + .into_iter() + .any(|message_object| { + let message = message_object["message"] + .as_str() + .expect("Failed to decode message as string"); + let found1 = filter_keywords.iter().any(|keyword| { + message + .to_uppercase() + .find(&keyword.to_uppercase()) + .is_some() + }); + let found2 = message_object["is_template"] + .as_bool() + .unwrap_or(true); + let found = found1 || found2; + found + }); + + if filter_keywords_found { + return None; + } + + return Some(json); + }); + + // Calculate the response time in seconds + let response_time = filtered_chats + .clone() + .map(|messages| { + let json = messages.unwrap(); + let talk_histories = &json["talk_histories"]; + + // dbg!(&talk_histories); + + // talk_histories.as_array().unwrap().into_iter().enumerate().for_each(|(pos, message_obj)|{println!("{}: {}", pos, message_obj["message"])}); + + // find the bot transfer message + let bot_transfer_message = talk_histories + .as_array() + .expect("Wrong message type received from talk histories") + .into_iter() + .enumerate() + .filter(|(pos, message_object)| { + let user_name = message_object["user"]["name"] + .as_str() + .expect("Failed to decode message as string"); + user_name == "PipeBot".to_string() + }) + .find(|(pos, message_object)| { + let message = message_object["message"] + .as_str() + .expect("Failed to decode message as string"); + //let found = message.find("Atendimento transferido para a fila [NovaNet -> Atendimento -> Financeiro NVL2]"); + let found = message.find("Atendimento entregue da fila de espera para o agente [FIN - "); + found.is_some() + }); + + // Find first agent message sent after the last bot message + let (pos, transfer_message) = + bot_transfer_message.expect("Failed to get the transfer bot message position"); + + let msg = talk_histories + .as_array() + .expect("Wrong message type received from talk histories") + .into_iter() + .take(pos) + .rev() + .filter(|message| { + message["type"] == "out".to_string() + // && message["user"]["name"] != "PipeBot".to_string() + && message["user"]["name"].as_str().map_or(false, |name| name.starts_with("FIN -")) + }) + .take(1) + .collect_vec(); + + //if msg[0] { + // return None; + //} + let agent_first_message = msg[0]; + + // Calculate time difference between bot message and agent message + let date_user_message_sent = agent_first_message["sent_at"].as_str().unwrap(); + + let format = "%Y-%m-%d %H:%M:%S"; + + let date_user_message_sent_parsed = + match chrono::NaiveDateTime::parse_from_str(date_user_message_sent, format) { + Ok(dt) => dt, + Err(e) => { + println!("Error parsing DateTime: {}", e); + panic!("Failed parsing date") + } + }; + + let date_transfer_message_sent_parsed = match chrono::NaiveDateTime::parse_from_str( + transfer_message["sent_at"].as_str().unwrap(), + format, + ) { + Ok(dt) => dt, + Err(e) => { + println!("Error parsing DateTime: {}", e); + panic!("Failed parsing date") + } + }; + + let response_time = (date_user_message_sent_parsed - date_transfer_message_sent_parsed) + .as_seconds_f32(); + let name = agent_first_message["user"]["name"] + .as_str() + .unwrap() + .to_owned(); + let id = json["tracking_number"].as_str().unwrap_or("").to_owned(); + let bot_transfer_date = date_transfer_message_sent_parsed.to_owned(); + let user_response_date = date_user_message_sent.to_owned(); + println!( + "response_time: {}s", + (date_user_message_sent_parsed - date_transfer_message_sent_parsed) + .as_seconds_f32() + ); + + format!( + "{};{};{};{};{}", + name, id, response_time, bot_transfer_date, user_response_date + ) + }) + .reduce(|acc, e| format!("{}\n{}", acc, e)) + .unwrap_or("".to_string()); + + // return Ok(()); + // Open file and write to it + let header = "NOME;ID_TALK;TEMPO DE RESPOSTA;TRANFERENCIA PELO BOT;PRIMEIRA RESPOSTA DO AGENTE"; + let mut response_time_file = std::fs::OpenOptions::new() + .write(true) + .open(format!( + "./evaluations/{formatted_day_before}/response_time.csv" + )) + .expect("Failed to open response time file for write"); + response_time_file + .write_all(format!("{header}\n{response_time}").as_bytes()) + .expect("Failed to write header to file"); + + filtered_chats.clone().skip(0).for_each(|result| { + let json = result.unwrap(); + let talk_histories = &json["talk_histories"]; + let data = &talk_histories["data"]; + + let talk = talk_histories + .as_array() + .expect("Wrong message type received from talk histories") + .iter() + .rev() + .map(|message_object| { + let new_json_filtered = format!( + "{{ + message: {}, + sent_at: {}, + type: {}, + user_name: {} +}}", + message_object["message"], + message_object["sent_at"], + message_object["type"], + message_object["user"]["name"] + ); + // println!("{}", new_json_filtered); + new_json_filtered + }) + .reduce(|acc, e| format!("{acc}\n{e}")) + .expect("Error extracting talk"); + + println!("{prompt}\n {talk}"); + + let ollama_api_request = client + .post(format!( + "http://{OLLAMA_SANITIZED_IP}:{OLLAMA_PORT}/api/generate" + )) + .body( + serde_json::json!({ + "model": OLLAMA_AI_MODEL, + "prompt": format!("{prompt} \n{talk}"), + // "options": serde_json::json!({"temperature": 0.1}), + "stream": false, + }) + .to_string(), + ); + + let result = ollama_api_request.timeout(Duration::from_secs(3600)).send(); + + match result { + Ok(response) => { + println!("Response: {:?}", response); + let response_json = response + .json::() + .expect("Failed to deserialize response to JSON"); + println!("{}", response_json); + let ai_response = response_json["response"] + .as_str() + .expect("Failed to get AI response as string"); + println!("AI Response: {}", ai_response); + + let csv_response = ai_response.replace("```csv\n", "").replace("```", ""); + + // Save the CSV response to a file + + let user_name = &json["agent"]["user"]["name"] + .as_str() + .unwrap_or("unknown_user"); + let talk_id = &json["id"].as_u64().unwrap_or(0); + let tracking_number = &json["tracking_number"].as_str().unwrap_or(""); + std::fs::write( + format!( + "./evaluations/{}/{} - {} - {}.csv", + formatted_day_before, user_name, talk_id, tracking_number + ), + csv_response, + ) + .expect("Unable to write file"); + std::fs::write( + format!( + "./evaluations/{}/{} - {} - {} - prompt.txt", + formatted_day_before, user_name, talk_id, tracking_number + ), + format!("{prompt} \n{talk}"), + ) + .expect("Unable to write file"); + } + Err(error) => { + println!("Error {error}"); + } + }; + }); + + // Compress folder into zip + let source_dir_str = format!("./evaluations/{formatted_day_before}"); + let output_zip_file_str = format!("./evaluations/{formatted_day_before}.zip"); + let source_dir = std::path::Path::new(source_dir_str.as_str()); + let output_zip_file = std::path::Path::new(output_zip_file_str.as_str()); + zip_directory_util::zip_directory_util::zip_source_dir_to_dst_file(source_dir, output_zip_file); + + // Send folder to email + //let recipients = "Wilson da Conceição Oliveira , Isadora G. Moura de Moura "; + let recipients = "Wilson da Conceição Oliveira "; + println!("Trying to send email... Recipients {recipients}"); + + send_mail_util::send_mail_util::send_email( + &format!("Avaliacao atendimentos {formatted_day_before}"), + &BOT_EMAIL, + &BOT_EMAIL_PASSWORD, + recipients, + &output_zip_file_str, + ); + + return Ok(()); +} + +fn get_piperun_chats_on_date( + PIPERUN_API_URL: &String, + client: &reqwest::blocking::Client, + access_token: &String, + formatted_day_before_at_midnight: String, + formatted_day_before_at_23_59_59: String, +) -> Vec { + let start_of_talk_code: String = "talk_start".to_string(); + let support_queue_id: String = "16".to_string(); + + // API V2 + let report_type = "consolidated".to_string(); + let page = "1".to_string(); + let per_page = "15".to_string(); + + let talks_request = client + .get(format!("https://{}/api/v2/reports/talks", PIPERUN_API_URL)) + .bearer_auth(access_token) + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .query(&[ + ("page", page.clone()), + ("perPage", per_page.clone()), + ("report_type", report_type.clone()), + ("start_date", formatted_day_before_at_midnight.clone()), + ("end_date", formatted_day_before_at_23_59_59.clone()), + ("date_range_type", start_of_talk_code.clone()), + ("queue_id[]", support_queue_id.clone()), + ]); + + println!("Sending request for consolidated talks... {talks_request:?}"); + let talks_response = talks_request.send(); + + let json_response = match talks_response { + Ok(resp) => { + if resp.status().is_success() { + let json: serde_json::Value = resp.json().unwrap(); + json + } else { + eprintln!("Failed to get consolidated talks: {}", resp.status()); + let json: serde_json::Value = resp.json().unwrap(); + eprintln!("Response body: {:?}", json); + panic!("Failed to retrieve consolidated talks from Piperun API"); + } + } + Err(e) => { + eprintln!("Error: {e}"); + panic!("Failed to send the request for talks to PipeRUN API"); + } + }; + + let mut aggregated_talks = json_response["data"] + .as_array() + .expect("Failed to parse messages as array") + .to_owned(); + + let current_page = json_response["current_page"] + .as_i64() + .expect("Failed to obtain current page number"); + let last_page = json_response["last_page"] + .as_i64() + .expect("Failed to obtain current page number"); + + if current_page == last_page { + return aggregated_talks; + } + + let mut all_other_messages = (current_page..last_page) + .into_iter() + .map(|page| { + let page_to_request = page + 1; + let talks_request = client + .get(format!("https://{}/api/v2/reports/talks", PIPERUN_API_URL)) + .bearer_auth(access_token) + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .query(&[ + ("page", page_to_request.to_string()), + ("perPage", per_page.clone()), + ("report_type", report_type.clone()), + ("start_date", formatted_day_before_at_midnight.clone()), + ("end_date", formatted_day_before_at_23_59_59.clone()), + ("date_range_type", start_of_talk_code.clone()), + ("queue_id[]", support_queue_id.clone()), + ]); + + println!("Sending request for consolidated talks... {talks_request:?}"); + let talks_response = talks_request.send(); + + let json_response = match talks_response { + Ok(resp) => { + if resp.status().is_success() { + let json: serde_json::Value = resp.json().unwrap(); + json + } else { + eprintln!("Failed to get consolidated talks: {}", resp.status()); + let json: serde_json::Value = resp.json().unwrap(); + eprintln!("Response body: {:?}", json); + panic!("Failed to retrieve consolidated talks from Piperun API"); + } + } + Err(e) => { + eprintln!("Error: {e}"); + panic!("Failed to send the request for talks to PipeRUN API"); + } + }; + + let aggregated_talks = json_response["data"] + .as_array() + .expect("Failed to parse messages as array") + .to_owned(); + + return aggregated_talks; + }) + .reduce(|mut this, mut acc| { + acc.append(&mut this); + acc + }) + .expect("Failed to concatenate all vectors of messages"); + + aggregated_talks.append(&mut all_other_messages); + aggregated_talks +} diff --git a/src/groupped_repport_monthly.rs b/src/groupped_repport_monthly.rs new file mode 100644 index 0000000..f556b6e --- /dev/null +++ b/src/groupped_repport_monthly.rs @@ -0,0 +1,544 @@ +use std::fmt::Debug; + +use chrono::{Datelike, NaiveDate}; +use itertools::Itertools; +use polars::prelude::*; +use reqwest; +use std::env; +use std::time::Duration; + +use csv; + +pub mod send_mail_util; +pub mod zip_directory_util; + +#[derive(Debug, serde::Deserialize)] +struct CsvHeader { + CATEGORIA: String, + PONTOS: Option, +} + +#[derive(Debug, serde::Deserialize)] +struct CsvEvaluation { + APRESENTACAO: u8, + CONFIRMACAO_DE_EMAIL: u8, + CONFIRMACAO_DE_TELEFONE: u8, + PROTOCOLO: u8, + USO_DO_PORTUGUES: u8, + PACIENCIA_E_EDUCACAO: u8, + DISPONIBILIDADE: u8, + ESCLARECIMENTO: u8, + ID_TALK: String, +} + +// --- ADICAO PARA AGRUPAR O response_time.csv --- +#[derive(Debug, serde::Deserialize)] +struct ResponseTimeRecord { + NOME: String, + ID_TALK: String, + #[serde(rename = "TEMPO DE RESPOSTA")] + TEMPO_DE_RESPOSTA: u32, + #[serde(rename = "TRANFERENCIA PELO BOT")] + TRANFERENCIA_PELO_BOT: String, + #[serde(rename = "PRIMEIRA RESPOSTA DO AGENTE")] + PRIMEIRA_RESPOSTA_DO_AGENTE: String, +} +// --- FIM DA ADIÇÃO --- + +fn main() { + match dotenv::dotenv().ok() { + Some(_) => println!("Environment variables loaded from .env file"), + None => eprintln!("Failed to load .env file, using defaults"), + } + + // Read environment variables + let OLLAMA_URL = env::var("OLLAMA_URL").unwrap_or("localhost".to_string()); + let OLLAMA_PORT = env::var("OLLAMA_PORT") + .unwrap_or("11432".to_string()) + .parse::() + .unwrap_or(11432); + let OLLAMA_AI_MODEL_DATA_SANITIZATION = env::var("OLLAMA_AI_MODEL_DATA_SANITIZATION") + .expect("Missing environment variable OLLAMA_AI_MODEL_DATA_SANITIZATION"); + let BOT_EMAIL = env::var("BOT_EMAIL").expect("BOT_EMAIL has not been set!"); + let BOT_EMAIL_PASSWORD = + env::var("BOT_EMAIL_PASSWORD").expect("BOT_EMAIL_PASSWORD has not been set!"); + + let ip_address = ipaddress::IPAddress::parse(OLLAMA_URL.to_string()); + let OLLAMA_SANITIZED_IP = match ip_address { + Ok(ip) => { + if ip.is_ipv4() { + OLLAMA_URL.clone() + } else { + format!("[{}]", OLLAMA_URL.clone()) + } + } + Err(e) => OLLAMA_URL.clone(), + }; + + // Get the current day in the format YYYY-MM-DD + let current_date = chrono::Local::now(); + let formatted_date = current_date.format("%Y-%m-%d").to_string(); + + let current_date = chrono::Local::now(); + // let first_day_of_current_month = NaiveDate::fro + + let current_day_of_last_month = current_date + .checked_sub_months(chrono::Months::new(1)) + .expect("Failed to subtract one month"); + + let first_day_of_last_month = NaiveDate::from_ymd_opt( + current_day_of_last_month.year(), + current_day_of_last_month.month(), + 1, + ) + .expect("Failed to obtain date"); + let last_day_of_last_month = NaiveDate::from_ymd_opt( + current_day_of_last_month.year(), + current_day_of_last_month.month(), + current_day_of_last_month.num_days_in_month() as u32, + ) + .expect("Failed to obtain date"); + + let previous_month_folder_names = std::fs::read_dir(std::path::Path::new("./evaluations")) + .expect("Failed to read directory ./evaluations") + .filter_map_ok(|entry| { + if entry.metadata().unwrap().is_dir() { + Some(entry.file_name()) + } else { + None + } + }) + .filter_map_ok(|entry_string_name| { + let regex_match_date = + regex::Regex::new(r"(\d{4}-\d{2}-\d{2})").expect("Failed to build regex"); + + let filename = entry_string_name.to_str().unwrap(); + let matches_find = regex_match_date.find(filename); + + match matches_find { + Some(found) => { + let date = chrono::NaiveDate::parse_from_str(found.as_str(), "%Y-%m-%d"); + return Some((date.unwrap(), entry_string_name)); + } + None => { + return None; + } + }; + }) + .filter_map_ok(|(folder_name_date, directory_string)| { + if folder_name_date.year() == first_day_of_last_month.year() + && folder_name_date.month() == first_day_of_last_month.month() + { + return Some(directory_string); + } + return None; + }) + .filter_map(|value| { + if value.is_ok() { + return Some(value.unwrap()); + } else { + return None; + } + }) + .sorted() + .collect_vec(); + + println!("{:?}", previous_month_folder_names); + + let prompt_data_sanitization = std::fs::read_to_string("./PROMPT_DATA_SANITIZATION.txt") + .expect("Failed to read PROMPT_DATA_SANITIZATION.txt"); + let client = reqwest::blocking::Client::new(); + + let groupped_values = previous_month_folder_names + .iter() + .map(|folder_name| { + let folder_base_path = std::path::Path::new("./evaluations"); + let folder_date_path = folder_base_path.join(folder_name); + std::fs::read_dir(folder_date_path) + }) + .filter_map_ok(|files_inside_folder_on_date| { + let groupped_by_user_on_day = files_inside_folder_on_date + .filter_ok(|entry| { + let entry_file_name_as_str = entry + .file_name() + .into_string() + .expect("Failed to get filename as a String"); + + entry_file_name_as_str.ends_with(".csv") + && !entry_file_name_as_str.contains("response_time.csv") + }) + .filter_map(|value| { + if value.is_ok() { + return Some(value.unwrap()); + } + None + }) + .map(|file_name_csv| { + println!("{:?}", file_name_csv.path()); + let file_contents = std::fs::read_to_string(file_name_csv.path()) + .expect("Failed to read CSV file"); + + let ollama_api_request = client + .post(format!( + "http://{OLLAMA_SANITIZED_IP}:{OLLAMA_PORT}/api/generate" + )) + .body( + serde_json::json!({ + "model": OLLAMA_AI_MODEL_DATA_SANITIZATION, + "prompt": format!("{prompt_data_sanitization} \n{file_contents}"), + "temperature": 0.0, // Get predictable and reproducible output + "stream": false, + }) + .to_string(), + ); + + let result = ollama_api_request.timeout(Duration::from_secs(3600)).send(); + + match result { + Ok(response) => { + println!("Response: {:?}", response); + let response_json = response + .json::() + .expect("Failed to deserialize response to JSON"); + let ai_response = response_json["response"] + .as_str() + .expect("Failed to get AI response as string"); + + let ai_response = ai_response.to_string(); + + let ai_response = if let Some(resp) = ai_response + .strip_prefix(" ") + .unwrap_or(&ai_response) + .strip_prefix("```csv\n") + { + resp.to_string() + } else { + ai_response + }; + let ai_response = if let Some(resp) = ai_response + .strip_suffix(" ") + .unwrap_or(&ai_response) + .strip_suffix("```") + { + resp.to_string() + } else { + ai_response + }; + + return Ok((ai_response, file_name_csv)); + } + Err(error) => { + println!("Error {error}"); + return Err(error); + } + }; + }) + .filter_map_ok(|(ai_response, file_path_csv)| { + + + // ---------- LOG 1: mostra qual arquivo está sendo processado ---------- + eprintln!("🔍 Processando arquivo: {:?}", file_path_csv); + // Opcional: mostrar as primeiras linhas do CSV + eprintln!("📄 Primeiras 200 caracteres do CSV:\n{}", &ai_response[..200.min(ai_response.len())]); + // + + let mut reader = csv::ReaderBuilder::new() + .has_headers(true) + .delimiter(b';') + .from_reader(ai_response.as_bytes()); + + + // ---------- LOG 2: tenta desserializar e conta os registros ---------- + let deserialized = reader.deserialize::().collect::>(); + eprintln!("🧾 Total de linhas lidas (incluindo cabeçalho?): {}", deserialized.len()); + + for (i, result) in deserialized.iter().enumerate() { + match result { + Ok(record) => { + eprintln!("✅ Linha {}: CATEGORIA={}, PONTOS={:?}", i, record.CATEGORIA, record.PONTOS); + } + Err(e) => { + eprintln!("❌ Linha {} ERRO: {}", i, e); + } + } + } + // + +/* + let mut deserialized_iter = reader.deserialize::(); + let mut columns = deserialized_iter + .filter_ok(|value| value.PONTOS.is_some()) + .map_ok(|value| { + let col = + Column::new(value.CATEGORIA.into(), [value.PONTOS.unwrap() as u32]); + col + }) + .filter_map(|value| { + if value.is_ok() { + return Some(value.unwrap()); + } + None + }) + .collect_vec(); +*/ + let mut columns = deserialized + .into_iter() // usa os registros já lidos + .filter_ok(|value| { + // Se PONTOS for None, considera como 0 e mantém a linha + true // sempre mantém + }) + .map_ok(|value| { + let pontos = value.PONTOS.unwrap_or(0) as u32; + Column::new(value.CATEGORIA.into(), [pontos]) + }) + .filter_map(|value| value.ok()) + .collect_vec(); + + if columns.len() != 8 { + return None; + } + + // Parse id talk from file_path + // filename example is: CC - Erraoander Quintana - 515578 - 20251020515578.csv + // id talk is the last information, so in the example is: 20251020515578 + let regex_filename = + // regex::Regex::new(r"(FIN - )((\w+\s*)+) - (\d+) - (\d+).csv").unwrap(); + regex::Regex::new(r"FIN - (.+?) - (\d+) - (\d+)\.csv").unwrap(); + + let filename = file_path_csv + .file_name() + .into_string() + .expect("Failed to convert file name as Rust &str"); + let found_regex_groups_in_filename = regex_filename + .captures(filename.as_str()) + .expect("Failed to do regex capture"); + + let user_name = found_regex_groups_in_filename + .get(1) + .expect("Failed to get the id from regex maches"); + let talk_id = found_regex_groups_in_filename + .get(3) + .expect("Failed to get the id from regex maches"); + + let excelence_percentual = columns + .iter() + .map(|col| col.as_materialized_series().u32().unwrap().sum().unwrap()) + .sum::() as f32 + / columns.iter().len() as f32 + * 100.0; + columns.push(Column::new( + "PERCENTUAL DE EXELENCIA".into(), + [format!("{excelence_percentual:.2}")], + )); + + columns.push(Column::new("ID_TALK".into(), [talk_id.clone().as_str()])); + + let df = polars::frame::DataFrame::new(columns) + .expect("Failed to concatenate into a dataframe"); + + // return a tuple with the dataframe and the user name, so it can be correctly merged after + return Some((user_name.as_str().to_owned(), df)); + }) + .filter_map(|res| { + if res.is_ok() { + return Some(res.unwrap()); + } + return None; + }) + .into_group_map() + .into_iter() + .map(|(name, eval_dataframe_vec)| { + let groupped_df = eval_dataframe_vec + .iter() + .cloned() + .reduce(|acc, e| acc.vstack(&e).unwrap()) + .expect("Failed to concatenate dataframes"); + (name, groupped_df) + }) + .into_group_map(); + + dbg!(&groupped_by_user_on_day); + return Some(groupped_by_user_on_day); + }) + .filter_map(|res| { + if res.is_ok() { + return Some(res.unwrap()); + } + return None; + }) + .reduce(|mut acc, mut e| { + e.iter_mut().for_each(|(key, val)| { + if acc.contains_key(key) { + acc.get_mut(key) + .expect("Failed to obtain key that should already be present") + .append(val); + } else { + acc.insert(key.to_owned(), val.to_owned()); + } + }); + acc + }) + .and_then(|groupped_hashmap_df| { + let result = groupped_hashmap_df + .iter() + .map(|(key, val)| { + let dfs = val + .iter() + .cloned() + .reduce(|acc, e| acc.vstack(&e).unwrap()) + .expect("Failed to concatenate dataframes"); + (key.clone(), dfs) + }) + .collect_vec(); + return Some(result); + }); + + // Setup groupped folder + if !std::fs::exists(format!("./groupped/")).unwrap() { + std::fs::create_dir(format!("./groupped")).expect("Failed to create directory") + } + + // Setup previous week folder + if !std::fs::exists(format!("./groupped/{first_day_of_last_month}")).unwrap() { + std::fs::create_dir(format!("./groupped/{first_day_of_last_month}")) + .expect("Failed to create directory") + } + + match groupped_values { + Some(mut val) => { + val.iter_mut().for_each(|(agent, groupped_evaluations)| { + let mut save_file_csv = std::fs::File::create(format!( + "./groupped/{first_day_of_last_month}/{agent}.csv" + )) + .expect("Could not create csv file for saving"); + CsvWriter::new(&mut save_file_csv) + .include_header(true) + .with_separator(b';') + .finish(groupped_evaluations) + .expect("Failed to save Groupped DataFrame to CSV File"); + }); + } + None => {} + } + +// --- ADICAO DO PROCESSAMENTO MENSAL DO response_time.csv --- + let response_times_data = previous_month_folder_names + .iter() + .map(|folder_name| { + let folder_base_path = std::path::Path::new("./evaluations"); + let folder_date_path = folder_base_path.join(folder_name); + std::fs::read_dir(folder_date_path) + }) + .filter_map_ok(|files_inside_folder_on_date| { + let response_time_files = files_inside_folder_on_date + .filter_ok(|entry| { + let entry_file_name_as_str = entry + .file_name() + .into_string() + .expect("Failed to get filename as a String"); + + entry_file_name_as_str.ends_with("response_time.csv") + }) + .filter_map(|value| { + if value.is_ok() { + return Some(value.unwrap()); + } + None + }) + .map(|file_path| { + println!("Processing response time file: {:?}", file_path.path()); + + let mut rdr = csv::ReaderBuilder::new() + .delimiter(b';') + .has_headers(true) + .from_reader(std::fs::File::open(file_path.path()).unwrap()); + + let records: Vec = rdr + .deserialize() + .filter_map(Result::ok) + .collect(); + + records + }) + .flat_map(|records| records) + .collect_vec(); + + Some(response_time_files) + }) + .filter_map(|res| { + if res.is_ok() { + return Some(res.unwrap()); + } + return None; + }) + .flat_map(|records| records) + .collect_vec(); + + // Salvar response times consolidados do mês + if !response_times_data.is_empty() { + let response_time_file_path = format!( + "./groupped/{first_day_of_last_month}/response_times_consolidated_{first_day_of_last_month}.csv" + ); + + let mut wtr = csv::WriterBuilder::new() + .delimiter(b';') + .from_path(&response_time_file_path) + .expect("Failed to create response times CSV"); + + // Escrever cabeçalho + wtr.write_record(&["NOME", "ID_TALK", "TEMPO DE RESPOSTA", "TRANFERENCIA PELO BOT", "PRIMEIRA RESPOSTA DO AGENTE"]) + .expect("Failed to write header"); + + for record in &response_times_data { + wtr.write_record(&[ + &record.NOME, + &record.ID_TALK, + &record.TEMPO_DE_RESPOSTA.to_string(), + &record.TRANFERENCIA_PELO_BOT, + &record.PRIMEIRA_RESPOSTA_DO_AGENTE, + ]).expect("Failed to write record"); + } + + wtr.flush().expect("Failed to flush writer"); + + // Calcular estatísticas mensais + let total_records = response_times_data.len(); + let avg_response_time = response_times_data.iter() + .map(|r| r.TEMPO_DE_RESPOSTA) + .sum::() as f32 / total_records as f32; + + let min_response_time = response_times_data.iter() + .map(|r| r.TEMPO_DE_RESPOSTA) + .min() + .unwrap_or(0); + + let max_response_time = response_times_data.iter() + .map(|r| r.TEMPO_DE_RESPOSTA) + .max() + .unwrap_or(0); + + println!("Response times consolidated successfully for month {}!", first_day_of_last_month); + println!("Total records: {}", total_records); + println!("Average response time: {:.2} seconds", avg_response_time); + println!("Min response time: {} seconds", min_response_time); + println!("Max response time: {} seconds", max_response_time); + } else { + println!("No response time data found for the month {}.", first_day_of_last_month); + } +// --- FIM DA ADIÇÃO --- + + zip_directory_util::zip_directory_util::zip_source_dir_to_dst_file( + std::path::Path::new(&format!("./groupped/{first_day_of_last_month}")), + std::path::Path::new(&format!("./groupped/{first_day_of_last_month}.zip")), + ); + + let recipients = "Wilson da Conceição Oliveira , nicolas.borges@nova.net.br"; + println!("Trying to send mail... {recipients}"); + send_mail_util::send_mail_util::send_email( + &format!("Relatório agrupado dos atendimentos da fila do Financeiro N2 do mês {first_day_of_last_month}"), + &BOT_EMAIL, + &BOT_EMAIL_PASSWORD, + recipients, + &format!("./groupped/{first_day_of_last_month}.zip"), + ); +} diff --git a/src/groupped_repport_weekly.rs b/src/groupped_repport_weekly.rs new file mode 100644 index 0000000..eeb3dca --- /dev/null +++ b/src/groupped_repport_weekly.rs @@ -0,0 +1,597 @@ +use std::fmt::Debug; + +use itertools::Itertools; +use polars::prelude::*; +use reqwest; +use std::env; +use std::time::Duration; +use std::path::Path; +use csv; + +use std::fs::metadata; +use std::io::Read; + +pub mod send_mail_util; +pub mod zip_directory_util; + +#[derive(Debug, serde::Deserialize)] +struct CsvHeader { + CATEGORIA: String, + PONTOS: Option, +} + +#[derive(Debug, serde::Deserialize)] +struct CsvEvaluation { + APRESENTACAO: u8, + CONFIRMAÇÃO_DE_EMAIL: u8, + CONFIRMAÇÃO_DE_TELEFONE: u8, + PROTOCOLO: u8, + USO_DO_PORTUGUES: u8, + PACIENCIA_E_EDUCACAO: u8, + DISPONIBILIDADE: u8, +// CONHECIMENTO_TÉCNICO: u8, + ESCLARECIMENTO: u8, + ID_TALK: String, +} + +//inclusão de estrutura para agrupar o response_time.cvs +#[derive(Debug, serde::Deserialize)] +struct ResponseTimeRecord { + NOME: String, + ID_TALK: String, + #[serde(rename = "TEMPO DE RESPOSTA")] + TEMPO_DE_RESPOSTA: u32, + #[serde(rename = "TRANFERENCIA PELO BOT")] + TRANFERENCIA_PELO_BOT: String, + #[serde(rename = "PRIMEIRA RESPOSTA DO AGENTE")] + PRIMEIRA_RESPOSTA_DO_AGENTE: String, +} +//fim da inclusão + +fn main() { + match dotenv::dotenv().ok() { + Some(_) => println!("Environment variables loaded from .env file"), + None => eprintln!("Failed to load .env file, using defaults"), + } + + // Read environment variables + let OLLAMA_URL = env::var("OLLAMA_URL").unwrap_or("localhost".to_string()); + let OLLAMA_PORT = env::var("OLLAMA_PORT") + .unwrap_or("11432".to_string()) + .parse::() + .unwrap_or(11432); + let OLLAMA_AI_MODEL_DATA_SANITIZATION = env::var("OLLAMA_AI_MODEL_DATA_SANITIZATION") + .expect("Missing environment variable OLLAMA_AI_MODEL_DATA_SANITIZATION"); + let BOT_EMAIL = env::var("BOT_EMAIL").expect("BOT_EMAIL has not been set!"); + let BOT_EMAIL_PASSWORD = + env::var("BOT_EMAIL_PASSWORD").expect("BOT_EMAIL_PASSWORD has not been set!"); + + let ip_address = ipaddress::IPAddress::parse(OLLAMA_URL.to_string()); + let OLLAMA_SANITIZED_IP = match ip_address { + Ok(ip) => { + if ip.is_ipv4() { + OLLAMA_URL.clone() + } else { + format!("[{}]", OLLAMA_URL.clone()) + } + } + Err(e) => OLLAMA_URL.clone(), + }; + + // Get the current day in the format YYYY-MM-DD + let current_date = chrono::Local::now(); + let formatted_date = current_date.format("%Y-%m-%d").to_string(); + + let current_date = chrono::Local::now(); + let first_day_of_current_week = current_date + .date_naive() + .week(chrono::Weekday::Sun) + .first_day(); + let current_date_minus_one_week = first_day_of_current_week + .checked_sub_days(chrono::Days::new(1)) + .expect("Failed to subtract one day"); + let first_day_of_last_week = current_date_minus_one_week + .week(chrono::Weekday::Sun) + .first_day(); + let last_day_of_last_week = current_date_minus_one_week + .week(chrono::Weekday::Sun) + .last_day(); + + let previous_week_folder_names = std::fs::read_dir(std::path::Path::new("./evaluations")) + .expect("Failed to read directory ./evaluations") + .filter_map_ok(|entry| { + if entry.metadata().unwrap().is_dir() { + Some(entry.file_name()) + } else { + None + } + }) + .filter_map_ok(|entry_string_name| { + let regex_match_date = + regex::Regex::new(r"(\d{4}-\d{2}-\d{2})").expect("Failed to build regex"); + + let filename = entry_string_name.to_str().unwrap(); + let matches_find = regex_match_date.find(filename); + + match matches_find { + Some(found) => { + let date = chrono::NaiveDate::parse_from_str(found.as_str(), "%Y-%m-%d"); + return Some((date.unwrap().week(chrono::Weekday::Sun), entry_string_name)); + } + None => { + return None; + } + }; + }) + .filter_map_ok(|(week, directory_string)| { + let first_day_of_week_in_folder_name = week.first_day(); + + if first_day_of_last_week == first_day_of_week_in_folder_name { + return Some(directory_string); + } + return None; + }) + .filter_map(|value| { + if value.is_ok() { + return Some(value.unwrap()); + } else { + return None; + } + }) + .sorted() + .collect_vec(); + + println!("{:?}", previous_week_folder_names); + + let prompt_data_sanitization = std::fs::read_to_string("./PROMPT_DATA_SANITIZATION.txt") + .expect("Failed to read PROMPT_DATA_SANITIZATION.txt"); + let client = reqwest::blocking::Client::new(); + + let groupped_values = previous_week_folder_names + .iter() + .map(|folder_name| { + let folder_base_path = std::path::Path::new("./evaluations"); + let folder_date_path = folder_base_path.join(folder_name); + std::fs::read_dir(folder_date_path) + }) + .filter_map_ok(|files_inside_folder_on_date| { + let groupped_by_user_on_day = files_inside_folder_on_date + .filter_ok(|entry| { + let entry_file_name_as_str = entry + .file_name() + .into_string() + .expect("Failed to get filename as a String"); + + entry_file_name_as_str.ends_with(".csv") + && !entry_file_name_as_str.contains("response_time.csv") + }) + .filter_map(|value| { + if value.is_ok() { + return Some(value.unwrap()); + } + None + }) + .map(|file_name_csv| { + println!("{:?}", file_name_csv.path()); + let file_contents = std::fs::read_to_string(file_name_csv.path()) + .expect("Failed to read CSV file"); + + let ollama_api_request = client + .post(format!( + "http://{OLLAMA_SANITIZED_IP}:{OLLAMA_PORT}/api/generate" + )) + .body( + serde_json::json!({ + "model": OLLAMA_AI_MODEL_DATA_SANITIZATION, + "prompt": format!("{prompt_data_sanitization} \n{file_contents}"), + "temperature": 0.0, // Get predictable and reproducible output + "stream": false, + }) + .to_string(), + ); + + let result = ollama_api_request.timeout(Duration::from_secs(3600)).send(); + + match result { + Ok(response) => { + println!("Response: {:?}", response); + let response_json = response + .json::() + .expect("Failed to deserialize response to JSON"); + let ai_response = response_json["response"] + .as_str() + .expect("Failed to get AI response as string"); + + let ai_response = ai_response.to_string(); + + let ai_response = if let Some(resp) = ai_response + .strip_prefix(" ") + .unwrap_or(&ai_response) + .strip_prefix("```csv\n") + { + resp.to_string() + } else { + ai_response + }; + let ai_response = if let Some(resp) = ai_response + .strip_suffix(" ") + .unwrap_or(&ai_response) + .strip_suffix("```") + { + resp.to_string() + } else { + ai_response + }; + + return Ok((ai_response, file_name_csv)); + } + Err(error) => { + println!("Error {error}"); + return Err(error); + } + }; + }) + .filter_map_ok(|(ai_response, file_path_csv)| { + + // ---------- LOG 1: mostra qual arquivo está sendo processado ---------- +// eprintln!("🔍 Processando arquivo: {:?}", file_path_csv); + + // Mostra o caminho absoluto + let path = file_path_csv.path(); + + // Caminho absoluto + if let Ok(abs_path) = std::fs::canonicalize(&path) { + eprintln!("📁 Caminho absoluto: {:?}", abs_path); + } + + // Metadados + if let Ok(meta) = std::fs::metadata(&path) { + if let Ok(modified) = meta.modified() { + let datetime: chrono::DateTime = modified.into(); + eprintln!("📅 Modificado (local): {}", datetime.format("%Y-%m-%d %H:%M:%S%.3f")); + //eprintln!("🕒 Modificado: {:?}", modified); + } + eprintln!("📏 Tamanho: {} bytes", meta.len()); + } + + // Opcional: mostrar as primeiras linhas do CSV + eprintln!("📄 Primeiras 200 caracteres do CSV antes de sanitizar:\n{}", &ai_response[..200.min(ai_response.len())]); + // + + // --- SALVAR CSV SANITIZADO PARA INSPEÇÃO --- + //let sanitized_path = file_path_csv.path().with_extension("sanitized.csv"); + // if let Err(e) = std::fs::write(&sanitized_path, &ai_response) { + // eprintln!("⚠️ Erro ao salvar CSV sanitizado: {}", e); + //} else { + // eprintln!("💾 CSV sanitizado salvo: {:?}", sanitized_path); + // } + // --------------------------------------------- + + + // --- SALVAR CSV SANITIZADO EM PASTA SEPARADA --- + // Define o diretório base para os arquivos sanitizados + let sanitized_base = Path::new("./evaluations_sanitized"); + + // Obtém o caminho relativo do arquivo original em relação a "./evaluations" + // Exemplo: "./evaluations/2026-02-09/arquivo.csv" -> "2026-02-09/arquivo.csv" + if let Ok(relative_path) = file_path_csv.path().strip_prefix("./evaluations") { + let dest_path = sanitized_base.join(relative_path); + + // Cria o diretório de destino, se necessário + if let Some(parent) = dest_path.parent() { + std::fs::create_dir_all(parent).expect("Falha ao criar diretório para sanitizados"); + } + + // Altera a extensão para .sanitized.csv (ou mantém .csv, como preferir) + let dest_path = dest_path.with_extension("sanitized.csv"); + + // Escreve o arquivo + if let Err(e) = std::fs::write(&dest_path, &ai_response) { + eprintln!("⚠️ Erro ao salvar CSV sanitizado em {:?}: {}", dest_path, e); + } else { + eprintln!("💾 CSV sanitizado salvo em: {:?}", dest_path); + } + } else { + eprintln!("⚠️ Caminho do arquivo não está dentro de ./evaluations: {:?}", file_path_csv.path()); + } + + + let mut reader = csv::ReaderBuilder::new() + .has_headers(true) + .delimiter(b';') + .from_reader(ai_response.as_bytes()); + + // ---------- LOG 2: tenta desserializar e conta os registros ---------- + let deserialized = reader.deserialize::().collect::>(); + eprintln!("🧾 Total de linhas lidas (incluindo cabeçalho?): {}", deserialized.len()); + + for (i, result) in deserialized.iter().enumerate() { + match result { + Ok(record) => { + eprintln!("✅ Linha {}: CATEGORIA={}, PONTOS={:?}", i, record.CATEGORIA, record.PONTOS); + } + Err(e) => { + eprintln!("❌ Linha {} ERRO: {}", i, e); + } + } + } +/* + + let mut deserialized_iter = reader.deserialize::(); + let mut columns = deserialized_iter + .filter_ok(|value| value.PONTOS.is_some()) + .map_ok(|value| { + let col = + Column::new(value.CATEGORIA.into(), [value.PONTOS.unwrap() as u32]); + col + }) + .filter_map(|value| { + if value.is_ok() { + return Some(value.unwrap()); + } + None + }) + .collect_vec(); +*/ + let mut columns = deserialized + .into_iter() // usa os registros já lidos + .filter_ok(|value| { + // Se PONTOS for None, considera como 0 e mantém a linha + true // sempre mantém + }) + .map_ok(|value| { + let pontos = value.PONTOS.unwrap_or(0) as u32; + Column::new(value.CATEGORIA.into(), [pontos]) + }) +/* + .filter_ok(|value| value.PONTOS.is_some()) + .map_ok(|value| { + let pontos = value.PONTOS.unwrap() as u32; + Column::new(value.CATEGORIA.into(), [pontos]) + }) +*/ + .filter_map(|value| value.ok()) + .collect_vec(); + + + if columns.len() != 8 { + return None; + } + + // Parse id talk from file_path + // filename example is: FIN - Lais Mota - 515578 - 20251020515578.csv + // id talk is the last information, so in the example is: 20251020515578 + let regex_filename = + //regex::Regex::new(r"(FIN - )((\s*\w+\s*)+) - (\d+) - (\d+).csv").unwrap(); + regex::Regex::new(r"FIN - (.+?) - (\d+) - (\d+)\.csv").unwrap(); + + let filename = file_path_csv + .file_name() + .into_string() + .expect("Failed to convert file name as Rust &str"); + let found_regex_groups_in_filename = regex_filename + .captures(filename.as_str()) + .expect("Failed to do regex capture"); + + let user_name = found_regex_groups_in_filename + .get(1) + .expect("Failed to get the id from regex maches"); + let talk_id = found_regex_groups_in_filename + .get(3) + .expect("Failed to get the id from regex maches"); + + let excelence_percentual = columns + .iter() + .map(|col| col.as_materialized_series().u32().unwrap().sum().unwrap()) + .sum::() as f32 + / columns.iter().len() as f32 + * 100.0; + columns.push(Column::new( + "PERCENTUAL DE EXELENCIA".into(), + [format!("{excelence_percentual:.2}")], + )); + + columns.push(Column::new("ID_TALK".into(), [talk_id.clone().as_str()])); + + let df = polars::frame::DataFrame::new(columns) + .expect("Failed to concatenate into a dataframe"); + + // return a tuple with the dataframe and the user name, so it can be correctly merged after + return Some((user_name.as_str().to_owned(), df)); + }) + .filter_map(|res| { + if res.is_ok() { + return Some(res.unwrap()); + } + return None; + }) + .into_group_map() + .into_iter() + .map(|(name, eval_dataframe_vec)| { + let groupped_df = eval_dataframe_vec + .iter() + .cloned() + .reduce(|acc, e| acc.vstack(&e).unwrap()) + .expect("Failed to concatenate dataframes"); + (name, groupped_df) + }) + .into_group_map(); + + dbg!(&groupped_by_user_on_day); + return Some(groupped_by_user_on_day); + }) + .filter_map(|res| { + if res.is_ok() { + return Some(res.unwrap()); + } + return None; + }) + .reduce(|mut acc, mut e| { + e.iter_mut().for_each(|(key, val)| { + if acc.contains_key(key) { + acc.get_mut(key) + .expect("Failed to obtain key that should already be present") + .append(val); + } else { + acc.insert(key.to_owned(), val.to_owned()); + } + }); + acc + }) + .and_then(|groupped_hashmap_df| { + let result = groupped_hashmap_df + .iter() + .map(|(key, val)| { + let dfs = val + .iter() + .cloned() + .reduce(|acc, e| acc.vstack(&e).unwrap()) + .expect("Failed to concatenate dataframes"); + (key.clone(), dfs) + }) + .collect_vec(); + return Some(result); + }); + + // Setup groupped folder + if !std::fs::exists(format!("./groupped/")).unwrap() { + std::fs::create_dir(format!("./groupped")).expect("Failed to create directory") + } + + // Setup previous week folder + if !std::fs::exists(format!( + "./groupped/{first_day_of_last_week} - {last_day_of_last_week}" + )) + .unwrap() + { + std::fs::create_dir(format!( + "./groupped/{first_day_of_last_week} - {last_day_of_last_week}" + )) + .expect("Failed to create directory") + } + + match groupped_values { + Some(mut val) => { + val.iter_mut().for_each(|(agent, groupped_evaluations)| { + let mut save_file_csv = std::fs::File::create(format!( + "./groupped/{first_day_of_last_week} - {last_day_of_last_week}/{agent}.csv" + )) + .expect("Could not create csv file for saving"); + CsvWriter::new(&mut save_file_csv) + .include_header(true) + .with_separator(b';') + .finish(groupped_evaluations) + .expect("Failed to save Groupped DataFrame to CSV File"); + }); + } + None => {} + } + +//inclusão nova para agrupar o response_time.csv + // Processar response_time.csv separadamente + let response_times_data = previous_week_folder_names + .iter() + .map(|folder_name| { + let folder_base_path = std::path::Path::new("./evaluations"); + let folder_date_path = folder_base_path.join(folder_name); + std::fs::read_dir(folder_date_path) + }) + .filter_map_ok(|files_inside_folder_on_date| { + let response_time_files = files_inside_folder_on_date + .filter_ok(|entry| { + let entry_file_name_as_str = entry + .file_name() + .into_string() + .expect("Failed to get filename as a String"); + + entry_file_name_as_str.ends_with("response_time.csv") + }) + .filter_map(|value| { + if value.is_ok() { + return Some(value.unwrap()); + } + None + }) + .map(|file_path| { + println!("Processing response time file: {:?}", file_path.path()); + + let mut rdr = csv::ReaderBuilder::new() + .delimiter(b';') + .has_headers(true) + .from_reader(std::fs::File::open(file_path.path()).unwrap()); + + let records: Vec = rdr + .deserialize() + .filter_map(Result::ok) + .collect(); + + records + }) + .flat_map(|records| records) + .collect_vec(); + + Some(response_time_files) + }) + .filter_map(|res| { + if res.is_ok() { + return Some(res.unwrap()); + } + return None; + }) + .flat_map(|records| records) + .collect_vec(); + // Salvar response times consolidados + if !response_times_data.is_empty() { + let response_time_file_path = format!( + "./groupped/{first_day_of_last_week} - {last_day_of_last_week}/response_times_consolidated.csv" + ); + + let mut wtr = csv::WriterBuilder::new() + .delimiter(b';') + .from_path(response_time_file_path) + .expect("Failed to create response times CSV"); + + // Escrever cabeçalho + wtr.write_record(&["NOME", "ID_TALK", "TEMPO DE RESPOSTA", "TRANFERENCIA PELO BOT", "PRIMEIRA RESPOSTA DO AGENTE"]) + .expect("Failed to write header"); + + for record in response_times_data { + wtr.write_record(&[ + &record.NOME, + &record.ID_TALK, + &record.TEMPO_DE_RESPOSTA.to_string(), + &record.TRANFERENCIA_PELO_BOT, + &record.PRIMEIRA_RESPOSTA_DO_AGENTE, + ]).expect("Failed to write record"); + } + + wtr.flush().expect("Failed to flush writer"); + println!("Response times consolidated successfully!"); + } else { + println!("No response time data found for the period."); + } +// --- FIM DA ADIÇÃO --- + +//fim da inclusão + + zip_directory_util::zip_directory_util::zip_source_dir_to_dst_file( + std::path::Path::new(&format!( + "./groupped/{first_day_of_last_week} - {last_day_of_last_week}" + )), + std::path::Path::new(&format!( + "./groupped/{first_day_of_last_week} - {last_day_of_last_week}.zip" + )), + ); + + let recipients = "Wilson da Conceição Oliveira , nicolas.borges@nova.net.br"; + println!("Trying to send mail... {recipients}"); + send_mail_util::send_mail_util::send_email( + &format!( + "Relatório agrupado dos atendimentos da fila do Financeiro N2 - semana {first_day_of_last_week} - {last_day_of_last_week}" + ), + &BOT_EMAIL, + &BOT_EMAIL_PASSWORD, + recipients, + &format!("./groupped/{first_day_of_last_week} - {last_day_of_last_week}.zip"), + ); +} diff --git a/src/groupped_repport_weekly.rs.save b/src/groupped_repport_weekly.rs.save new file mode 100644 index 0000000..89f0082 --- /dev/null +++ b/src/groupped_repport_weekly.rs.save @@ -0,0 +1,493 @@ +use std::fmt::Debug; + +use itertools::Itertools; +use polars::prelude::*; +use reqwest; +use std::env; +use std::time::Duration; + +use csv; + +pub mod send_mail_util; +pub mod zip_directory_util; + +#[derive(Debug, serde::Deserialize)] +struct CsvHeader { + CATEGORIA: String, + PONTOS: Option, +} + +#[derive(Debug, serde::Deserialize)] +struct CsvEvaluation { + APRESENTAÇÃO: u8, + CONFIRMAÇÃO_DE_EMAIL: u8, + CONFIRMAÇÃO_DE_TELEFONE: u8, + PROTOCOLO: u8, + USO_DO_PORTUGUÊS: u8, + PACIÊNCIA_E_EDUCAÇÃO: u8, + DISPONIBILIDADE: u8, + CONHECIMENTO_TÉCNICO: u8, + DIDATISMO: u8, + ID_TALK: String, +} + +//inclusão de estrutura para agrupar o response_time.cvs +#[derive(Debug, serde::Deserialize)] +struct ResponseTimeRecord { + NOME: String, + ID_TALK: String, + #[serde(rename = "TEMPO DE RESPOSTA")] + TEMPO_DE_RESPOSTA: u32, + #[serde(rename = "TRANFERENCIA PELO BOT")] + TRANFERENCIA_PELO_BOT: String, + #[serde(rename = "PRIMEIRA RESPOSTA DO AGENTE")] + PRIMEIRA_RESPOSTA_DO_AGENTE: String, +} +//fim da inclusão + +fn main() { + match dotenv::dotenv().ok() { + Some(_) => println!("Environment variables loaded from .env file"), + None => eprintln!("Failed to load .env file, using defaults"), + } + + // Read environment variables + let OLLAMA_URL = env::var("OLLAMA_URL").unwrap_or("localhost".to_string()); + let OLLAMA_PORT = env::var("OLLAMA_PORT") + .unwrap_or("11432".to_string()) + .parse::() + .unwrap_or(11432); + let OLLAMA_AI_MODEL_DATA_SANITIZATION = env::var("OLLAMA_AI_MODEL_DATA_SANITIZATION") + .expect("Missing environment variable OLLAMA_AI_MODEL_DATA_SANITIZATION"); + let BOT_EMAIL = env::var("BOT_EMAIL").expect("BOT_EMAIL has not been set!"); + let BOT_EMAIL_PASSWORD = + env::var("BOT_EMAIL_PASSWORD").expect("BOT_EMAIL_PASSWORD has not been set!"); + + let ip_address = ipaddress::IPAddress::parse(OLLAMA_URL.to_string()); + let OLLAMA_SANITIZED_IP = match ip_address { + Ok(ip) => { + if ip.is_ipv4() { + OLLAMA_URL.clone() + } else { + format!("[{}]", OLLAMA_URL.clone()) + } + } + Err(e) => OLLAMA_URL.clone(), + }; + + // Get the current day in the format YYYY-MM-DD + let current_date = chrono::Local::now(); + let formatted_date = current_date.format("%Y-%m-%d").to_string(); + + let current_date = chrono::Local::now(); + let first_day_of_current_week = current_date + .date_naive() + .week(chrono::Weekday::Sun) + .first_day(); + let current_date_minus_one_week = first_day_of_current_week + .checked_sub_days(chrono::Days::new(1)) + .expect("Failed to subtract one day"); + let first_day_of_last_week = current_date_minus_one_week + .week(chrono::Weekday::Sun) + .first_day(); + let last_day_of_last_week = current_date_minus_one_week + .week(chrono::Weekday::Sun) + .last_day(); + + let previous_week_folder_names = std::fs::read_dir(std::path::Path::new("./evaluations")) + .expect("Failed to read directory ./evaluations") + .filter_map_ok(|entry| { + if entry.metadata().unwrap().is_dir() { + Some(entry.file_name()) + } else { + None + } + }) + .filter_map_ok(|entry_string_name| { + let regex_match_date = + regex::Regex::new(r"(\d{4}-\d{2}-\d{2})").expect("Failed to build regex"); + + let filename = entry_string_name.to_str().unwrap(); + let matches_find = regex_match_date.find(filename); + + match matches_find { + Some(found) => { + let date = chrono::NaiveDate::parse_from_str(found.as_str(), "%Y-%m-%d"); + return Some((date.unwrap().week(chrono::Weekday::Sun), entry_string_name)); + } + None => { + return None; + } + }; + }) + .filter_map_ok(|(week, directory_string)| { + let first_day_of_week_in_folder_name = week.first_day(); + + if first_day_of_last_week == first_day_of_week_in_folder_name { + return Some(directory_string); + } + return None; + }) + .filter_map(|value| { + if value.is_ok() { + return Some(value.unwrap()); + } else { + return None; + } + }) + .sorted() + .collect_vec(); + + println!("{:?}", previous_week_folder_names); + + let prompt_data_sanitization = std::fs::read_to_string("./PROMPT_DATA_SANITIZATION.txt") + .expect("Failed to read PROMPT_DATA_SANITIZATION.txt"); + let client = reqwest::blocking::Client::new(); + + let groupped_values = previous_week_folder_names + .iter() + .map(|folder_name| { + let folder_base_path = std::path::Path::new("./evaluations"); + let folder_date_path = folder_base_path.join(folder_name); + std::fs::read_dir(folder_date_path) + }) + .filter_map_ok(|files_inside_folder_on_date| { + let groupped_by_user_on_day = files_inside_folder_on_date + .filter_ok(|entry| { + let entry_file_name_as_str = entry + .file_name() + .into_string() + .expect("Failed to get filename as a String"); + + entry_file_name_as_str.ends_with(".csv") + && !entry_file_name_as_str.contains("response_time.csv") + }) + .filter_map(|value| { + if value.is_ok() { + return Some(value.unwrap()); + } + None + }) + .map(|file_name_csv| { + println!("{:?}", file_name_csv.path()); + let file_contents = std::fs::read_to_string(file_name_csv.path()) + .expect("Failed to read CSV file"); + + let ollama_api_request = client + .post(format!( + "http://{OLLAMA_SANITIZED_IP}:{OLLAMA_PORT}/api/generate" + )) + .body( + serde_json::json!({ + "model": OLLAMA_AI_MODEL_DATA_SANITIZATION, + "prompt": format!("{prompt_data_sanitization} \n{file_contents}"), + "temperature": 0.0, // Get predictable and reproducible output + "stream": false, + }) + .to_string(), + ); + + let result = ollama_api_request.timeout(Duration::from_secs(3600)).send(); + + match result { + Ok(response) => { + println!("Response: {:?}", response); + let response_json = response + .json::() + .expect("Failed to deserialize response to JSON"); + let ai_response = response_json["response"] + .as_str() + .expect("Failed to get AI response as string"); + + let ai_response = ai_response.to_string(); + + let ai_response = if let Some(resp) = ai_response + .strip_prefix(" ") + .unwrap_or(&ai_response) + .strip_prefix("```csv\n") + { + resp.to_string() + } else { + ai_response + }; + let ai_response = if let Some(resp) = ai_response + .strip_suffix(" ") + .unwrap_or(&ai_response) + .strip_suffix("```") + { + resp.to_string() + } else { + ai_response + }; + + return Ok((ai_response, file_name_csv)); + } + Err(error) => { + println!("Error {error}"); + return Err(error); + } + }; + }) + .filter_map_ok(|(ai_repsonse, file_path_csv)| { + let mut reader = csv::ReaderBuilder::new() + .has_headers(true) + .delimiter(b';') + .from_reader(ai_repsonse.as_bytes()); + + let mut deserialized_iter = reader.deserialize::(); + + let mut columns = deserialized_iter + .filter_ok(|value| value.PONTOS.is_some()) + .map_ok(|value| { + let col = + Column::new(value.CATEGORIA.into(), [value.PONTOS.unwrap() as u32]); + col + }) + .filter_map(|value| { + if value.is_ok() { + return Some(value.unwrap()); + } + None + }) + .collect_vec(); + + if columns.len() != 9 { + return None; + } + + // Parse id talk from file_path + // filename example is: FIN - Lais Mota - 515578 - 20251020515578.csv + // id talk is the last information, so in the example is: 20251020515578 + let regex_filename = + //regex::Regex::new(r"(FIN - )((\s*\w+\s*)+) - (\d+) - (\d+).csv").unwrap(); + + let filename = file_path_csv + .file_name() + .into_string() + .expect("Failed to convert file name as Rust &str"); + let found_regex_groups_in_filename = regex_filename + .captures(filename.as_str()) + .expect("Failed to do regex capture"); + + let user_name = found_regex_groups_in_filename + .get(2) + .expect("Failed to get the id from regex maches"); + let talk_id = found_regex_groups_in_filename + .get(5) + .expect("Failed to get the id from regex maches"); + + let excelence_percentual = columns + .iter() + .map(|col| col.as_materialized_series().u32().unwrap().sum().unwrap()) + .sum::() as f32 + / columns.iter().len() as f32 + * 100.0; + columns.push(Column::new( + "PERCENTUAL DE EXELENCIA".into(), + [format!("{excelence_percentual:.2}")], + )); + + columns.push(Column::new("ID_TALK".into(), [talk_id.clone().as_str()])); + + let df = polars::frame::DataFrame::new(columns) + .expect("Failed to concatenate into a dataframe"); + + // return a tuple with the dataframe and the user name, so it can be correctly merged after + return Some((user_name.as_str().to_owned(), df)); + }) + .filter_map(|res| { + if res.is_ok() { + return Some(res.unwrap()); + } + return None; + }) + .into_group_map() + .into_iter() + .map(|(name, eval_dataframe_vec)| { + let groupped_df = eval_dataframe_vec + .iter() + .cloned() + .reduce(|acc, e| acc.vstack(&e).unwrap()) + .expect("Failed to concatenate dataframes"); + (name, groupped_df) + }) + .into_group_map(); + + dbg!(&groupped_by_user_on_day); + return Some(groupped_by_user_on_day); + }) + .filter_map(|res| { + if res.is_ok() { + return Some(res.unwrap()); + } + return None; + }) + .reduce(|mut acc, mut e| { + e.iter_mut().for_each(|(key, val)| { + if acc.contains_key(key) { + acc.get_mut(key) + .expect("Failed to obtain key that should already be present") + .append(val); + } else { + acc.insert(key.to_owned(), val.to_owned()); + } + }); + acc + }) + .and_then(|groupped_hashmap_df| { + let result = groupped_hashmap_df + .iter() + .map(|(key, val)| { + let dfs = val + .iter() + .cloned() + .reduce(|acc, e| acc.vstack(&e).unwrap()) + .expect("Failed to concatenate dataframes"); + (key.clone(), dfs) + }) + .collect_vec(); + return Some(result); + }); + + // Setup groupped folder + if !std::fs::exists(format!("./groupped/")).unwrap() { + std::fs::create_dir(format!("./groupped")).expect("Failed to create directory") + } + + // Setup previous week folder + if !std::fs::exists(format!( + "./groupped/{first_day_of_last_week} - {last_day_of_last_week}" + )) + .unwrap() + { + std::fs::create_dir(format!( + "./groupped/{first_day_of_last_week} - {last_day_of_last_week}" + )) + .expect("Failed to create directory") + } + + match groupped_values { + Some(mut val) => { + val.iter_mut().for_each(|(agent, groupped_evaluations)| { + let mut save_file_csv = std::fs::File::create(format!( + "./groupped/{first_day_of_last_week} - {last_day_of_last_week}/{agent}.csv" + )) + .expect("Could not create csv file for saving"); + CsvWriter::new(&mut save_file_csv) + .include_header(true) + .with_separator(b';') + .finish(groupped_evaluations) + .expect("Failed to save Groupped DataFrame to CSV File"); + }); + } + None => {} + } + +//inclusão nova para agrupar o response_time.csv + // Processar response_time.csv separadamente + let response_times_data = previous_week_folder_names + .iter() + .map(|folder_name| { + let folder_base_path = std::path::Path::new("./evaluations"); + let folder_date_path = folder_base_path.join(folder_name); + std::fs::read_dir(folder_date_path) + }) + .filter_map_ok(|files_inside_folder_on_date| { + let response_time_files = files_inside_folder_on_date + .filter_ok(|entry| { + let entry_file_name_as_str = entry + .file_name() + .into_string() + .expect("Failed to get filename as a String"); + + entry_file_name_as_str.ends_with("response_time.csv") + }) + .filter_map(|value| { + if value.is_ok() { + return Some(value.unwrap()); + } + None + }) + .map(|file_path| { + println!("Processing response time file: {:?}", file_path.path()); + + let mut rdr = csv::ReaderBuilder::new() + .delimiter(b';') + .has_headers(true) + .from_reader(std::fs::File::open(file_path.path()).unwrap()); + + let records: Vec = rdr + .deserialize() + .filter_map(Result::ok) + .collect(); + + records + }) + .flat_map(|records| records) + .collect_vec(); + + Some(response_time_files) + }) + .filter_map(|res| { + if res.is_ok() { + return Some(res.unwrap()); + } + return None; + }) + .flat_map(|records| records) + .collect_vec(); + // Salvar response times consolidados + if !response_times_data.is_empty() { + let response_time_file_path = format!( + "./groupped/{first_day_of_last_week} - {last_day_of_last_week}/response_times_consolidated.csv" + ); + + let mut wtr = csv::WriterBuilder::new() + .delimiter(b';') + .from_path(response_time_file_path) + .expect("Failed to create response times CSV"); + + // Escrever cabeçalho + wtr.write_record(&["NOME", "ID_TALK", "TEMPO DE RESPOSTA", "TRANFERENCIA PELO BOT", "PRIMEIRA RESPOSTA DO AGENTE"]) + .expect("Failed to write header"); + + for record in response_times_data { + wtr.write_record(&[ + &record.NOME, + &record.ID_TALK, + &record.TEMPO_DE_RESPOSTA.to_string(), + &record.TRANFERENCIA_PELO_BOT, + &record.PRIMEIRA_RESPOSTA_DO_AGENTE, + ]).expect("Failed to write record"); + } + + wtr.flush().expect("Failed to flush writer"); + println!("Response times consolidated successfully!"); + } else { + println!("No response time data found for the period."); + } +// --- FIM DA ADIÇÃO --- + +//fim da inclusão + + zip_directory_util::zip_directory_util::zip_source_dir_to_dst_file( + std::path::Path::new(&format!( + "./groupped/{first_day_of_last_week} - {last_day_of_last_week}" + )), + std::path::Path::new(&format!( + "./groupped/{first_day_of_last_week} - {last_day_of_last_week}.zip" + )), + ); + + let recipients = "Wilson da Conceição Oliveira , nicolas.borges@nova.net.br"; + println!("Trying to send mail... {recipients}"); + send_mail_util::send_mail_util::send_email( + &format!( + "Relatório agrupado dos atendimentos da fila do Financeiro N2 - semana {first_day_of_last_week} - {last_day_of_last_week}" + ), + &BOT_EMAIL, + &BOT_EMAIL_PASSWORD, + recipients, + &format!("./groupped/{first_day_of_last_week} - {last_day_of_last_week}.zip"), + ); +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..9c449c4 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,749 @@ +use std::{any::Any, env, fmt::format, iter, time::Duration}; + +use chrono::{self, Timelike}; +use dotenv; +use ipaddress; +use itertools::{self, Itertools}; +use lettre::{self, message}; +use reqwest; +use serde_json::{self, json}; + +use std::io::prelude::*; + +pub mod send_mail_util; +pub mod zip_directory_util; + +fn main() -> anyhow::Result<()> { + match dotenv::dotenv().ok() { + Some(_) => println!("Environment variables loaded from .env file"), + None => eprintln!("Failed to load .env file, using defaults"), + } + + // Read environment variables + let OLLAMA_URL = env::var("OLLAMA_URL").unwrap_or("localhost".to_string()); + let OLLAMA_PORT = env::var("OLLAMA_PORT") + .unwrap_or("11432".to_string()) + .parse::() + .unwrap_or(11432); + let PIPERUN_API_URL = env::var("PIPERUN_API_URL").expect("PIPERUN_API_URL has not been set!"); + let PIPERUN_CLIENT_ID = env::var("PIPERUN_CLIENT_ID") + .expect("PIPERUN_CLIENT_ID has not been set!") + .parse::() + .unwrap_or(0); + let PIPERUN_CLIENT_SECRET = + env::var("PIPERUN_CLIENT_SECRET").expect("PIPERUN_CLIENT_SECRET has not been set!"); + let PIPERUN_BOT_USERNAME = + env::var("PIPERUN_BOT_USERNAME").expect("PIPERUN_BOT_USERNAME has not been set!"); + let PIPERUN_BOT_PASSWORD = + env::var("PIPERUN_BOT_PASSWORD").expect("PIPERUN_BOT_PASSWORD has not been set!"); + let OLLAMA_AI_MODEL = env::var("OLLAMA_AI_MODEL").expect("OLLAMA_AI_MODEL has not been set!"); + let MINIMUM_NUMBER_OF_MESSAGES_TO_EVALUATE = env::var("MINIMUM_NUMBER_OF_MESSAGES_TO_EVALUATE") + .expect("MINIMUM_NUMBER_OF_MESSAGES_TO_EVALUATE has not been set!") + .parse::() + .unwrap_or(10); + let MINIMUM_NUMBER_OF_MESSAGES_WITH_AGENT_TO_EVALUATE = + env::var("MINIMUM_NUMBER_OF_MESSAGES_WITH_AGENT_TO_EVALUATE") + .expect("MINIMUM_NUMBER_OF_MESSAGES_WITH_AGENT_TO_EVALUATE has not been set!") + .parse::() + .unwrap_or(12); + let BOT_EMAIL = env::var("BOT_EMAIL").expect("BOT_EMAIL has not been set!"); + let BOT_EMAIL_PASSWORD = + env::var("BOT_EMAIL_PASSWORD").expect("BOT_EMAIL_PASSWORD has not been set!"); + + // Print the configuration + println!("OLLAMA_URL: {}", OLLAMA_URL); + println!("OLLAMA_PORT: {}", OLLAMA_PORT); + println!("OLLAMA_AI_MODEL: {}", OLLAMA_AI_MODEL); + println!("PIPERUN_API_URL: {}", PIPERUN_API_URL); + println!("PIPERUN_CLIENT_ID: {}", PIPERUN_CLIENT_ID); + println!("PIPERUN_CLIENT_SECRET: {}", PIPERUN_CLIENT_SECRET); + println!("PIPERUN_BOT_USERNAME: {}", PIPERUN_BOT_USERNAME); + println!("PIPERUN_BOT_PASSWORD: {}", PIPERUN_BOT_PASSWORD); + + let ip_address = ipaddress::IPAddress::parse(OLLAMA_URL.to_string()); + let OLLAMA_SANITIZED_IP = match ip_address { + Ok(ip) => { + if ip.is_ipv4() { + OLLAMA_URL.clone() + } else { + format!("[{}]", OLLAMA_URL.clone()) + } + } + Err(e) => OLLAMA_URL.clone(), + }; + + // Send the authentication request + let client = reqwest::blocking::Client::new(); + + let auth_request = client + .post(format!("https://{}/oauth/token", PIPERUN_API_URL)) + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .body( + serde_json::json!({ + "grant_type": "password", + "client_id": PIPERUN_CLIENT_ID, + "client_secret": PIPERUN_CLIENT_SECRET, + "username": PIPERUN_BOT_USERNAME, + "password": PIPERUN_BOT_PASSWORD, + }) + .to_string(), + ); + + println!("Sending authentication request to Piperun API..."); + println!("{:?}", auth_request); + + let response = auth_request.send(); + let access_token = match response { + Ok(resp) => { + if resp.status().is_success() { + let json: serde_json::Value = resp.json()?; + // println!("Authentication successful: {:?}", json); + + // Extract the access token + if let Some(access_token) = json.get("access_token") { + println!("Access Token: {}", access_token); + access_token + .as_str() + .expect("Failed to get token") + .to_string() + } else { + eprintln!("Access token not found in response"); + panic!("Failed to retrieve access token from Piperun API"); + } + } else { + eprintln!("Authentication failed: {}", resp.status()); + let json: serde_json::Value = resp.json()?; + eprintln!("Response body: {:?}", json); + + panic!("Failed to authenticate with Piperun API"); + } + } + Err(e) => { + eprintln!("Error sending authentication request: {}", e); + panic!("Failed to send authentication request to Piperun API"); + } + }; + + + + // Get the current day in the format YYYY-MM-DD + let current_date = chrono::Local::now(); + let formatted_date = current_date.format("%Y-%m-%d").to_string(); + + println!("Current date: {}", formatted_date); + + // Get the day before the current date + let day_before = current_date + .checked_sub_signed(chrono::Duration::days(1)) + .expect("Failed to get the day before"); + let formatted_day_before = day_before.format("%Y-%m-%d").to_string(); + + println!("Day before: {}", formatted_day_before); + + let day_before_at_midnight = day_before + .with_hour(0) + .unwrap() + .with_minute(0) + .unwrap() + .with_second(0) + .unwrap(); + let formatted_day_before_at_midnight = + day_before_at_midnight.format("%Y-%m-%d %H:%M").to_string(); + + let day_before_at_23_59_59 = day_before + .with_hour(23) + .unwrap() + .with_minute(59) + .unwrap() + .with_second(59) + .unwrap(); + let formatted_day_before_at_23_59_59 = + day_before_at_23_59_59.format("%Y-%m-%d %H:%M").to_string(); + + println!( + "Day before at midnight: {}, Day before at 23:59:59: {}", + formatted_day_before_at_midnight, formatted_day_before_at_23_59_59 + ); + + let formatted_day_before = day_before_at_midnight.format("%Y-%m-%d").to_string(); + + // Create a folder named with the day_before + if !std::fs::exists(format!("./evaluations/{formatted_day_before}")).unwrap() { + std::fs::create_dir(format!("./evaluations/{formatted_day_before}")) + .expect("Failed to create directory") + } + + // Create the response time folder + if !std::fs::exists(format!( + "./evaluations/{formatted_day_before}/response_time.csv" + )) + .unwrap() + { + let mut response_time_file = std::fs::File::create_new(format!( + "./evaluations/{formatted_day_before}/response_time.csv" + )) + .expect("Failed to response_time.csv"); + } + +/* + // --- NOVO: processar argumento de linha de comando para data específica --- + use std::env; + use chrono::NaiveDate; + + let args: Vec = env::args().collect(); + let target_day = if args.len() > 1 { + // Se um argumento foi passado, interpretar como YYYY-MM-DD + let naive_date = NaiveDate::parse_from_str(&args[1], "%Y-%m-%d") + .expect("Formato de data inválido. Use YYYY-MM-DD"); + // Converter para DateTime com hora 00:00:00 do fuso local + naive_date.and_hms_opt(0, 0, 0).unwrap() + } else { + // Comportamento padrão: dia anterior ao atual + (chrono::Local::now() - chrono::Duration::days(1)).naive_local() + }; + + println!("Processando o dia: {}", target_day.format("%Y-%m-%d")); + + // Definir início e fim do dia (para consultas na API) + let day_start = target_day; // já 00:00 + let day_end = target_day + .with_hour(23).unwrap() + .with_minute(59).unwrap() + .with_second(59).unwrap(); + + let formatted_day = target_day.format("%Y-%m-%d").to_string(); + let formatted_day_start = day_start.format("%Y-%m-%d %H:%M").to_string(); + let formatted_day_end = day_end.format("%Y-%m-%d %H:%M").to_string(); + + println!("Início do dia: {}", formatted_day_start); + println!("Fim do dia: {}", formatted_day_end); + + // Criar pasta com o nome do dia processado + if !std::fs::exists(format!("./evaluations/{formatted_day}")).unwrap() { + std::fs::create_dir(format!("./evaluations/{formatted_day}")) + .expect("Failed to create directory"); + } + + // Criar arquivo response_time.csv se não existir + if !std::fs::exists(format!("./evaluations/{formatted_day}/response_time.csv")).unwrap() { + let _ = std::fs::File::create_new(format!( + "./evaluations/{formatted_day}/response_time.csv" + )) + .expect("Failed to create response_time.csv"); + } + +*/ + + // Read system prompt + let prompt = std::fs::read_to_string("PROMPT.txt").unwrap(); + let filter_file_contents = std::fs::read_to_string("FILTER.txt").unwrap_or(String::new()); + let filter_keywords = filter_file_contents + .split("\n") + .filter(|keyword| !keyword.is_empty()) + .collect::>(); + + let talks_array = get_piperun_chats_on_date( + &PIPERUN_API_URL, + &client, + &access_token, + formatted_day_before_at_midnight, + //formatted_day_start, + formatted_day_before_at_23_59_59, + //formatted_day_end, + ); + + println!("Number of consolidated talks: {}", talks_array.len()); + + let talk_ids = talks_array + .iter() + .cloned() + .map(|value| { + serde_json::from_value::(value).expect("Failed to parse the JSON") + ["id"] + .clone() + .to_string() + }) + .collect::>(); + + println!("IDS {:?}", talk_ids); + + // Gather messages and apply filtering + let filtered_chats = talk_ids + .iter() + .cloned() + .map(|talk_id| { + let talk_id_get_request = client + .get(format!("https://{}/api/talk_histories", PIPERUN_API_URL)) + .bearer_auth(&access_token) + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .query(&[ + ("talk_id", talk_id), + ("type", "a".to_string()), + ("only_view", "1".to_string()), + ]); + + let talk_id_get_result = talk_id_get_request.send(); + + return talk_id_get_result; + }) + .filter_map_ok(|result| { + let json = result + .json::() + .expect("Failed to deserialize response to JSON") + .to_owned(); + let talk_histories = &json["talk_histories"]; + let data = &talk_histories["data"]; + + // Filter chats that have very few messages + let talk_lenght = talk_histories + .as_array() + .expect("Wrong message type received from talk histories") + .len(); + if talk_lenght < MINIMUM_NUMBER_OF_MESSAGES_TO_EVALUATE { + return None; + } + + // Filter chats that have less that specified ammount of talks with support agent form the last queue transfer + let found = talk_histories + .as_array() + .expect("Wrong message type received from talk histories") + .into_iter() + .enumerate() + .find(|(pos, message_object)| { + let message = message_object["message"] + .as_str() + .expect("Failed to decode message as string"); + let found = message.find( + "Atendimento transferido para a fila [NovaNet -> Atendimento -> NOC - Clientes]", + ); + found.is_some() + }); + + match found { + None => { + return None; + } + Some(pos) => { + let pos_found = pos.0; + if pos_found < MINIMUM_NUMBER_OF_MESSAGES_WITH_AGENT_TO_EVALUATE { + return None; + } + } + }; + + // Filter Bot finished chats + if json["agent"]["user"]["name"] + .as_str() + .unwrap_or("unknown_user") + == "PipeBot" + { + return None; + } + + // Filtra os chats que nao foram encerrados + if json["finished_at"].to_string() + == "null" + { + return None; + } + + // Apply keyword based filtering + let filter_keywords_found = talk_histories + .as_array() + .expect("Wrong message type received from talk histories") + .into_iter() + .any(|message_object| { + let message = message_object["message"] + .as_str() + .expect("Failed to decode message as string"); + let found1 = filter_keywords.iter().any(|keyword| { + message + .to_uppercase() + .find(&keyword.to_uppercase()) + .is_some() + }); + let found2 = message_object["is_template"] + .as_bool() + .unwrap_or(true); + let found = found1 || found2; + found + }); + + if filter_keywords_found { + return None; + } + + return Some(json); + }); + + // Calculate the response time in seconds + let response_time = filtered_chats + .clone() + .map(|messages| { + let json = messages.unwrap(); + let talk_histories = &json["talk_histories"]; + + // find the bot transfer message + let bot_transfer_message = talk_histories + .as_array() + .expect("Wrong message type received from talk histories") + .into_iter() + .enumerate() + .filter(|(pos, message_object)| { + let user_name = message_object["user"]["name"] + .as_str() + .expect("Failed to decode message as string"); + user_name == "PipeBot".to_string() + }) + .find(|(pos, message_object)| { + let message = message_object["message"] + .as_str() + .expect("Failed to decode message as string"); + let found = message.find("Atendimento entregue da fila de espera para o agente [NOC - "); + let found = message.find("Olá, faço parte do setor de Redes aqui da NovaNet."); + found.is_some() + }); + + // Find first agent message sent after the last bot message + // let (pos, transfer_message) = + // bot_transfer_message.expect("Failed to get the transfer bot message position"); + + let (pos, transfer_message) = match bot_transfer_message { + Some((pos, msg)) => (pos, msg), + None => return "".to_string(), // Retorna string vazia em vez de panic + }; + + let msg = talk_histories + .as_array() + .expect("Wrong message type received from talk histories") + .into_iter() + .take(pos) + .rev() + .filter(|message| { + message["type"] == "out".to_string() + // && message["user"]["name"] != "PipeBot".to_string() + && message["user"]["name"].as_str().map_or(false, |name| name.starts_with("NOC -")) + }) + .take(1) + .collect_vec(); + // Se não encontrar mensagem do agente, retorna string vazia + if msg.is_empty() { + return "".to_string(); + } + + let agent_first_message = msg[0]; + + // Calculate time difference between bot message and agent message + let date_user_message_sent = agent_first_message["sent_at"].as_str().unwrap(); + + let format = "%Y-%m-%d %H:%M:%S"; + + let date_user_message_sent_parsed = + match chrono::NaiveDateTime::parse_from_str(date_user_message_sent, format) { + Ok(dt) => dt, + Err(e) => { + println!("Error parsing DateTime: {}", e); + panic!("Failed parsing date") + } + }; + + let date_transfer_message_sent_parsed = match chrono::NaiveDateTime::parse_from_str( + transfer_message["sent_at"].as_str().unwrap(), + format, + ) { + Ok(dt) => dt, + Err(e) => { + println!("Error parsing DateTime: {}", e); + panic!("Failed parsing date") + } + }; + + let response_time = (date_user_message_sent_parsed - date_transfer_message_sent_parsed) + .as_seconds_f32(); + let name = agent_first_message["user"]["name"] + .as_str() + .unwrap() + .to_owned(); + let id = json["tracking_number"].as_str().unwrap_or("").to_owned(); + let bot_transfer_date = date_transfer_message_sent_parsed.to_owned(); + let user_response_date = date_user_message_sent.to_owned(); + println!( + "response_time: {}s", + (date_user_message_sent_parsed - date_transfer_message_sent_parsed) + .as_seconds_f32() + ); + + format!( + "{};{};{};{};{}", + name, id, response_time, bot_transfer_date, user_response_date + ) + }) + .filter(|s| !s.is_empty()) // Filtra strings vazias + .reduce(|acc, e| format!("{}\n{}", acc, e)) + .unwrap_or("".to_string()); + + // return Ok(()); + // Open file and write to it + let header = "NOME;ID_TALK;TEMPO DE RESPOSTA;TRANFERENCIA PELO BOT;PRIMEIRA RESPOSTA DO AGENTE"; + let mut response_time_file = std::fs::OpenOptions::new() + .write(true) + .open(format!( + "./evaluations/{formatted_day_before}/response_time.csv" + //"./evaluations/{formatted_day}/response_time.csv" + )) + .expect("Failed to open response time file for write"); + response_time_file + .write_all(format!("{header}\n{response_time}").as_bytes()) + .expect("Failed to write header to file"); + + filtered_chats.clone().skip(0).for_each(|result| { + let json = result.unwrap(); + let talk_histories = &json["talk_histories"]; + let data = &talk_histories["data"]; + + let talk = talk_histories + .as_array() + .expect("Wrong message type received from talk histories") + .iter() + .rev() + .map(|message_object| { + let new_json_filtered = format!( + "{{ + message: {}, + sent_at: {}, + type: {}, + user_name: {} +}}", + message_object["message"], + message_object["sent_at"], + message_object["type"], + message_object["user"]["name"] + ); + // println!("{}", new_json_filtered); + new_json_filtered + }) + .reduce(|acc, e| format!("{acc}\n{e}")) + .expect("Error extracting talk"); + + println!("{prompt}\n {talk}"); + + let ollama_api_request = client + .post(format!( + "http://{OLLAMA_SANITIZED_IP}:{OLLAMA_PORT}/api/generate" + )) + .body( + serde_json::json!({ + "model": OLLAMA_AI_MODEL, + "prompt": format!("{prompt} \n{talk}"), + // "options": serde_json::json!({"temperature": 0.1}), + "stream": false, + }) + .to_string(), + ); + + let result = ollama_api_request.timeout(Duration::from_secs(3600)).send(); + + match result { + Ok(response) => { + println!("Response: {:?}", response); + let response_json = response + .json::() + .expect("Failed to deserialize response to JSON"); + println!("{}", response_json); + let ai_response = response_json["response"] + .as_str() + .expect("Failed to get AI response as string"); + println!("AI Response: {}", ai_response); + + let csv_response = ai_response.replace("```csv\n", "").replace("```", ""); + + // Save the CSV response to a file + + let user_name = &json["agent"]["user"]["name"] + .as_str() + .unwrap_or("unknown_user"); + let talk_id = &json["id"].as_u64().unwrap_or(0); + let tracking_number = &json["tracking_number"].as_str().unwrap_or(""); + std::fs::write( + format!( + "./evaluations/{}/{} - {} - {}.csv", + formatted_day_before, user_name, talk_id, tracking_number + //formatted_day, user_name, talk_id, tracking_number + ), + csv_response, + ) + .expect("Unable to write file"); + std::fs::write( + format!( + "./evaluations/{}/{} - {} - {} - prompt.txt", + formatted_day_before, user_name, talk_id, tracking_number + //formatted_day, user_name, talk_id, tracking_number + ), + format!("{prompt} \n{talk}"), + ) + .expect("Unable to write file"); + } + Err(error) => { + println!("Error {error}"); + } + }; + }); + + // Compress folder into zip + let source_dir_str = format!("./evaluations/{formatted_day_before}"); + //let source_dir_str = format!("./evaluations/{formatted_day}"); + let output_zip_file_str = format!("./evaluations/{formatted_day_before}.zip"); + //let output_zip_file_str = format!("./evaluations/{formatted_day}.zip"); + let source_dir = std::path::Path::new(source_dir_str.as_str()); + let output_zip_file = std::path::Path::new(output_zip_file_str.as_str()); + zip_directory_util::zip_directory_util::zip_source_dir_to_dst_file(source_dir, output_zip_file); + + + // Send folder to email + let recipients = "Wilson da Conceição Oliveira , Nicolas Borges da Silva "; + println!("Trying to send email... Recipients {recipients}"); + + send_mail_util::send_mail_util::send_email( + &format!("Avaliacao atendimentos da fila NOC - Clientes do dia {formatted_day_before}"), + //&format!("Avaliacao atendimentos da fila Financeiro NVL2 do dia {formatted_day}"), + &BOT_EMAIL, + &BOT_EMAIL_PASSWORD, + recipients, + &output_zip_file_str, + ); + + + return Ok(()); +} + +fn get_piperun_chats_on_date( + PIPERUN_API_URL: &String, + client: &reqwest::blocking::Client, + access_token: &String, + formatted_day_before_at_midnight: String, + //formatted_day_start: String, + formatted_day_before_at_23_59_59: String, + //formatted_day_end: String, +) -> Vec { + let start_of_talk_code: String = "talk_start".to_string(); + let support_queue_id: String = "19".to_string(); + + // API V2 + let report_type = "consolidated".to_string(); + let page = "1".to_string(); + let per_page = "15".to_string(); + + let talks_request = client + .get(format!("https://{}/api/v2/reports/talks", PIPERUN_API_URL)) + .bearer_auth(access_token) + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .query(&[ + ("page", page.clone()), + ("perPage", per_page.clone()), + ("report_type", report_type.clone()), + ("start_date", formatted_day_before_at_midnight.clone()), + //("start_date", formatted_day_start.clone()), + ("end_date", formatted_day_before_at_23_59_59.clone()), + //("end_date", formatted_day_end.clone()), + ("date_range_type", start_of_talk_code.clone()), + ("queue_id[]", support_queue_id.clone()), + ]); + + println!("Sending request for consolidated talks... {talks_request:?}"); + let talks_response = talks_request.send(); + + let json_response = match talks_response { + Ok(resp) => { + if resp.status().is_success() { + let json: serde_json::Value = resp.json().unwrap(); + json + } else { + eprintln!("Failed to get consolidated talks: {}", resp.status()); + let json: serde_json::Value = resp.json().unwrap(); + eprintln!("Response body: {:?}", json); + panic!("Failed to retrieve consolidated talks from Piperun API"); + } + } + Err(e) => { + eprintln!("Error: {e}"); + panic!("Failed to send the request for talks to PipeRUN API"); + } + }; + + let mut aggregated_talks = json_response["data"] + .as_array() + .expect("Failed to parse messages as array") + .to_owned(); + + let current_page = json_response["current_page"] + .as_i64() + .expect("Failed to obtain current page number"); + let last_page = json_response["last_page"] + .as_i64() + .expect("Failed to obtain current page number"); + + if current_page == last_page { + return aggregated_talks; + } + + let mut all_other_messages = (current_page..last_page) + .into_iter() + .map(|page| { + let page_to_request = page + 1; + let talks_request = client + .get(format!("https://{}/api/v2/reports/talks", PIPERUN_API_URL)) + .bearer_auth(access_token) + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .query(&[ + ("page", page_to_request.to_string()), + ("perPage", per_page.clone()), + ("report_type", report_type.clone()), + ("start_date", formatted_day_before_at_midnight.clone()), + //("start_date", formatted_day_start.clone()), + ("end_date", formatted_day_before_at_23_59_59.clone()), + //("end_date", formatted_day_end.clone()), + ("date_range_type", start_of_talk_code.clone()), + ("queue_id[]", support_queue_id.clone()), + ]); + + println!("Sending request for consolidated talks... {talks_request:?}"); + let talks_response = talks_request.send(); + + let json_response = match talks_response { + Ok(resp) => { + if resp.status().is_success() { + let json: serde_json::Value = resp.json().unwrap(); + json + } else { + eprintln!("Failed to get consolidated talks: {}", resp.status()); + let json: serde_json::Value = resp.json().unwrap(); + eprintln!("Response body: {:?}", json); + panic!("Failed to retrieve consolidated talks from Piperun API"); + } + } + Err(e) => { + eprintln!("Error: {e}"); + panic!("Failed to send the request for talks to PipeRUN API"); + } + }; + + let aggregated_talks = json_response["data"] + .as_array() + .expect("Failed to parse messages as array") + .to_owned(); + + return aggregated_talks; + }) + .reduce(|mut this, mut acc| { + acc.append(&mut this); + acc + }) + .expect("Failed to concatenate all vectors of messages"); + + aggregated_talks.append(&mut all_other_messages); + aggregated_talks +} diff --git a/src/main.rs.save b/src/main.rs.save new file mode 100644 index 0000000..9c0ea9f --- /dev/null +++ b/src/main.rs.save @@ -0,0 +1,744 @@ +use std::{any::Any, env, fmt::format, iter, time::Duration}; + +use chrono::{self, Timelike}; +use dotenv; +use ipaddress; +use itertools::{self, Itertools}; +use lettre::{self, message}; +use reqwest; +use serde_json::{self, json}; + +use std::io::prelude::*; + +pub mod send_mail_util; +pub mod zip_directory_util; + +fn main() -> anyhow::Result<()> { + match dotenv::dotenv().ok() { + Some(_) => println!("Environment variables loaded from .env file"), + None => eprintln!("Failed to load .env file, using defaults"), + } + + // Read environment variables + let OLLAMA_URL = env::var("OLLAMA_URL").unwrap_or("localhost".to_string()); + let OLLAMA_PORT = env::var("OLLAMA_PORT") + .unwrap_or("11432".to_string()) + .parse::() + .unwrap_or(11432); + let PIPERUN_API_URL = env::var("PIPERUN_API_URL").expect("PIPERUN_API_URL has not been set!"); + let PIPERUN_CLIENT_ID = env::var("PIPERUN_CLIENT_ID") + .expect("PIPERUN_CLIENT_ID has not been set!") + .parse::() + .unwrap_or(0); + let PIPERUN_CLIENT_SECRET = + env::var("PIPERUN_CLIENT_SECRET").expect("PIPERUN_CLIENT_SECRET has not been set!"); + let PIPERUN_BOT_USERNAME = + env::var("PIPERUN_BOT_USERNAME").expect("PIPERUN_BOT_USERNAME has not been set!"); + let PIPERUN_BOT_PASSWORD = + env::var("PIPERUN_BOT_PASSWORD").expect("PIPERUN_BOT_PASSWORD has not been set!"); + let OLLAMA_AI_MODEL = env::var("OLLAMA_AI_MODEL").expect("OLLAMA_AI_MODEL has not been set!"); + let MINIMUM_NUMBER_OF_MESSAGES_TO_EVALUATE = env::var("MINIMUM_NUMBER_OF_MESSAGES_TO_EVALUATE") + .expect("MINIMUM_NUMBER_OF_MESSAGES_TO_EVALUATE has not been set!") + .parse::() + .unwrap_or(10); + let MINIMUM_NUMBER_OF_MESSAGES_WITH_AGENT_TO_EVALUATE = + env::var("MINIMUM_NUMBER_OF_MESSAGES_WITH_AGENT_TO_EVALUATE") + .expect("MINIMUM_NUMBER_OF_MESSAGES_WITH_AGENT_TO_EVALUATE has not been set!") + .parse::() + .unwrap_or(12); + let BOT_EMAIL = env::var("BOT_EMAIL").expect("BOT_EMAIL has not been set!"); + let BOT_EMAIL_PASSWORD = + env::var("BOT_EMAIL_PASSWORD").expect("BOT_EMAIL_PASSWORD has not been set!"); + + // Print the configuration + println!("OLLAMA_URL: {}", OLLAMA_URL); + println!("OLLAMA_PORT: {}", OLLAMA_PORT); + println!("OLLAMA_AI_MODEL: {}", OLLAMA_AI_MODEL); + println!("PIPERUN_API_URL: {}", PIPERUN_API_URL); + println!("PIPERUN_CLIENT_ID: {}", PIPERUN_CLIENT_ID); + println!("PIPERUN_CLIENT_SECRET: {}", PIPERUN_CLIENT_SECRET); + println!("PIPERUN_BOT_USERNAME: {}", PIPERUN_BOT_USERNAME); + println!("PIPERUN_BOT_PASSWORD: {}", PIPERUN_BOT_PASSWORD); + + let ip_address = ipaddress::IPAddress::parse(OLLAMA_URL.to_string()); + let OLLAMA_SANITIZED_IP = match ip_address { + Ok(ip) => { + if ip.is_ipv4() { + OLLAMA_URL.clone() + } else { + format!("[{}]", OLLAMA_URL.clone()) + } + } + Err(e) => OLLAMA_URL.clone(), + }; + + // Send the authentication request + let client = reqwest::blocking::Client::new(); + + let auth_request = client + .post(format!("https://{}/oauth/token", PIPERUN_API_URL)) + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .body( + serde_json::json!({ + "grant_type": "password", + "client_id": PIPERUN_CLIENT_ID, + "client_secret": PIPERUN_CLIENT_SECRET, + "username": PIPERUN_BOT_USERNAME, + "password": PIPERUN_BOT_PASSWORD, + }) + .to_string(), + ); + + println!("Sending authentication request to Piperun API..."); + println!("{:?}", auth_request); + + let response = auth_request.send(); + let access_token = match response { + Ok(resp) => { + if resp.status().is_success() { + let json: serde_json::Value = resp.json()?; + // println!("Authentication successful: {:?}", json); + + // Extract the access token + if let Some(access_token) = json.get("access_token") { + println!("Access Token: {}", access_token); + access_token + .as_str() + .expect("Failed to get token") + .to_string() + } else { + eprintln!("Access token not found in response"); + panic!("Failed to retrieve access token from Piperun API"); + } + } else { + eprintln!("Authentication failed: {}", resp.status()); + let json: serde_json::Value = resp.json()?; + eprintln!("Response body: {:?}", json); + + panic!("Failed to authenticate with Piperun API"); + } + } + Err(e) => { + eprintln!("Error sending authentication request: {}", e); + panic!("Failed to send authentication request to Piperun API"); + } + }; + + // Get the current day in the format YYYY-MM-DD + let current_date = chrono::Local::now(); + let formatted_date = current_date.format("%Y-%m-%d").to_string(); + + println!("Current date: {}", formatted_date); + +/* + + // Get the day before the current date + let day_before = current_date + .checked_sub_signed(chrono::Duration::days(1)) + .expect("Failed to get the day before"); + let formatted_day_before = day_before.format("%Y-%m-%d").to_string(); + + println!("Day before: {}", formatted_day_before); + + let day_before_at_midnight = day_before + .with_hour(0) + .unwrap() + .with_minute(0) + .unwrap() + .with_second(0) + .unwrap(); + let formatted_day_before_at_midnight = + day_before_at_midnight.format("%Y-%m-%d %H:%M").to_string(); + + let day_before_at_23_59_59 = day_before + .with_hour(23) + .unwrap() + .with_minute(59) + .unwrap() + .with_second(59) + .unwrap(); + let formatted_day_before_at_23_59_59 = + day_before_at_23_59_59.format("%Y-%m-%d %H:%M").to_string(); + + println!( + "Day before at midnight: {}, Day before at 23:59:59: {}", + formatted_day_before_at_midnight, formatted_day_before_at_23_59_59 + ); + + let formatted_day_before = day_before_at_midnight.format("%Y-%m-%d").to_string(); + + // Create a folder named with the day_before + if !std::fs::exists(format!("./evaluations/{formatted_day_before}")).unwrap() { + std::fs::create_dir(format!("./evaluations/{formatted_day_before}")) + .expect("Failed to create directory") + } + + // Create the response time folder + if !std::fs::exists(format!( + "./evaluations/{formatted_day_before}/response_time.csv" + )) + .unwrap() + { + let mut response_time_file = std::fs::File::create_new(format!( + "./evaluations/{formatted_day_before}/response_time.csv" + )) + .expect("Failed to response_time.csv"); + } + +*/ + +// --- NOVO: processar argumento de linha de comando para data específica --- +use std::env; +use chrono::NaiveDate; + +let args: Vec = env::args().collect(); +let target_day = if args.len() > 1 { + // Se um argumento foi passado, interpretar como YYYY-MM-DD + let naive_date = NaiveDate::parse_from_str(&args[1], "%Y-%m-%d") + .expect("Formato de data inválido. Use YYYY-MM-DD"); + // Converter para DateTime com hora 00:00:00 do fuso local + naive_date.and_hms_opt(0, 0, 0).unwrap() +} else { + // Comportamento padrão: dia anterior ao atual + (chrono::Local::now() - chrono::Duration::days(1)).naive_local() +}; + +println!("Processando o dia: {}", target_day.format("%Y-%m-%d")); + +// Definir início e fim do dia (para consultas na API) +let day_start = target_day; // já 00:00 +let day_end = target_day + .with_hour(23).unwrap() + .with_minute(59).unwrap() + .with_second(59).unwrap(); + +let formatted_day = target_day.format("%Y-%m-%d").to_string(); +let formatted_day_start = day_start.format("%Y-%m-%d %H:%M").to_string(); +let formatted_day_end = day_end.format("%Y-%m-%d %H:%M").to_string(); + +println!("Início do dia: {}", formatted_day_start); +println!("Fim do dia: {}", formatted_day_end); + +// Criar pasta com o nome do dia processado +if !std::fs::exists(format!("./evaluations/{formatted_day}")).unwrap() { + std::fs::create_dir(format!("./evaluations/{formatted_day}")) + .expect("Failed to create directory"); +} + +// Criar arquivo response_time.csv se não existir +if !std::fs::exists(format!("./evaluations/{formatted_day}/response_time.csv")).unwrap() { + let _ = std::fs::File::create_new(format!( + "./evaluations/{formatted_day}/response_time.csv" + )) + .expect("Failed to create response_time.csv"); +} +//----------------------------------------------------------------------- + + // Read system prompt + let prompt = std::fs::read_to_string("PROMPT.txt").unwrap(); + let filter_file_contents = std::fs::read_to_string("FILTER.txt").unwrap_or(String::new()); + let filter_keywords = filter_file_contents + .split("\n") + .filter(|keyword| !keyword.is_empty()) + .collect::>(); + + let talks_array = get_piperun_chats_on_date( + &PIPERUN_API_URL, + &client, + &access_token, + formatted_day_before_at_midnight, + formatted_day_before_at_23_59_59, + ); + + println!("Number of consolidated talks: {}", talks_array.len()); + + let talk_ids = talks_array + .iter() + .cloned() + .map(|value| { + serde_json::from_value::(value).expect("Failed to parse the JSON") + ["id"] + .clone() + .to_string() + }) + .collect::>(); + + println!("IDS {:?}", talk_ids); + + // Gather messages and apply filtering + let filtered_chats = talk_ids + .iter() + .cloned() + .map(|talk_id| { + let talk_id_get_request = client + .get(format!("https://{}/api/talk_histories", PIPERUN_API_URL)) + .bearer_auth(&access_token) + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .query(&[ + ("talk_id", talk_id), + ("type", "a".to_string()), + ("only_view", "1".to_string()), + ]); + + let talk_id_get_result = talk_id_get_request.send(); + + return talk_id_get_result; + }) + .filter_map_ok(|result| { + let json = result + .json::() + .expect("Failed to deserialize response to JSON") + .to_owned(); + let talk_histories = &json["talk_histories"]; + let data = &talk_histories["data"]; + + // Filter chats that have very few messages + let talk_lenght = talk_histories + .as_array() + .expect("Wrong message type received from talk histories") + .len(); + if talk_lenght < MINIMUM_NUMBER_OF_MESSAGES_TO_EVALUATE { + return None; + } + + // Filter chats that have less that specified ammount of talks with support agent form the last queue transfer + let found = talk_histories + .as_array() + .expect("Wrong message type received from talk histories") + .into_iter() + .enumerate() + .find(|(pos, message_object)| { + let message = message_object["message"] + .as_str() + .expect("Failed to decode message as string"); + let found = message.find( + "Atendimento transferido para a fila [NovaNet -> Atendimento -> Financeiro NVL2]", + ); + found.is_some() + }); + + match found { + None => { + return None; + } + Some(pos) => { + let pos_found = pos.0; + if pos_found < MINIMUM_NUMBER_OF_MESSAGES_WITH_AGENT_TO_EVALUATE { + return None; + } + } + }; + + // Filter Bot finished chats + if json["agent"]["user"]["name"] + .as_str() + .unwrap_or("unknown_user") + == "PipeBot" + { + return None; + } + + // Filtra os chats que nao foram encerrados + if json["finished_at"].to_string() + == "null" + { + return None; + } + + // Apply keyword based filtering + let filter_keywords_found = talk_histories + .as_array() + .expect("Wrong message type received from talk histories") + .into_iter() + .any(|message_object| { + let message = message_object["message"] + .as_str() + .expect("Failed to decode message as string"); + let found1 = filter_keywords.iter().any(|keyword| { + message + .to_uppercase() + .find(&keyword.to_uppercase()) + .is_some() + }); + let found2 = message_object["is_template"] + .as_bool() + .unwrap_or(true); + let found = found1 || found2; + found + }); + + if filter_keywords_found { + return None; + } + + return Some(json); + }); + + // Calculate the response time in seconds + let response_time = filtered_chats + .clone() + .map(|messages| { + let json = messages.unwrap(); + let talk_histories = &json["talk_histories"]; + + /* + dbg!(&talk_histories); + + talk_histories.as_array().unwrap().into_iter().enumerate().for_each(|(pos, message_obj)|{println!("{}: {}", pos, message_obj["message"])}); + + find the bot transfer message + */ + let bot_transfer_message = talk_histories + .as_array() + .expect("Wrong message type received from talk histories") + .into_iter() + .enumerate() + .filter(|(pos, message_object)| { + let user_name = message_object["user"]["name"] + .as_str() + .expect("Failed to decode message as string"); + user_name == "PipeBot".to_string() + }) + .find(|(pos, message_object)| { + let message = message_object["message"] + .as_str() + .expect("Failed to decode message as string"); + //let found = message.find("Atendimento transferido para a fila [NovaNet -> Atendimento -> Financeiro NVL2]"); + let found = message.find("Atendimento entregue da fila de espera para o agente [FIN - "); + found.is_some() + }); + + /* + Find first agent message sent after the last bot message + let (pos, transfer_message) = + bot_transfer_message.expect("Failed to get the transfer bot message position"); + */ + + let (pos, transfer_message) = match bot_transfer_message { + Some((pos, msg)) => (pos, msg), + None => return "".to_string(), // Retorna string vazia em vez de panic + }; + + let msg = talk_histories + .as_array() + .expect("Wrong message type received from talk histories") + .into_iter() + .take(pos) + .rev() + .filter(|message| { + message["type"] == "out".to_string() + // && message["user"]["name"] != "PipeBot".to_string() + && message["user"]["name"].as_str().map_or(false, |name| name.starts_with("FIN -")) + }) + .take(1) + .collect_vec(); + // Se não encontrar mensagem do agente, retorna string vazia + if msg.is_empty() { + return "".to_string(); + } + + let agent_first_message = msg[0]; + + // Calculate time difference between bot message and agent message + let date_user_message_sent = agent_first_message["sent_at"].as_str().unwrap(); + + let format = "%Y-%m-%d %H:%M:%S"; + + let date_user_message_sent_parsed = + match chrono::NaiveDateTime::parse_from_str(date_user_message_sent, format) { + Ok(dt) => dt, + Err(e) => { + println!("Error parsing DateTime: {}", e); + panic!("Failed parsing date") + } + }; + + let date_transfer_message_sent_parsed = match chrono::NaiveDateTime::parse_from_str( + transfer_message["sent_at"].as_str().unwrap(), + format, + ) { + Ok(dt) => dt, + Err(e) => { + println!("Error parsing DateTime: {}", e); + panic!("Failed parsing date") + } + }; + + let response_time = (date_user_message_sent_parsed - date_transfer_message_sent_parsed) + .as_seconds_f32(); + let name = agent_first_message["user"]["name"] + .as_str() + .unwrap() + .to_owned(); + let id = json["tracking_number"].as_str().unwrap_or("").to_owned(); + let bot_transfer_date = date_transfer_message_sent_parsed.to_owned(); + let user_response_date = date_user_message_sent.to_owned(); + println!( + "response_time: {}s", + (date_user_message_sent_parsed - date_transfer_message_sent_parsed) + .as_seconds_f32() + ); + + format!( + "{};{};{};{};{}", + name, id, response_time, bot_transfer_date, user_response_date + ) + }) + .filter(|s| !s.is_empty()) // Filtra strings vazias + .reduce(|acc, e| format!("{}\n{}", acc, e)) + .unwrap_or("".to_string()); + + // return Ok(()); + // Open file and write to it + let header = "NOME;ID_TALK;TEMPO DE RESPOSTA;TRANFERENCIA PELO BOT;PRIMEIRA RESPOSTA DO AGENTE"; + let mut response_time_file = std::fs::OpenOptions::new() + .write(true) + .open(format!( + "./evaluations/{formatted_day_before}/response_time.csv" + )) + .expect("Failed to open response time file for write"); + response_time_file + .write_all(format!("{header}\n{response_time}").as_bytes()) + .expect("Failed to write header to file"); + + filtered_chats.clone().skip(0).for_each(|result| { + let json = result.unwrap(); + let talk_histories = &json["talk_histories"]; + let data = &talk_histories["data"]; + + let talk = talk_histories + .as_array() + .expect("Wrong message type received from talk histories") + .iter() + .rev() + .map(|message_object| { + let new_json_filtered = format!( + "{{ + message: {}, + sent_at: {}, + type: {}, + user_name: {} +}}", + message_object["message"], + message_object["sent_at"], + message_object["type"], + message_object["user"]["name"] + ); + // println!("{}", new_json_filtered); + new_json_filtered + }) + .reduce(|acc, e| format!("{acc}\n{e}")) + .expect("Error extracting talk"); + + println!("{prompt}\n {talk}"); + + let ollama_api_request = client + .post(format!( + "http://{OLLAMA_SANITIZED_IP}:{OLLAMA_PORT}/api/generate" + )) + .body( + serde_json::json!({ + "model": OLLAMA_AI_MODEL, + "prompt": format!("{prompt} \n{talk}"), + // "options": serde_json::json!({"temperature": 0.1}), + "stream": false, + }) + .to_string(), + ); + + let result = ollama_api_request.timeout(Duration::from_secs(3600)).send(); + + match result { + Ok(response) => { + println!("Response: {:?}", response); + let response_json = response + .json::() + .expect("Failed to deserialize response to JSON"); + println!("{}", response_json); + let ai_response = response_json["response"] + .as_str() + .expect("Failed to get AI response as string"); + println!("AI Response: {}", ai_response); + + let csv_response = ai_response.replace("```csv\n", "").replace("```", ""); + + // Save the CSV response to a file + + let user_name = &json["agent"]["user"]["name"] + .as_str() + .unwrap_or("unknown_user"); + let talk_id = &json["id"].as_u64().unwrap_or(0); + let tracking_number = &json["tracking_number"].as_str().unwrap_or(""); + std::fs::write( + format!( + "./evaluations/{}/{} - {} - {}.csv", + formatted_day_before, user_name, talk_id, tracking_number + ), + csv_response, + ) + .expect("Unable to write file"); + std::fs::write( + format!( + "./evaluations/{}/{} - {} - {} - prompt.txt", + formatted_day_before, user_name, talk_id, tracking_number + ), + format!("{prompt} \n{talk}"), + ) + .expect("Unable to write file"); + } + Err(error) => { + println!("Error {error}"); + } + }; + }); + + // Compress folder into zip + let source_dir_str = format!("./evaluations/{formatted_day_before}"); + let output_zip_file_str = format!("./evaluations/{formatted_day_before}.zip"); + let source_dir = std::path::Path::new(source_dir_str.as_str()); + let output_zip_file = std::path::Path::new(output_zip_file_str.as_str()); + zip_directory_util::zip_directory_util::zip_source_dir_to_dst_file(source_dir, output_zip_file); + + /* + Send folder to email + let recipients = "Wilson da Conceição Oliveira , nicolas.borges@nova.net.br"; + let recipients = "Wilson da Conceição Oliveira , nicolas.borges@nova.net.br"; + println!("Trying to send email... Recipients {recipients}"); + + send_mail_util::send_mail_util::send_email( + &format!("Avaliacao atendimentos da fila Financeiro NVL2 do dia {formatted_day_before}"), + &BOT_EMAIL, + &BOT_EMAIL_PASSWORD, + recipients, + &output_zip_file_str, + ); + */ + + return Ok(()); +} + +fn get_piperun_chats_on_date( + PIPERUN_API_URL: &String, + client: &reqwest::blocking::Client, + access_token: &String, + formatted_day_before_at_midnight: String, + formatted_day_before_at_23_59_59: String, +) -> Vec { + let start_of_talk_code: String = "talk_start".to_string(); + let support_queue_id: String = "16".to_string(); + + // API V2 + let report_type = "consolidated".to_string(); + let page = "1".to_string(); + let per_page = "15".to_string(); + + let talks_request = client + .get(format!("https://{}/api/v2/reports/talks", PIPERUN_API_URL)) + .bearer_auth(access_token) + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .query(&[ + ("page", page.clone()), + ("perPage", per_page.clone()), + ("report_type", report_type.clone()), + ("start_date", formatted_day_before_at_midnight.clone()), + ("end_date", formatted_day_before_at_23_59_59.clone()), + ("date_range_type", start_of_talk_code.clone()), + ("queue_id[]", support_queue_id.clone()), + ]); + + println!("Sending request for consolidated talks... {talks_request:?}"); + let talks_response = talks_request.send(); + + let json_response = match talks_response { + Ok(resp) => { + if resp.status().is_success() { + let json: serde_json::Value = resp.json().unwrap(); + json + } else { + eprintln!("Failed to get consolidated talks: {}", resp.status()); + let json: serde_json::Value = resp.json().unwrap(); + eprintln!("Response body: {:?}", json); + panic!("Failed to retrieve consolidated talks from Piperun API"); + } + } + Err(e) => { + eprintln!("Error: {e}"); + panic!("Failed to send the request for talks to PipeRUN API"); + } + }; + + let mut aggregated_talks = json_response["data"] + .as_array() + .expect("Failed to parse messages as array") + .to_owned(); + + let current_page = json_response["current_page"] + .as_i64() + .expect("Failed to obtain current page number"); + let last_page = json_response["last_page"] + .as_i64() + .expect("Failed to obtain current page number"); + + if current_page == last_page { + return aggregated_talks; + } + + let mut all_other_messages = (current_page..last_page) + .into_iter() + .map(|page| { + let page_to_request = page + 1; + let talks_request = client + .get(format!("https://{}/api/v2/reports/talks", PIPERUN_API_URL)) + .bearer_auth(access_token) + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .query(&[ + ("page", page_to_request.to_string()), + ("perPage", per_page.clone()), + ("report_type", report_type.clone()), + ("start_date", formatted_day_before_at_midnight.clone()), + ("end_date", formatted_day_before_at_23_59_59.clone()), + ("date_range_type", start_of_talk_code.clone()), + ("queue_id[]", support_queue_id.clone()), + ]); + + println!("Sending request for consolidated talks... {talks_request:?}"); + let talks_response = talks_request.send(); + + let json_response = match talks_response { + Ok(resp) => { + if resp.status().is_success() { + let json: serde_json::Value = resp.json().unwrap(); + json + } else { + eprintln!("Failed to get consolidated talks: {}", resp.status()); + let json: serde_json::Value = resp.json().unwrap(); + eprintln!("Response body: {:?}", json); + panic!("Failed to retrieve consolidated talks from Piperun API"); + } + } + Err(e) => { + eprintln!("Error: {e}"); + panic!("Failed to send the request for talks to PipeRUN API"); + } + }; + + let aggregated_talks = json_response["data"] + .as_array() + .expect("Failed to parse messages as array") + .to_owned(); + + return aggregated_talks; + }) + .reduce(|mut this, mut acc| { + acc.append(&mut this); + acc + }) + .expect("Failed to concatenate all vectors of messages"); + + aggregated_talks.append(&mut all_other_messages); + aggregated_talks +} diff --git a/src/send_mail_util.rs b/src/send_mail_util.rs new file mode 100644 index 0000000..e6dbb28 --- /dev/null +++ b/src/send_mail_util.rs @@ -0,0 +1,46 @@ +pub mod send_mail_util { + use lettre::{ + Message, SmtpTransport, Transport, + message::{self, Attachment, Mailboxes, MultiPart, SinglePart, header::ContentType}, + }; + + pub fn send_email( + subject_of_email: &str, + bot_email: &str, + bot_email_password: &str, + to: &str, + zip_file_name: &str, + ) { + let filebody = std::fs::read(zip_file_name).unwrap(); + let content_type = ContentType::parse("application/zip").unwrap(); + let attachment = Attachment::new(zip_file_name.to_string()).body(filebody, content_type); + let mailboxes: Mailboxes = to.parse().unwrap(); + let to_header: message::header::To = mailboxes.into(); + + let email = Message::builder() + .from(format!("PipeRUN bot <{bot_email}>").parse().unwrap()) + .reply_to(format!("PipeRUN bot <{bot_email}>").parse().unwrap()) + .mailbox(to_header) + .subject(format!("{subject_of_email}")) + .multipart( + MultiPart::mixed() + .singlepart( + SinglePart::builder() + .header(ContentType::TEXT_PLAIN) + .body(String::from("Avaliacao dos atendimentos")), + ) + .singlepart(attachment), + ) + .unwrap(); + + // Create the SMTPS transport + let sender = SmtpTransport::from_url(&format!( + "smtps://{bot_email}:{bot_email_password}@mail.nova.net.br" + )) + .unwrap() + .build(); + + // Send the email via remote relay + sender.send(&email).unwrap(); + } +} diff --git a/src/zip_directory_util.rs b/src/zip_directory_util.rs new file mode 100644 index 0000000..c7ae699 --- /dev/null +++ b/src/zip_directory_util.rs @@ -0,0 +1,69 @@ +pub mod zip_directory_util { + + use std::io::prelude::*; + use zip::write::SimpleFileOptions; + + use std::fs::File; + use std::path::Path; + use walkdir::{DirEntry, WalkDir}; + + fn zip_dir( + it: &mut dyn Iterator, + prefix: &Path, + writer: T, + method: zip::CompressionMethod, + ) where + T: Write + Seek, + { + let mut zip = zip::ZipWriter::new(writer); + let options = SimpleFileOptions::default() + .compression_method(method) + .unix_permissions(0o755); + + let prefix = Path::new(prefix); + let mut buffer = Vec::new(); + for entry in it { + let path = entry.path(); + let name = path.strip_prefix(prefix).unwrap(); + let path_as_string = name + .to_str() + .map(str::to_owned) + .expect("Failed to parse path"); + + // Write file or directory explicitly + // Some unzip tools unzip files with directory paths correctly, some do not! + if path.is_file() { + println!("adding file {path:?} as {name:?} ..."); + zip.start_file(path_as_string, options) + .expect("Failed to add file"); + let mut f = File::open(path).unwrap(); + + f.read_to_end(&mut buffer).expect("Failed to read file"); + zip.write_all(&buffer).expect("Failed to write file"); + buffer.clear(); + } else if !name.as_os_str().is_empty() { + // Only if not root! Avoids path spec / warning + // and mapname conversion failed error on unzip + println!("adding dir {path_as_string:?} as {name:?} ..."); + zip.add_directory(path_as_string, options) + .expect("Failed to add directory"); + } + } + zip.finish().expect("Failed to ZIP"); + } + + pub fn zip_source_dir_to_dst_file(src_dir: &Path, dst_file: &Path) { + if !Path::new(src_dir).is_dir() { + panic!("src_dir must be a directory"); + } + + let method = zip::CompressionMethod::Stored; + let path = Path::new(dst_file); + let file = File::create(path).unwrap(); + + let walkdir = WalkDir::new(src_dir); + let it = walkdir.into_iter(); + + zip_dir(&mut it.filter_map(|e| e.ok()), src_dir, file, method); + } +}