commit 503f2457c9a526aa6f4e13b88676f06afe4066b2 Author: rizary Date: Mon Apr 7 12:44:22 2025 +0700 initial working concept diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..04743de --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +target/ +**/target/ +dist/ +node_modules/ + +# Environment files +.env +.env.prod +docker.dev/**/.env +**/.env +.env.*local + +# Data directories +docker.dev/**/volumes/db/data/* +!docker.dev/**/volumes/.gitkeep +**/kong.yaml + +# misc +*.crt +*.pem +.DS_Store + +# Local files for common editors +.idea +.vscode \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..ed0525e --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2967 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "actix" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de7fa236829ba0841304542f7614c42b80fca007455315c45c785ccfa873a85b" +dependencies = [ + "actix-macros", + "actix-rt", + "actix_derive", + "bitflags 2.9.0", + "bytes", + "crossbeam-channel", + "futures-core", + "futures-sink", + "futures-task", + "futures-util", + "log", + "once_cell", + "parking_lot", + "pin-project-lite", + "smallvec", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix-codec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" +dependencies = [ + "bitflags 2.9.0", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-cors" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0346d8c1f762b41b458ed3145eea914966bb9ad20b9be0d6d463b20d45586370" +dependencies = [ + "actix-utils", + "actix-web", + "derive_more 0.99.19", + "futures-util", + "log", + "once_cell", + "smallvec", +] + +[[package]] +name = "actix-files" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0773d59061dedb49a8aed04c67291b9d8cf2fe0b60130a381aab53c6dd86e9be" +dependencies = [ + "actix-http", + "actix-service", + "actix-utils", + "actix-web", + "bitflags 2.9.0", + "bytes", + "derive_more 0.99.19", + "futures-core", + "http-range", + "log", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "v_htmlescape", +] + +[[package]] +name = "actix-http" +version = "3.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa882656b67966045e4152c634051e70346939fced7117d5f0b52146a7c74c9" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-tls", + "actix-utils", + "base64 0.22.1", + "bitflags 2.9.0", + "bytes", + "bytestring", + "derive_more 2.0.1", + "encoding_rs", + "foldhash", + "futures-core", + "h2", + "http", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "actix-router" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" +dependencies = [ + "bytestring", + "cfg-if", + "http", + "regex-lite", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" +dependencies = [ + "actix-macros", + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6398974fd4284f4768af07965701efbbb5fdc0616bff20cade1bb14b77675e24" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "socket2", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "actix-tls" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac453898d866cdbecdbc2334fe1738c747b4eba14a677261f2b768ba05329389" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "impl-more", + "pin-project-lite", + "tokio", + "tokio-rustls 0.23.4", + "tokio-util", + "tracing", + "webpki-roots 0.22.6", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2e3b15b3dc6c6ed996e4032389e9849d4ab002b1e92fbfe85b5f307d1479b4d" +dependencies = [ + "actix-codec", + "actix-http", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-tls", + "actix-utils", + "bytes", + "bytestring", + "cfg-if", + "derive_more 2.0.1", + "encoding_rs", + "foldhash", + "futures-core", + "futures-util", + "impl-more", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex-lite", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2", + "time", + "tracing", + "url", +] + +[[package]] +name = "actix-web-actors" +version = "4.3.1+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98c5300b38fd004fe7d2a964f9a90813fdbe8a81fed500587e78b1b71c6f980" +dependencies = [ + "actix", + "actix-codec", + "actix-http", + "actix-web", + "bytes", + "bytestring", + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix_derive" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6ac1e58cded18cb28ddc17143c4dea5345b3ad575e14f32f66e4054a56eb271" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.15", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys 0.59.0", +] + +[[package]] +name = "anyhow" +version = "1.0.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" + +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[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 = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "bytestring" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e465647ae23b2823b0753f50decb2d5a86d2bb2cac04788fafd1f80e45378e5f" +dependencies = [ + "bytes", +] + +[[package]] +name = "cc" +version = "1.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.5.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "config" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23738e11972c7643e4ec947840fc463b6a571afcd3e735bdfce7d03c7a784aca" +dependencies = [ + "async-trait", + "json5", + "lazy_static", + "nom", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml", + "yaml-rust", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +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 = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_more" +version = "0.99.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3da29a38df43d6f156149c9b43ded5e018ddff2a855cf2cfd62e8cd7d079c69f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[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 = "dlv-list" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" + +[[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 = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[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.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[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 = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.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.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + +[[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.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http", + "hyper", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.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 = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "impl-more" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" + +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown 0.15.2", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "is-terminal" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[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 = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" + +[[package]] +name = "litemap" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" + +[[package]] +name = "local-channel" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" +dependencies = [ + "futures-core", + "futures-sink", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff70ce3e48ae43fa075863cef62e8b43b71a4f2382229920e0df362592919430" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.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", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[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.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "ordered-multimap" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" +dependencies = [ + "dlv-list", + "hashbrown 0.12.3", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pest" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" +dependencies = [ + "memchr", + "thiserror 2.0.12", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[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 = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +dependencies = [ + "rand_chacha", + "rand_core", + "zerocopy", +] + +[[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.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" + +[[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.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls 0.21.12", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tokio-rustls 0.24.1", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 0.25.4", + "winreg", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted 0.7.1", + "web-sys", + "winapi", +] + +[[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.15", + "libc", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "ron" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" +dependencies = [ + "base64 0.13.1", + "bitflags 1.3.2", + "serde", +] + +[[package]] +name = "rust-ini" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" +dependencies = [ + "bitflags 2.9.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.20.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" +dependencies = [ + "log", + "ring 0.16.20", + "sct", + "webpki", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring 0.17.14", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring 0.17.14", + "untrusted 0.9.0", +] + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[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 = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring 0.17.14", + "untrusted 0.9.0", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.0", + "core-foundation", + "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 = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +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-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" + +[[package]] +name = "socket2" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +dependencies = [ + "fastrand", + "getrandom 0.3.2", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +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.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.44.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "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.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls 0.20.9", + "tokio", + "webpki", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[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 = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +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 = "twitter-monitor" +version = "0.1.0" +dependencies = [ + "actix", + "actix-cors", + "actix-files", + "actix-rt", + "actix-web", + "actix-web-actors", + "anyhow", + "chrono", + "clap", + "config", + "env_logger", + "futures", + "log", + "regex", + "reqwest", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "urlencoding", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "v_htmlescape" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "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 = "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 = "webpki" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" +dependencies = [ + "ring 0.17.14", + "untrusted 0.9.0", +] + +[[package]] +name = "webpki-roots" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +dependencies = [ + "webpki", +] + +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[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-result" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +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 = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0f641fc --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "twitter-monitor" +version = "0.1.0" +edition = "2021" + +[dependencies] +# HTTP client +reqwest = { version = "0.11", features = ["json", "rustls-tls"] } +tokio = { version = "1.0", features = ["full"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +anyhow = "1.0" +thiserror = "1.0" +clap = { version = "4.0", features = ["derive"] } +config = "0.13" +log = "0.4" +env_logger = "0.10" + +# Web framework +actix-web = { version = "4.0", default-features = false, features = ["rustls"] } +actix-rt = "2.0" +actix-cors = "0.6" +actix-files = "0.6" +actix = "0.13" +actix-web-actors = "4.0" + +# Other +futures = "0.3" +regex = "1.0" + +# Datetime handling +chrono = "0.4" + +# URL utilities +urlencoding = "2.1" diff --git a/README.md b/README.md new file mode 100644 index 0000000..9bb92b5 --- /dev/null +++ b/README.md @@ -0,0 +1,204 @@ +# Twitter Monitor + +A Rust-based Twitter monitoring service that streams tweets from your home timeline to a web frontend. Supports both cURL-based authentication and Twitter API authentication. + +## Features + +- Real-time home timeline monitoring +- WebSocket-based streaming +- Detailed response logging to files +- Support for both cURL and API authentication +- Clean, responsive web interface +- Configurable polling intervals with countdown timer +- Robust error handling and retries + +## How It Works + +The application consists of three main components: + +1. **Twitter Client**: Handles authentication and API requests to fetch tweets +2. **WebSocket Server**: Streams tweets to connected clients in real-time +3. **Frontend**: Displays tweets and provides a user interface + +## Authentication Methods + +### cURL-based Authentication (Recommended) + +This method captures an authenticated browser session to access Twitter without API keys: + +1. **Get cURL Command**: + ```bash + # Using Chrome browser: + 1. Go to x.com (Twitter) + 2. Log in to your account + 3. Navigate to your home timeline (click "For you" or "Following") + 4. Press F12 to open DevTools + 5. Go to the Network tab + 6. Find the "HomeLatestTimeline" request + 7. Right-click on the request + 8. Select "Copy" > "Copy as cURL (bash)" + ``` + +2. **Save cURL Command**: + ```bash + # Create a file in the config directory + mkdir -p config + touch config/curl.txt + + # Paste the copied cURL command into curl.txt + ``` + +### Twitter API Authentication (Alternative) + +For official API access (Not fully implemented yet): + +1. **Create Twitter Developer Account** at https://developer.twitter.com/ +2. **Get API Keys** from your Twitter Developer Portal +3. **Configure API Keys** in your configuration file + +## Installation + +1. **Clone the Repository**: + ```bash + git clone https://github.com/yourusername/twitter-monitor + cd twitter-monitor + ``` + +2. **Build the Project**: + ```bash + cargo build --release + ``` + +3. **Configure the Project**: + Create a configuration file in the `config` directory: + + ```yaml + # config/session.yaml + twitter: + # Path to your curl command file + curl_file: "config/curl.txt" + # Or use a bearer token (coming soon) + # bearer_token: "your_bearer_token" + + monitoring: + # Polling interval in seconds + polling_interval: 60 + # Maximum tweets to fetch per request + max_tweets: 10 + + server: + # WebSocket server host + host: "127.0.0.1" + # WebSocket server port + port: 8080 + ``` + +## Running the Project + +Start the server with: + +```bash +# Development mode +cargo run -- --config config/session.yaml + +# Production mode +cargo run --release -- --config config/session.yaml +``` + +The application will: +1. Parse the cURL command to extract authentication details +2. Start monitoring your Twitter home timeline +3. Start a WebSocket server to stream tweets +4. Save all Twitter API responses to files for debugging + +## Project Architecture + +### Core Components + +1. **TwitterClient** (`src/twitter/client.rs`) + - Responsible for Twitter API interactions + - Handles both cURL-based and API token authentication + - Implements tweet parsing and monitoring logic + - Includes robust error handling and retries + +2. **CurlAuth** (`src/twitter/curl.rs`) + - Parses cURL commands to extract authentication data + - Extracts URL, headers, cookies, and request data + - Provides logging of parsed components + +3. **Server** (`src/server.rs`) + - Manages WebSocket connections + - Broadcasts tweets to connected clients + - Implements client heartbeat mechanisms + +4. **Main** (`src/main.rs`) + - Coordinates application components + - Loads configuration and initializes services + - Sets up monitoring and broadcasting + +### Technical Challenges Solved + +1. **Twitter API Authentication** + - Implemented parsing of cURL commands to extract session cookies + - Added required feature flags to match Twitter's GraphQL API requirements + - Handled authentication errors with clear messaging + +2. **Robust Data Handling** + - Implemented safe UTF-8 text handling to avoid panics + - Added detailed response logging to files for debugging + - Carefully parsed nested JSON structures + +3. **Real-time Communication** + - Built WebSocket server with proper connection handling + - Implemented heartbeat mechanism to detect disconnected clients + - Added broadcast functionality with error handling + +4. **User Experience** + - Added countdown timer between scans + - Implemented clean logging for better readability + - Provided detailed status updates during operation + +## Troubleshooting + +### cURL Authentication Issues + +1. **Session Expired**: + - Twitter sessions expire after some time + - Generate a new cURL command and update your `curl.txt` file + - Restart the application + +2. **400 Bad Request Errors**: + - Twitter's API requirements change frequently + - The application automatically adds required feature flags + - Response details are saved to `twitter_response_*.txt` files for debugging + +3. **Parse Errors**: + - Twitter's response format may change + - Check the saved response files + - Update the parsing logic if necessary + +## Configuration Options + +```yaml +twitter: + # Path to your curl command file + curl_file: "config/curl.txt" + # Or use a bearer token (coming soon) + # bearer_token: "your_bearer_token" + +monitoring: + # Polling interval in seconds + polling_interval: 60 + # Maximum tweets to fetch per request + max_tweets: 10 + +server: + # WebSocket server host + host: "127.0.0.1" + # WebSocket server port + port: 8080 +``` + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. \ No newline at end of file diff --git a/config/api.yaml b/config/api.yaml new file mode 100644 index 0000000..3ce8b72 --- /dev/null +++ b/config/api.yaml @@ -0,0 +1,39 @@ +# Twitter API Configuration (API-based) +twitter: + # Base URL for Twitter API + base_url: "https://api.twitter.com/2" + + # API-based authentication using bearer token + bearer_token: "AAAAAAAAAAAAAAAAAAAAAI5P0QEAAAAAVe2ijbvQuIj0Bsfv%2F98u9zYnHbk%3DJuKMSUNSy3ae71QWL58fS1S1KUcJVr3Om5hJjVF1BqJqzWZQLL" + + # API endpoints + endpoints: + search: "/tweets/search/recent" + user: "/users" + timeline: "/timeline/home" + +# Monitoring Configuration +monitoring: + # List of Twitter usernames to monitor + usernames: + - "elonmusk" + - "jack" + + # Polling interval in seconds + polling_interval: 300 # 5 minutes + + # Maximum number of tweets to fetch per request + max_tweets: 10 + +# Database Configuration +database: + # SQLite database file path + path: "tweets.db" + +# Logging Configuration +logging: + # Log level (debug, info, warn, error) + level: "info" + + # Log file path + file: "twitter_monitor.log" \ No newline at end of file diff --git a/config/curl.example.txt b/config/curl.example.txt new file mode 100644 index 0000000..b716bac --- /dev/null +++ b/config/curl.example.txt @@ -0,0 +1,3 @@ +curl '' \ + -H 'x-twitter-auth-type: OAuth2Session' \ + -H 'x-twitter-client-language: en' \ No newline at end of file diff --git a/config/default.yaml b/config/default.yaml new file mode 100644 index 0000000..cd5b316 --- /dev/null +++ b/config/default.yaml @@ -0,0 +1,46 @@ +# Twitter API Configuration +twitter: + # Base URL for Twitter API + base_url: "https://api.twitter.com/2" + + # Authentication Method (choose one): + # 1. Session-based authentication using cURL command + # 2. API-based authentication using bearer token + + # For session-based authentication: + # curl_command: "curl 'https://twitter.com/i/api/2/timeline/home.json' -H 'authorization: Bearer ...' -H 'cookie: ...'" + + # For API-based authentication: + bearer_token: "AAAAAAAAAAAAAAAAAAAAAI5P0QEAAAAAVe2ijbvQuIj0Bsfv%2F98u9zYnHbk%3DJuKMSUNSy3ae71QWL58fS1S1KUcJVr3Om5hJjVF1BqJqzWZQLL" + + # API endpoints + endpoints: + search: "/tweets/search/recent" + user: "/users" + timeline: "/timeline/home" + +# Monitoring Configuration +monitoring: + # List of Twitter usernames to monitor + usernames: + - "elonmusk" + - "jack" + + # Polling interval in seconds + polling_interval: 300 # 5 minutes + + # Maximum number of tweets to fetch per request + max_tweets: 10 + +# Database Configuration +database: + # SQLite database file path + path: "tweets.db" + +# Logging Configuration +logging: + # Log level (debug, info, warn, error) + level: "info" + + # Log file path + file: "twitter_monitor.log" \ No newline at end of file diff --git a/config/session.yaml b/config/session.yaml new file mode 100644 index 0000000..8b0a980 --- /dev/null +++ b/config/session.yaml @@ -0,0 +1,16 @@ +# Twitter API Configuration (Session-based) +twitter: + curl_file: "./config/curl.txt" + +# Monitoring Configuration +monitoring: + # Polling interval in seconds + polling_interval: 180 + + # Maximum number of tweets to fetch per request + max_tweets: 20 + +# Server Configuration +server: + host: "127.0.0.1" + port: 8080 \ No newline at end of file diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..721bcba --- /dev/null +++ b/src/config.rs @@ -0,0 +1,66 @@ +use anyhow::{Context, Result}; +use serde::Deserialize; +use std::fs; +use std::path::Path; + +#[derive(Debug, Deserialize)] +pub struct Config { + pub twitter: TwitterConfig, + pub monitoring: MonitoringConfig, + pub database: DatabaseConfig, + pub logging: LoggingConfig, +} + +#[derive(Debug, Deserialize)] +pub struct TwitterConfig { + pub base_url: String, + #[serde(default)] + pub curl_command: Option, + #[serde(default)] + pub bearer_token: Option, + pub endpoints: EndpointConfig, +} + +#[derive(Debug, Deserialize)] +pub struct EndpointConfig { + pub search: String, + pub user: String, + pub timeline: String, +} + +#[derive(Debug, Deserialize)] +pub struct MonitoringConfig { + pub usernames: Vec, + pub polling_interval: u64, + pub max_tweets: u32, +} + +#[derive(Debug, Deserialize)] +pub struct DatabaseConfig { + pub path: String, +} + +#[derive(Debug, Deserialize)] +pub struct LoggingConfig { + pub level: String, + pub file: String, +} + +impl Config { + pub fn load(path: &Path) -> Result { + let content = fs::read_to_string(path) + .with_context(|| format!("Failed to read config file: {}", path.display()))?; + + let config: Config = serde_yaml::from_str(&content) + .with_context(|| format!("Failed to parse config file: {}", path.display()))?; + + // Validate authentication configuration + if config.twitter.curl_command.is_none() && config.twitter.bearer_token.is_none() { + return Err(anyhow::anyhow!( + "No authentication method configured. Please provide either curl_command or bearer_token" + )); + } + + Ok(config) + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..c7f31b4 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,135 @@ +mod twitter; +mod server; + +use anyhow::{anyhow, Result}; +use clap::Parser; +use config::Config; +use std::fs; +use std::path::PathBuf; +use std::time::Duration; +use twitter::client::TwitterClient; +use twitter::curl::CurlAuth; +use serde::Deserialize; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + /// Path to the configuration file + #[arg(short, long, value_name = "FILE", default_value = "config/session.yaml")] + config: String, + + /// Authentication method to use (session or api) + #[arg(short, long, value_name = "METHOD", default_value = "session")] + auth_method: String, +} + +#[derive(Clone, Deserialize, Debug)] +struct Settings { + twitter: TwitterSettings, + server: ServerSettings, + monitoring: MonitoringSettings, +} + +#[derive(Clone, Deserialize, Debug)] +struct TwitterSettings { + curl_file: Option, + bearer_token: Option, +} + +#[derive(Clone, Deserialize, Debug)] +struct ServerSettings { + host: String, + port: u16, +} + +#[derive(Clone, Deserialize, Debug)] +struct MonitoringSettings { + polling_interval: u64, + max_tweets: u32, +} + +#[actix_rt::main] +async fn main() -> Result<()> { + let args = Args::parse(); + + println!("Loading configuration from: {}", &args.config); + + // Load configuration + let config_builder = Config::builder() + .add_source(config::File::from(PathBuf::from(&args.config))); + + let config_result = config_builder.build(); + if let Err(e) = &config_result { + println!("Error loading config: {}", e); + return Err(anyhow!("Failed to load configuration: {}", e)); + } + + let config = config_result?; + let settings_result = config.try_deserialize::(); + if let Err(e) = &settings_result { + println!("Error deserializing config: {}", e); + return Err(anyhow!("Failed to deserialize configuration: {}", e)); + } + + let settings = settings_result?; + println!("Configuration loaded successfully"); + + // Create server + let server = server::Server::new(); + + // Initialize client + let client = if let Some(curl_file_path) = settings.twitter.curl_file.clone() { + println!("Using curl file: {}", curl_file_path); + let curl_file_result = fs::read_to_string(&curl_file_path); + if let Err(e) = &curl_file_result { + println!("Error reading curl file: {}", e); + return Err(anyhow!("Failed to read curl file: {}", e)); + } + + let curl_command = curl_file_result?; + let curl_auth_result = CurlAuth::from_curl(&curl_command); + if let Err(e) = &curl_auth_result { + println!("Error parsing curl command: {}", e); + return Err(anyhow!("Failed to parse curl command: {}", e)); + } + + let curl_auth = curl_auth_result?; + TwitterClient::new(Some(curl_auth), None) + } else if let Some(token) = settings.twitter.bearer_token.clone() { + println!("Using bearer token authentication"); + TwitterClient::new(None, Some(token)) + } else { + return Err(anyhow!("No authentication method specified in configuration")); + }; + + // Start monitoring the home timeline + println!("Starting to monitor home timeline"); + + let client_clone = client.clone(); + let server_clone = server.clone(); + let polling_interval = settings.monitoring.polling_interval; + let max_tweets = settings.monitoring.max_tweets; + + tokio::spawn(async move { + if let Err(e) = client_clone.monitor_home_timeline( + max_tweets, + |tweets| { + for tweet in tweets { + let tweet_json = serde_json::to_string(&tweet)?; + server_clone.broadcast_tweet(&tweet_json)?; + println!("New tweet from @{}: {}", tweet.author_username.as_deref().unwrap_or("unknown"), tweet.text); + } + Ok(()) + }, + Duration::from_secs(polling_interval), + ).await { + eprintln!("Error monitoring home timeline: {}", e); + } + }); + + // Start the server (this is a blocking call) + println!("Starting server on {}:{}", settings.server.host, settings.server.port); + server.start(&settings.server.host, settings.server.port).await?; + + Ok(()) +} diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..7d00e0c --- /dev/null +++ b/src/server.rs @@ -0,0 +1,129 @@ +use actix_cors::Cors; +use actix_files::Files; +use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer}; +use actix_web_actors::ws; +use actix_web_actors::ws::WebsocketContext; +use anyhow::Result; +use tokio::sync::broadcast; +use actix::Actor; +use actix::ActorContext; +use actix::AsyncContext; +use actix::StreamHandler; +use std::time::{Duration, Instant}; + +const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); +const CLIENT_TIMEOUT: Duration = Duration::from_secs(10); + +#[derive(Clone)] +pub struct Server { + pub tx: broadcast::Sender, +} + +impl Server { + pub fn new() -> Self { + let (tx, _) = broadcast::channel(100); + Self { tx } + } + + pub async fn start(&self, host: &str, port: u16) -> std::io::Result<()> { + let tx = self.tx.clone(); + + let server = HttpServer::new(move || { + let tx = tx.clone(); + + let cors = Cors::default() + .allow_any_origin() + .allow_any_method() + .allow_any_header(); + + App::new() + .wrap(cors) + .app_data(web::Data::new(tx.clone())) + .route("/ws", web::get().to(ws_index)) + .service(Files::new("/static", "./static")) + }) + .bind((host, port))?; + + server.run().await + } + + pub fn broadcast_tweet(&self, tweet: &str) -> Result<(), broadcast::error::SendError> { + match self.tx.send(tweet.to_string()) { + Ok(num_receivers) => { + if num_receivers > 0 { + println!("Tweet broadcasted to {} active WebSocket client(s)", num_receivers); + } + Ok(()) + } + Err(e) => { + Ok(()) + } + } + } +} + +async fn ws_index( + req: HttpRequest, + stream: web::Payload, + tx: web::Data>, +) -> Result { + ws::start( + WebSocketSession { + tx: tx.get_ref().clone(), + hb: Instant::now() + }, + &req, + stream + ) +} + +pub struct WebSocketSession { + tx: broadcast::Sender, + hb: Instant, +} + +impl WebSocketSession { + fn hb(&self, ctx: &mut WebsocketContext) { + ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| { + if Instant::now().duration_since(act.hb) > CLIENT_TIMEOUT { + println!("WebSocket Client heartbeat failed, disconnecting!"); + ctx.stop(); + return; + } + ctx.ping(b""); + }); + } +} + +impl Actor for WebSocketSession { + type Context = WebsocketContext; + + fn started(&mut self, ctx: &mut Self::Context) { + self.hb(ctx); + } +} + +impl StreamHandler> for WebSocketSession { + fn handle(&mut self, msg: Result, ctx: &mut Self::Context) { + match msg { + Ok(ws::Message::Ping(msg)) => { + self.hb = Instant::now(); + ctx.pong(&msg); + } + Ok(ws::Message::Pong(_)) => { + self.hb = Instant::now(); + } + Ok(ws::Message::Text(text)) => { + if let Err(e) = self.tx.send(text.to_string()) { + println!("Error sending message: {}", e); + } + } + Ok(ws::Message::Binary(bin)) => ctx.binary(bin), + Ok(ws::Message::Close(reason)) => { + ctx.close(reason); + ctx.stop(); + } + _ => ctx.stop(), + } + } +} \ No newline at end of file diff --git a/src/twitter/client.rs b/src/twitter/client.rs new file mode 100644 index 0000000..bf0ad6d --- /dev/null +++ b/src/twitter/client.rs @@ -0,0 +1,897 @@ +use anyhow::{anyhow, Result}; +use reqwest::header; +use serde::{Deserialize, Serialize}; +use std::time::Duration; +use tokio::time; +use crate::twitter::curl::CurlAuth; +use thiserror::Error; +use std::fs::File; +use std::io::{self, Write}; +use std::path::Path; +use urlencoding; + +#[derive(Debug, thiserror::Error)] +pub enum TwitterError { + #[error("Failed to parse tweet data: {0}")] + ParseError(String), + #[error("API request failed: {0}")] + RequestError(#[from] reqwest::Error), + #[error("Authentication failed: {0}")] + AuthError(String), + #[error("Rate limit exceeded")] + RateLimitExceeded, + #[error("No authentication method configured")] + NoAuthMethod, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Tweet { + pub id: String, + pub text: String, + pub author_id: String, + pub author_username: Option, + pub author_name: Option, + pub created_at: String, + pub entities: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TweetEntities { + pub hashtags: Option>, + pub mentions: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Hashtag { + pub tag: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Mention { + pub username: String, + pub id: String, +} + +#[derive(Clone)] +pub struct TwitterClient { + client: reqwest::Client, + curl_auth: Option, + bearer_token: Option, + home_timeline_url: String, +} + +impl TwitterClient { + pub fn new( + curl_auth: Option, + bearer_token: Option, + ) -> Self { + let client_builder = reqwest::Client::builder() + .timeout(Duration::from_secs(30)) + .redirect(reqwest::redirect::Policy::none()) + .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"); + + // Build client with proper headers based on auth method + let client = match &curl_auth { + Some(auth) => { + client_builder + .default_headers(auth.headers.clone()) + .build() + .unwrap() + }, + None if bearer_token.is_some() => { + let mut headers = header::HeaderMap::new(); + if let Some(token) = &bearer_token { + if let Ok(auth_value) = format!("Bearer {}", token).parse() { + headers.insert(header::AUTHORIZATION, auth_value); + } + } + client_builder + .default_headers(headers) + .build() + .unwrap() + }, + _ => client_builder.build().unwrap(), + }; + + // Process URL from the curl command + let home_timeline_url = if let Some(ref auth) = curl_auth { + let url = &auth.url; + + // For GraphQL endpoints, we should strip query parameters + // and use them in the request body instead + if url.contains("/graphql/") && url.contains('?') { + // Extract the base URL without query parameters + if let Some(base_url) = url.split('?').next() { + // Parse any URL parameters that might be useful + if let Some(params) = parse_twitter_url_params(url) { + println!("Extracted URL parameters - will use in POST request."); + println!("Using base URL: {}", base_url); + + // Now check if we have a data field in the curl auth + // If not, we need to create one from the URL parameters + if auth.data.is_none() && curl_auth.is_some() { + // Create data JSON from URL parameters + let (variables, features) = params; + let mut data = serde_json::json!({}); + + if let Some(vars) = variables { + data["variables"] = vars; + } + + if let Some(feats) = features { + data["features"] = feats; + } + + // Can't modify auth directly since it's borrowed, + // so we'll handle this in get_home_timeline instead + println!("Created data from URL parameters"); + } + } + + base_url.to_string() + } else { + url.clone() + } + } else { + url.clone() + } + } else { + "https://x.com/i/api/graphql/nMyTQqsJiUGBKLGNSQamAA/HomeLatestTimeline".to_string() + }; + + Self { + client, + curl_auth, + bearer_token, + home_timeline_url, + } + } + + pub async fn get_home_timeline(&self, max_results: u32) -> Result> { + println!("Making request to URL: {}", self.home_timeline_url); + + // Check if we have auth data + if let Some(ref auth) = self.curl_auth { + let url = &auth.url; + + println!("Using URL from curl command: {}", url); + + // Extract cursor information if present + let cursor = extract_cursor_from_url(url); + if let Some(ref cursor_val) = cursor { + println!("Found cursor in URL: {}", cursor_val); + } + + // Get the base URL without query parameters + let base_url = url.split('?').next().unwrap_or(url); + + // For Twitter GraphQL API, we should prefer POST requests for complex queries + if url.contains("graphql") && auth.data.is_some() { + println!("Using POST request for GraphQL API"); + + // Get the request body from curl command + let body = auth.data.as_ref().map(|d| d.as_str()).unwrap_or("{}"); + println!("Request body: {}", body); + + // Always add required features to the request body + let fixed_body = { + let mut json_body: serde_json::Value = serde_json::from_str(body).unwrap_or(serde_json::json!({})); + + // Add the required features if not already present + if !json_body.get("features").is_some() { + println!("Adding missing features to request body"); + json_body["features"] = serde_json::json!({ + "verified_phone_label_enabled": true, + "responsive_web_graphql_exclude_directive_enabled": true, + "responsive_web_graphql_timeline_navigation_enabled": true, + "responsive_web_graphql_skip_user_profile_image_extensions_enabled": false, + "creator_subscriptions_tweet_preview_api_enabled": true, + "responsive_web_enhance_cards_enabled": false, + "view_counts_everywhere_api_enabled": true, + "longform_notetweets_consumption_enabled": true, + "responsive_web_twitter_article_tweet_consumption_enabled": false, + "responsive_web_grok_share_attachment_enabled": false, + "responsive_web_jetfuel_frame": false, + // Add all the missing features + "responsive_web_grok_image_annotation_enabled": true, + "articles_preview_enabled": true, + "responsive_web_grok_analyze_button_fetch_trends_enabled": true, + "premium_content_api_read_enabled": true, + "longform_notetweets_rich_text_read_enabled": true, + "profile_label_improvements_pcf_label_in_post_enabled": true, + "tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": true, + "graphql_is_translatable_rweb_tweet_is_translatable_enabled": true, + "rweb_video_screen_enabled": true, + "responsive_web_grok_show_grok_translated_post": true, + "responsive_web_grok_analysis_button_from_backend": true, + "creator_subscriptions_quote_tweet_preview_enabled": true, + "tweet_awards_web_tipping_enabled": true, + "communities_web_enable_tweet_community_results_fetch": true, + "responsive_web_edit_tweet_api_enabled": true, + "c9s_tweet_anatomy_moderator_badge_enabled": true, + "standardized_nudges_misinfo": true, + "responsive_web_grok_analyze_post_followups_enabled": true, + "freedom_of_speech_not_reach_fetch_enabled": true, + "rweb_tipjar_consumption_enabled": true, + "longform_notetweets_inline_media_enabled": true + }); + } + + // Add variables if not already present + if !json_body.get("variables").is_some() { + println!("Adding missing variables to request body"); + json_body["variables"] = serde_json::json!({ + "count": max_results, + "includePromotedContent": false, + "latestControlAvailable": true, + "requestContext": "launch" + }); + + // If we have a cursor, add it to the variables + if let Some(ref cursor_val) = cursor { + json_body["variables"]["cursor"] = serde_json::Value::String(cursor_val.clone()); + } + } + + serde_json::to_string(&json_body).unwrap_or(body.to_string()) + }; + + println!("Using request body: {}", fixed_body); + + // Send POST request with the body - use the base URL without query parameters + let response = self.client + .post(base_url) + .body(fixed_body) + .send() + .await?; + + println!("Response status: {}", response.status()); + + // Check response status + if response.status() == reqwest::StatusCode::TOO_MANY_REQUESTS { + return Err(TwitterError::RateLimitExceeded.into()); + } + + if response.status() == reqwest::StatusCode::UNAUTHORIZED { + return Err(TwitterError::AuthError("Authentication failed".to_string()).into()); + } + + if !response.status().is_success() { + let status = response.status(); + let error_text = response.text().await?; + println!("Error response body: {}", error_text); + return Err(anyhow!("Request failed with status: {}", status)); + } + + // Parse the response + let response_body = response.text().await?; + + // // Uncomment if you want to see the response body + // let timestamp = chrono::Utc::now().timestamp(); + // let filename = format!("twitter_response_{}.txt", timestamp); + // println!("Writing response to file: {}", filename); + // + // if let Err(e) = save_response_to_file(&response_body, &filename) { + // println!("Error saving response to file: {}", e); + // } + + let timeline_data: serde_json::Value = serde_json::from_str(&response_body)?; + let tweets = self.parse_timeline_tweets(timeline_data)?; + + // Limit the number of tweets to max_results + let limited_tweets = if tweets.len() > max_results as usize { + tweets[0..max_results as usize].to_vec() + } else { + tweets + }; + + Ok(limited_tweets) + } else if url.contains("variables=") || url.contains("features=") { + println!("URL contains query parameters, using GET request"); + + // If URL already has query parameters, use it as is + let request_url = if url.contains("features=") { + url.to_string() + } else { + // Add features to the URL if not present + let mut url_with_features = url.to_string(); + + // Add separator if needed + if !url_with_features.contains('?') { + url_with_features.push('?'); + } else if !url_with_features.ends_with('&') && !url_with_features.ends_with('?') { + url_with_features.push('&'); + } + + // Add feature flags + url_with_features.push_str( + "features=verified_phone_label_enabled,responsive_web_graphql_exclude_directive_enabled,\ + responsive_web_graphql_timeline_navigation_enabled,responsive_web_graphql_skip_user_profile_image_extensions_enabled,\ + creator_subscriptions_tweet_preview_api_enabled,responsive_web_enhance_cards_enabled,\ + view_counts_everywhere_api_enabled,longform_notetweets_consumption_enabled,\ + responsive_web_twitter_article_tweet_consumption_enabled,responsive_web_grok_image_annotation_enabled,\ + articles_preview_enabled,responsive_web_grok_analyze_button_fetch_trends_enabled,\ + premium_content_api_read_enabled,longform_notetweets_rich_text_read_enabled,\ + profile_label_improvements_pcf_label_in_post_enabled,tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled,\ + graphql_is_translatable_rweb_tweet_is_translatable_enabled,rweb_video_screen_enabled,\ + responsive_web_grok_show_grok_translated_post,responsive_web_grok_analysis_button_from_backend,\ + creator_subscriptions_quote_tweet_preview_enabled,tweet_awards_web_tipping_enabled,\ + communities_web_enable_tweet_community_results_fetch,responsive_web_edit_tweet_api_enabled,\ + c9s_tweet_anatomy_moderator_badge_enabled,standardized_nudges_misinfo,\ + responsive_web_grok_analyze_post_followups_enabled,freedom_of_speech_not_reach_fetch_enabled,\ + rweb_tipjar_consumption_enabled,longform_notetweets_inline_media_enabled" + ); + + url_with_features + }; + + println!("Making GET request to: {}", request_url); + + // Send GET request with headers but no body + let response = self.client + .get(&request_url) + .send() + .await?; + + println!("Response status: {}", response.status()); + + // Check response status + if response.status() == reqwest::StatusCode::TOO_MANY_REQUESTS { + return Err(TwitterError::RateLimitExceeded.into()); + } + + if response.status() == reqwest::StatusCode::UNAUTHORIZED { + return Err(TwitterError::AuthError("Authentication failed".to_string()).into()); + } + + if !response.status().is_success() { + let status = response.status(); + let error_text = response.text().await?; + println!("Error response body: {}", error_text); + return Err(anyhow!("Request failed with status: {}", status)); + } + + // Parse the response + let response_body = response.text().await?; + + // // Uncomment if you want to see the response body + // let timestamp = chrono::Utc::now().timestamp(); + // let filename = format!("twitter_response_{}.txt", timestamp); + // println!("Writing response to file: {}", filename); + // + // if let Err(e) = save_response_to_file(&response_body, &filename) { + // println!("Error saving response to file: {}", e); + // } + + let timeline_data: serde_json::Value = serde_json::from_str(&response_body)?; + let tweets = self.parse_timeline_tweets(timeline_data)?; + + // Limit the number of tweets to max_results + let limited_tweets = if tweets.len() > max_results as usize { + tweets[0..max_results as usize].to_vec() + } else { + tweets + }; + + Ok(limited_tweets) + } else { + // Get the request body from curl command + let body = auth.data.as_ref().map(|d| d.as_str()).unwrap_or("{}"); + println!("Request body: {}", body); + + // Always add required features to the request body + let fixed_body = { + let mut json_body: serde_json::Value = serde_json::from_str(body).unwrap_or(serde_json::json!({})); + + // Add the required features if not already present + if !json_body.get("features").is_some() { + println!("Adding missing features to request body"); + json_body["features"] = serde_json::json!({ + "verified_phone_label_enabled": true, + "responsive_web_graphql_exclude_directive_enabled": true, + "responsive_web_graphql_timeline_navigation_enabled": true, + "responsive_web_graphql_skip_user_profile_image_extensions_enabled": false, + "creator_subscriptions_tweet_preview_api_enabled": true, + "responsive_web_enhance_cards_enabled": false, + "view_counts_everywhere_api_enabled": true, + "longform_notetweets_consumption_enabled": true, + "responsive_web_twitter_article_tweet_consumption_enabled": false, + "responsive_web_grok_share_attachment_enabled": false, + "responsive_web_jetfuel_frame": false, + // Add all the missing features + "responsive_web_grok_image_annotation_enabled": true, + "articles_preview_enabled": true, + "responsive_web_grok_analyze_button_fetch_trends_enabled": true, + "premium_content_api_read_enabled": true, + "longform_notetweets_rich_text_read_enabled": true, + "profile_label_improvements_pcf_label_in_post_enabled": true, + "tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": true, + "graphql_is_translatable_rweb_tweet_is_translatable_enabled": true, + "rweb_video_screen_enabled": true, + "responsive_web_grok_show_grok_translated_post": true, + "responsive_web_grok_analysis_button_from_backend": true, + "creator_subscriptions_quote_tweet_preview_enabled": true, + "tweet_awards_web_tipping_enabled": true, + "communities_web_enable_tweet_community_results_fetch": true, + "responsive_web_edit_tweet_api_enabled": true, + "c9s_tweet_anatomy_moderator_badge_enabled": true, + "standardized_nudges_misinfo": true, + "responsive_web_grok_analyze_post_followups_enabled": true, + "freedom_of_speech_not_reach_fetch_enabled": true, + "rweb_tipjar_consumption_enabled": true, + "longform_notetweets_inline_media_enabled": true + }); + } + + // Add variables if not already present + if !json_body.get("variables").is_some() { + println!("Adding missing variables to request body"); + json_body["variables"] = serde_json::json!({ + "count": max_results, + "includePromotedContent": false, + "latestControlAvailable": true, + "requestContext": "launch" + }); + } + + serde_json::to_string(&json_body).unwrap_or(body.to_string()) + }; + + println!("Using request body: {}", fixed_body); + + // Send POST request with the body + let response = self.client + .post(url) + .body(fixed_body) + .send() + .await?; + + println!("Response status: {}", response.status()); + + // Check response status + if response.status() == reqwest::StatusCode::TOO_MANY_REQUESTS { + return Err(TwitterError::RateLimitExceeded.into()); + } + + if response.status() == reqwest::StatusCode::UNAUTHORIZED { + return Err(TwitterError::AuthError("Authentication failed".to_string()).into()); + } + + if !response.status().is_success() { + let status = response.status(); + let error_text = response.text().await?; + println!("Error response body: {}", error_text); + return Err(anyhow!("Request failed with status: {}", status)); + } + + // Parse the response + let response_body = response.text().await?; + + // // Uncomment if you want to see the response body + // let timestamp = chrono::Utc::now().timestamp(); + // let filename = format!("twitter_response_{}.txt", timestamp); + // println!("Writing response to file: {}", filename); + // + // if let Err(e) = save_response_to_file(&response_body, &filename) { + // println!("Error saving response to file: {}", e); + // } + + let timeline_data: serde_json::Value = serde_json::from_str(&response_body)?; + let tweets = self.parse_timeline_tweets(timeline_data)?; + + // Limit the number of tweets to max_results + let limited_tweets = if tweets.len() > max_results as usize { + tweets[0..max_results as usize].to_vec() + } else { + tweets + }; + + Ok(limited_tweets) + } + } else if let Some(_token) = &self.bearer_token { + // If we implement Bearer token auth in the future + return Err(anyhow!("Bearer token auth not implemented yet")); + } else { + Err(TwitterError::NoAuthMethod.into()) + } + } + + fn parse_timeline_tweets(&self, data: serde_json::Value) -> Result> { + let mut tweets = Vec::new(); + + println!("Parsing tweets from response data..."); + + // Check top-level structure + if !data.get("data").is_some() { + println!("Warning: Data field missing in response"); + println!("Response keys: {:?}", data.as_object().map(|o| o.keys().collect::>())); + return Ok(vec![]); + } + + // Try to navigate to the timeline data + let timeline_data = data.get("data") + .and_then(|d| d.get("home")) + .and_then(|h| h.get("home_timeline_urt")); + + if timeline_data.is_none() { + println!("Warning: Could not find home_timeline_urt in response"); + if let Some(data_obj) = data.get("data") { + println!("Data keys: {:?}", data_obj.as_object().map(|o| o.keys().collect::>())); + if let Some(home) = data_obj.get("home") { + println!("Home keys: {:?}", home.as_object().map(|o| o.keys().collect::>())); + } + } + return Ok(vec![]); + } + + // Get instructions + let instructions = timeline_data.and_then(|t| t.get("instructions")).and_then(|i| i.as_array()); + if instructions.is_none() { + println!("Warning: No instructions found in timeline data"); + return Ok(vec![]); + } + + // Process instructions + let instructions = instructions.unwrap(); + println!("Found {} instruction(s)", instructions.len()); + + // Track if we found any actual tweets + let mut found_tweets = false; + let mut found_cursors = false; + + for (i, instruction) in instructions.iter().enumerate() { + println!("Processing instruction {} type: {}", i, + instruction.get("type").and_then(|t| t.as_str()).unwrap_or("unknown")); + + if let Some(entries) = instruction.get("entries").and_then(|e| e.as_array()) { + println!("Found {} entries in instruction {}", entries.len(), i); + + // Check if we only have cursor entries + let mut cursor_count = 0; + + for entry in entries.iter() { + if let Some(content) = entry.get("content") { + if let Some(entry_type) = content.get("entryType").and_then(|t| t.as_str()) { + if entry_type == "TimelineTimelineCursor" { + cursor_count += 1; + found_cursors = true; + + // Print cursor details for debugging + if let Some(cursor_type) = content.get("cursorType").and_then(|t| t.as_str()) { + println!(" Found cursor: {} - value: {}", cursor_type, + content.get("value").and_then(|v| v.as_str()).unwrap_or("unknown")); + } + } + } + } + } + + // If all entries are cursors, we might need to fetch with the cursor value + if cursor_count == entries.len() && cursor_count > 0 { + println!("WARNING: Response only contains cursor entries, no tweets! You may need to update your curl command."); + } + + // Continue with normal processing + for (j, entry) in entries.iter().enumerate() { + if let Some(content) = entry.get("content").and_then(|c| c.as_object()) { + // Try to find the tweet data + let tweet_data = content.get("itemContent") + .and_then(|ic| ic.get("tweet_results")) + .and_then(|tr| tr.get("result")); + + if tweet_data.is_none() { + // Skip this entry if it's not a tweet + continue; + } + + found_tweets = true; + let tweet = tweet_data.unwrap(); + + // Get user info + let user = tweet.get("core") + .and_then(|c| c.get("user_results")) + .and_then(|ur| ur.get("result")); + + if user.is_none() { + println!("Warning: User data missing for tweet in entry {}", j); + continue; + } + + let author_username = user + .and_then(|u| u.get("legacy")) + .and_then(|l| l.get("screen_name")) + .and_then(|n| n.as_str()) + .map(|s| s.to_string()); + + let author_name = user + .and_then(|u| u.get("legacy")) + .and_then(|l| l.get("name")) + .and_then(|n| n.as_str()) + .map(|s| s.to_string()); + + if let (Some(id), Some(created_at)) = ( + tweet.get("rest_id").and_then(|i| i.as_str()), + tweet.get("legacy").and_then(|l| l.get("created_at")).and_then(|c| c.as_str()), + ) { + // First try legacy.full_text + let text = tweet.get("legacy") + .and_then(|l| l.get("full_text")) + .and_then(|t| t.as_str()) + .or_else(|| { + // Then try note_tweet.text if it exists (for longer tweets) + tweet.get("note_tweet") + .and_then(|nt| nt.get("note_tweet_results")) + .and_then(|ntr| ntr.get("result")) + .and_then(|r| r.get("text")) + .and_then(|t| t.as_str()) + }) + .unwrap_or("Tweet text not found"); + + // Get author_id from user results + let author_id = tweet.get("core") + .and_then(|c| c.get("user_results")) + .and_then(|ur| ur.get("result")) + .and_then(|r| r.get("rest_id")) + .and_then(|ri| ri.as_str()) + .unwrap_or("unknown"); + + println!("Found tweet by @{}: {}", + author_username.as_deref().unwrap_or("unknown"), + safe_truncate(&text, 100)); + + let tweet = Tweet { + id: id.to_string(), + text: text.to_string(), + author_id: author_id.to_string(), + author_username, + author_name, + created_at: created_at.to_string(), + entities: None, + }; + tweets.push(tweet); + } else { + println!("Warning: Required tweet data missing for entry {}", j); + } + } + } + } else { + println!("Warning: No entries found in instruction {}", i); + } + } + + // Special message if we got cursor data but no tweets + if found_cursors && !found_tweets { + println!("\n==============================================================="); + println!("IMPORTANT: Twitter returned cursor data but no actual tweets!"); + println!("This usually happens when:"); + println!("1. Your curl command is expired or missing essential cookies"); + println!("2. You need to include a valid cursor value in your request"); + println!("3. Twitter has changed their API response format"); + println!("\nTry getting a fresh curl command from your browser's Network tab"); + println!("===============================================================\n"); + } + + println!("Successfully parsed {} tweets", tweets.len()); + Ok(tweets) + } + + pub async fn monitor_home_timeline( + &self, + max_tweets: u32, + callback: impl Fn(Vec) -> Result<()>, + interval: Duration, + ) -> Result<()> { + let mut last_tweet_id = None; + let mut retry_count = 0; + const MAX_RETRIES: u32 = 3; + + println!("Starting home timeline monitoring..."); + + loop { + match self.get_home_timeline(max_tweets).await { + Ok(tweets) => { + retry_count = 0; + + if !tweets.is_empty() { + println!("Received {} tweets", tweets.len()); + + if let Some(ref last_id) = last_tweet_id { + let new_tweets: Vec = tweets + .iter() + .take_while(|t| t.id != *last_id) + .cloned() + .collect(); + + if !new_tweets.is_empty() { + println!("Found {} new tweets", new_tweets.len()); + + // Print full text of each new tweet + for tweet in &new_tweets { + println!("\n==== Tweet from @{} ====", tweet.author_username.as_deref().unwrap_or("unknown")); + println!("{}", tweet.text); + println!("====================\n"); + } + + if let Err(e) = callback(new_tweets) { + // Log the error but continue monitoring + println!("Error in callback: {}. Continuing monitoring...", e); + } + } else { + println!("No new tweets"); + } + } else { + // First run, process all tweets + + // Print full text of a few tweets on first run + for tweet in tweets.iter().take(3) { + println!("\n==== Tweet from @{} ====", tweet.author_username.as_deref().unwrap_or("unknown")); + println!("{}", tweet.text); + println!("====================\n"); + } + + if let Err(e) = callback(tweets.clone()) { + // Log the error but continue monitoring + println!("Error in callback: {}. Continuing monitoring...", e); + } + } + + last_tweet_id = Some(tweets[0].id.clone()); + } else { + println!("No tweets found in timeline"); + } + } + Err(e) => { + println!("Error retrieving timeline: {}", e); + retry_count += 1; + if retry_count >= MAX_RETRIES { + return Err(e); + } + let backoff = Duration::from_secs(2u64.pow(retry_count - 1)); + println!("Retrying in {} seconds", backoff.as_secs()); + time::sleep(backoff).await; + continue; + } + } + + println!("Sleeping for {} seconds", interval.as_secs()); + + // Countdown timer logic + let start = std::time::Instant::now(); + let total_secs = interval.as_secs(); + + for remaining in (1..=total_secs).rev() { + let elapsed = start.elapsed().as_secs(); + if elapsed >= total_secs { + break; + } + + let remaining = total_secs - elapsed; + // Only print every 5 seconds to reduce console spam + if remaining % 5 == 0 || remaining <= 3 { + print!("\rNext scan in {} seconds...", remaining); + std::io::Write::flush(&mut std::io::stdout()).unwrap_or(()); + } + + // Sleep for 1 second + time::sleep(Duration::from_secs(1)).await; + } + + // Clear the line and prepare for new output + println!("\rScanning now... "); + + // Don't need the original sleep anymore since we're using the countdown timer + // time::sleep(interval).await; + } + } + + // Keep this method for backward compatibility + pub async fn get_user_tweets(&self, username: &str, max_results: u32) -> Result> { + // For now, just redirect to home timeline + println!("Note: Ignoring username '{}', fetching home timeline instead", username); + self.get_home_timeline(max_results).await + } + + // Keep this method for backward compatibility + pub async fn monitor_user( + &self, + username: &str, + callback: impl Fn(Vec) -> Result<()>, + interval: Duration, + ) -> Result<()> { + // Just redirect to home timeline + println!("Note: Ignoring username '{}', monitoring home timeline instead", username); + self.monitor_home_timeline(10, callback, interval).await + } +} + +fn safe_truncate(s: &str, max_chars: usize) -> String { + if s.chars().count() <= max_chars { + return s.to_string(); + } + + let mut result = String::new(); + let mut char_count = 0; + + for c in s.chars() { + if char_count >= max_chars { + break; + } + result.push(c); + char_count += 1; + } + + result + "..." +} + +fn save_response_to_file(response: &str, filename: &str) -> Result<()> { + let path = Path::new(filename); + let mut file = File::create(path)?; + file.write_all(response.as_bytes())?; + Ok(()) +} + +fn extract_cursor_from_url(url: &str) -> Option { + if let Some(query_part) = url.split('?').nth(1) { + // Extract the variables parameter + for param in query_part.split('&') { + if param.starts_with("variables=") { + let variables_str = param.split('=').nth(1)?; + + // URL decode the variables string + let decoded = urlencoding::decode(variables_str).ok()?; + + // Parse the JSON + if let Ok(variables) = serde_json::from_str::(&decoded) { + // Extract the cursor value + let cursor = variables + .get("cursor") + .and_then(|c| c.as_str()) + .map(|s| s.to_string()); + + if cursor.is_some() { + println!("Found cursor in URL variables: {}", cursor.as_ref().unwrap()); + return cursor; + } + } else { + println!("Failed to parse variables JSON: {}", decoded); + } + } + } + } + + None +} + +fn parse_twitter_url_params(url: &str) -> Option<(Option, Option)> { + let mut variables = None; + let mut features = None; + + if let Some(query_part) = url.split('?').nth(1) { + // Process each query parameter + for param in query_part.split('&') { + if param.starts_with("variables=") { + if let Some(vars_str) = param.split('=').nth(1) { + if let Ok(decoded) = urlencoding::decode(vars_str) { + if let Ok(json) = serde_json::from_str::(&decoded) { + variables = Some(json); + } + } + } + } else if param.starts_with("features=") { + if let Some(feat_str) = param.split('=').nth(1) { + if let Ok(decoded) = urlencoding::decode(feat_str) { + if let Ok(json) = serde_json::from_str::(&decoded) { + features = Some(json); + } + } + } + } + } + } + + if variables.is_some() || features.is_some() { + Some((variables, features)) + } else { + None + } +} \ No newline at end of file diff --git a/src/twitter/curl.rs b/src/twitter/curl.rs new file mode 100644 index 0000000..a49ab96 --- /dev/null +++ b/src/twitter/curl.rs @@ -0,0 +1,181 @@ +use anyhow::{anyhow, Result}; +use regex::Regex; +use std::collections::HashMap; +use reqwest::header; +use serde_json; +use urlencoding; + +#[derive(Debug, Clone)] +pub struct CurlAuth { + pub url: String, + pub headers: header::HeaderMap, + pub data: Option, + pub url_parameters: Option, +} + +impl CurlAuth { + pub fn from_curl(curl_command: &str) -> Result { + println!("Parsing curl command..."); + + // Extract the URL using regex + let url_regex = Regex::new(r"curl\s+'([^']+)'")?; + let url = match url_regex.captures(curl_command) { + Some(captures) => { + match captures.get(1) { + Some(url_match) => url_match.as_str().to_string(), + None => return Err(anyhow!("Could not extract URL from regex match")) + } + }, + None => return Err(anyhow!("Could not find URL in curl command: {}", curl_command)) + }; + + println!("URL: {}", url); + + // Create a new HeaderMap + let mut headers = header::HeaderMap::new(); + + // Extract headers using regex + let header_regex = Regex::new(r"-H\s+'([^']+)'")?; + println!("Extracting headers..."); + for cap in header_regex.captures_iter(curl_command) { + if let Some(header) = cap.get(1) { + let header_str = header.as_str(); + if let Some((key, value)) = header_str.split_once(':') { + let key = key.trim(); + let value = value.trim(); + + println!("Header: {} = {}", key, value); + + // Parse the header name and value + if let Ok(header_name) = key.parse::() { + if let Ok(header_value) = value.parse::() { + headers.insert(header_name, header_value); + } else { + println!("Warning: Could not parse header value: {}", value); + } + } else { + println!("Warning: Could not parse header name: {}", key); + } + } + } + } + + // Extract cookies using regex and add them to the headers + let cookie_regex = Regex::new(r"-b\s+'([^']+)'")?; + if let Some(captures) = cookie_regex.captures(curl_command) { + if let Some(cookies_str) = captures.get(1) { + println!("Found cookies: {}", cookies_str.as_str()); + if let Ok(cookie_value) = cookies_str.as_str().parse::() { + headers.insert(header::COOKIE, cookie_value); + } else { + println!("Warning: Could not parse cookie value"); + } + } + } + + // Extract data using regex - try different formats + let mut data = None; + + // First try --data-raw + let data_raw_regex = Regex::new(r"--data-raw\s+'([^']+)'")?; + if let Some(captures) = data_raw_regex.captures(curl_command) { + if let Some(data_match) = captures.get(1) { + data = Some(data_match.as_str().to_string()); + println!("Found data-raw: {}", data.as_ref().unwrap()); + } + } + + // If not found, try --data + if data.is_none() { + let data_regex = Regex::new(r"--data\s+'([^']+)'")?; + if let Some(captures) = data_regex.captures(curl_command) { + if let Some(data_match) = captures.get(1) { + data = Some(data_match.as_str().to_string()); + println!("Found data: {}", data.as_ref().unwrap()); + } + } + } + + // If not found, try -d + if data.is_none() { + let d_regex = Regex::new(r"-d\s+'([^']+)'")?; + if let Some(captures) = d_regex.captures(curl_command) { + if let Some(data_match) = captures.get(1) { + data = Some(data_match.as_str().to_string()); + println!("Found -d: {}", data.as_ref().unwrap()); + } + } + } + + // Extract URL parameters if present + let url_parameters = if url.contains('?') { + extract_url_parameters(&url) + } else { + None + }; + + // If we have URL parameters but no data, create data from parameters + if data.is_none() && url_parameters.is_some() { + let params = url_parameters.as_ref().unwrap(); + println!("Creating data from URL parameters"); + data = Some(serde_json::to_string(params).unwrap_or_default()); + println!("Created data: {}", data.as_ref().unwrap()); + } + + // Check for essential auth headers/cookies + if !headers.contains_key(header::COOKIE) { + println!("Warning: Missing cookie header"); + } + + println!("CurlAuth created with URL and {} headers", headers.len()); + println!("=== URL ==="); + println!("{}", url); + println!("=== Headers ==="); + for (name, value) in headers.iter() { + println!(" {}: {}", name, value.to_str().unwrap_or("[binary value]")); + } + println!("=== Data ==="); + if let Some(data_str) = &data { + println!("{}", data_str); + } else { + println!(" [No data]"); + } + println!("============"); + + Ok(Self { + url, + headers, + data, + url_parameters, + }) + } +} + +fn extract_url_parameters(url: &str) -> Option { + if let Some(query_part) = url.split('?').nth(1) { + let mut result = serde_json::Map::new(); + + // Process each query parameter + for param in query_part.split('&') { + let parts: Vec<&str> = param.split('=').collect(); + if parts.len() == 2 { + let key = parts[0]; + let value = parts[1]; + + if key == "variables" || key == "features" { + if let Ok(decoded) = urlencoding::decode(value) { + if let Ok(json) = serde_json::from_str::(&decoded) { + result.insert(key.to_string(), json); + } + } + } + } + } + + if !result.is_empty() { + return Some(serde_json::Value::Object(result)); + } + } + + None +} \ No newline at end of file diff --git a/src/twitter/mod.rs b/src/twitter/mod.rs new file mode 100644 index 0000000..453d43d --- /dev/null +++ b/src/twitter/mod.rs @@ -0,0 +1,5 @@ +pub mod client; +pub mod curl; + +pub use client::TwitterClient; +pub use curl::CurlAuth;