diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d71b85e --- /dev/null +++ b/.env.example @@ -0,0 +1,36 @@ +# Environment Configuration +ENVIRONMENT=development # or production + +# Server Configuration +SERVER_HOST=127.0.0.1 +SERVER_PORT=7878 +WEBHOOK_PATH=/webhook + +# Rate Limiting Configuration +RATE_LIMIT_MAX_REQUESTS=100 +RATE_LIMIT_WINDOW_SECONDS=3600 + +# Retry Configuration +RETRY_MAX_ATTEMPTS=3 +RETRY_INITIAL_DELAY_MS=1000 +RETRY_MAX_DELAY_MS=5000 + +# Timeout Configuration (in seconds) +TIMEOUT_CONNECT_SECONDS=10 +TIMEOUT_READ_SECONDS=30 +TIMEOUT_WRITE_SECONDS=30 + +# GitHub Configuration +GITHUB_TOKEN=ghp_your_github_personal_access_token_here +REPO_OWNER=repository_owner_or_organization +REPO_NAME=repository_name + +# X (Twitter) Configuration +# Get these from the X Developer Portal (https://developer.twitter.com/en/portal/dashboard) +X_API_KEY=your_x_api_key_here +X_API_SECRET=your_x_api_secret_here +X_ACCESS_TOKEN=your_x_access_token_here +X_ACCESS_SECRET=your_x_access_token_secret_here + +# Logging Configuration +LOG_LEVEL=debug # error, warn, info, debug, or trace diff --git a/.gitignore b/.gitignore index 6c82d88..b5fbf6c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,12 @@ +# Environment files +.env +.env.* +!.env.example + # Rust /target **/*.rs.bk +Cargo.lock # JetBrains .idea/ @@ -11,8 +17,19 @@ # VSCode .vscode/ -# macOS +# IDE +*.swp +*.swo + +# Logs +*.log +npm-debug.log* + +# OS .DS_Store +Thumbs.db + +# macOS .AppleDouble .LSOverride diff --git a/Cargo.lock b/Cargo.lock index 97021b6..c35cd6a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,5 +3,2771 @@ version = 4 [[package]] -name = "x-bot" +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 = "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 = "anyhow" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "async-trait" +version = "0.1.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.5.1", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 1.0.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[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.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[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.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" + +[[package]] +name = "cc" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9157bbaa6b165880c27a4293a474c91cdcf265cc68cc829bf10be0964a391caf" +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.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets 0.52.6", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +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.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +dependencies = [ + "libc", +] + +[[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 = "crypto-mac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25fab6889090c8133f3deb8f73ba3c65a7f456f66436fc012a1b1e272b1e103e" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "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 2.0.90", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +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 = "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 2.0.90", +] + +[[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", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[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 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hmac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +dependencies = [ + "crypto-mac", + "digest 0.9.0", +] + +[[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" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +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 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.2.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +dependencies = [ + "futures-util", + "http 1.2.0", + "hyper 1.5.1", + "hyper-util", + "log", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper 1.5.1", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper 0.14.31", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "hyper 1.5.1", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[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 2.0.90", +] + +[[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 = "indexmap" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + +[[package]] +name = "iri-string" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0f0a572e8ffe56e2ff4f769f32ffe919282c3916799f8b68688b6030063bea" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "js-sys" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "9.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" +dependencies = [ + "base64 0.21.7", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.168" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework 2.11.1", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "oauth-credentials" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6f9dee4cc95bd06fe0c1225a2b5680e0611627e1643ce78eb76daddd8dbccc" + +[[package]] +name = "oauth1-request" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d98aea71f24f4d18cd58372672a640654fa657eddbd6b54ffd1007e25e8aee3" +dependencies = [ + "base64 0.13.1", + "bitflags 1.3.2", + "cfg-if", + "hmac", + "oauth-credentials", + "oauth1-request-derive", + "percent-encoding", + "rand", + "sha-1", +] + +[[package]] +name = "oauth1-request-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58659ed77836b3a2ec0b7d5e894d9b18880a45c3898be6332baf90daa0f06afb" +dependencies = [ + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "oauth2" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c38841cdd844847e3e7c8d29cef9dcfed8877f8f56f9071f77843ecf3baf937f" +dependencies = [ + "base64 0.13.1", + "chrono", + "getrandom", + "http 0.2.12", + "rand", + "reqwest", + "serde", + "serde_json", + "serde_path_to_error", + "sha2", + "thiserror", + "url", +] + +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + +[[package]] +name = "octocrab" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b97f949a7cb04608441c2ddb28e15a377e8b5142c2d1835ad2686d434de8558" +dependencies = [ + "arc-swap", + "async-trait", + "base64 0.22.1", + "bytes", + "cfg-if", + "chrono", + "either", + "futures", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.5.1", + "hyper-rustls", + "hyper-timeout", + "hyper-util", + "jsonwebtoken", + "once_cell", + "percent-encoding", + "pin-project", + "secrecy", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "snafu", + "tokio", + "tower", + "tower-http", + "tracing", + "url", + "web-time", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl" +version = "0.10.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64 0.22.1", + "serde", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" + +[[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.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +dependencies = [ + "bitflags 2.6.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 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[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 0.8.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[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 0.2.12", + "http-body 0.4.6", + "hyper 0.14.31", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 0.1.2", + "system-configuration", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "winreg", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustix" +version = "0.38.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.23.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.0.1", +] + +[[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-pki-types" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "schannel" +version = "0.1.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 = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "zeroize", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.6.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1415a607e92bec364ea2cf9264646dcce0f91e6d65281bd6f2819cca3bf39c8" +dependencies = [ + "bitflags 2.6.0", + "core-foundation 0.10.0", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.216" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.216" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "serde_json" +version = "1.0.133" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "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 = "sha-1" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[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 = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "snafu" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "223891c85e2a29c3fe8fb900c1fae5e69c2e42415e3177752e8718475efa5019" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c3c6b7927ffe7ecaa769ee0e3994da3b8cafc8f444578982c83ecb161af917" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 1.0.109", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +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 = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[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 0.9.4", + "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.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[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 2.0.90", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +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.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" +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.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 1.0.2", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" +dependencies = [ + "bitflags 2.6.0", + "bytes", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "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 2.0.90", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "twitter-v2" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a7610e778b6f3a156fa8c766a70b243dc06d253901a6e4d5b4c9b81d1486d64" +dependencies = [ + "async-trait", + "futures", + "oauth1-request", + "oauth2", + "percent-encoding", + "pin-project-lite", + "reqwest", + "serde", + "serde_json", + "serde_urlencoded", + "strum", + "thiserror", + "time", + "tokio", + "url", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[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", + "serde", +] + +[[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 = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.90", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-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.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[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 = "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 = "x_bot" version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "chrono", + "dotenv", + "octocrab", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", + "twitter-v2", +] + +[[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 2.0.90", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[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 2.0.90", +] diff --git a/Cargo.toml b/Cargo.toml index 1c96eb7..b919d2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,19 @@ [package] -name = "x-bot" +name = "x_bot" version = "0.1.0" edition = "2021" +authors = ["dmbtechdev "] +description = "A bot that posts the Delta repository updates to X" [dependencies] +tokio = { version = "1.42.0", features = ["full"] } +axum = "0.7.9" +serde = { version = "1.0.216", features = ["derive"] } +serde_json = "1.0.133" +tracing = "0.1.41" +tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } +dotenv = "0.15.0" +anyhow = "1.0.94" +octocrab = "0.42.1" +twitter-v2 = "0.1.8" +chrono = { version = "0.4.39", features = ["serde"] } \ No newline at end of file diff --git a/README.md b/README.md index b863fb8..e7dcf55 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ We need an X bot that posts updates to `@deltaml_org` with information from GitH ### We need (for now) two types of posts: #### 1. For new contributors: + ``` Delta got a new contributor [Contributor Name]! @@ -35,10 +36,59 @@ New release ([Version Number]) of Delta out! 🎉 Link to release notes: [Release link] ``` -Implement these features in the bot, ensuring the messages are posted automatically whenever these events occur. +Implement these features in the bot, ensuring the messages are posted automatically whenever these events occur. + +--- + + + +## Setup + +1. Clone the repository +2. Set up environment variables: + - `GITHUB_TOKEN`: GitHub API token + - `X_API_KEY`: X (Twitter) API key + - `X_API_SECRET`: X (Twitter) API secret + - `X_ACCESS_TOKEN`: X (Twitter) access token + - `X_ACCESS_SECRET`: X (Twitter) access token secret + - `REPO_OWNER`: The owner of the GitHub repository + - `REPO_NAME`: The name of the GitHub repository + +## Github Token Setup + +1. Go to your GitHub account settings +2. Navigate to Developer settings > Personal access tokens +3. Generate a new token (classic) with the `repo` and `admin:repo_hook` scopes + +## Github Repo Webhook Setup + +1. Go to your GitHub repository settings +2. Navigate to Webhooks > Add webhook +3. Enter the webhook URL from the environment variable `WEBHOOK_PATH` +4. Select the events, "Push" and "Releases" you want to trigger the webhook + +## Webhooks + +* **Definition** : Webhooks are HTTP callbacks that allow one application to send real-time data to another whenever a specific event occurs. +* **Communication** : They use a request-response model. When an event occurs, the source application makes an HTTP POST request to a predefined URL (the webhook endpoint) on the target application, sending data about the event. +* **Use Case** : Commonly used for event-driven architectures where you want to notify another service about events, such as GitHub sending a notification about a new commit or release. +* **Example** : When a new issue is created in a GitHub repository, GitHub can send a webhook notification to your application to inform it of the new issue. + +## X api Setup + +Get these from the X Developer Portal (https://developer.twitter.com/en/portal/dashboard) + +1. Go to your X (Twitter) dashboard +2. Navigate to your API keys +3. Create a new API key + +--- + + If anything is unclear, reach out in the [Github Discussions](https://github.com/orgs/delta-rs/discussions/categories/general) here on GitHub. + ## Contributors The following contributors have either helped to start this project, have contributed diff --git a/src/config/env.rs b/src/config/env.rs new file mode 100644 index 0000000..818a9ec --- /dev/null +++ b/src/config/env.rs @@ -0,0 +1,466 @@ +use std::{ + env::var, + str::FromStr, + fmt::{Display, Formatter}}; +use serde::Deserialize; +use anyhow::Context; + + +/// Runtime environment for the application +#[derive(Debug, Clone, Copy, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Environment { + Development, + Production, +} + +// impl Default for Environment { +// fn default() -> Self { +// Environment::Development +// } +// } + +// impl From for String { +// fn from(environment: Environment) -> String { +// match environment { +// Environment::Development => "development".to_string(), +// Environment::Production => "production".to_string(), +// } +// } +// } + +// convert string from env var file to Environment +impl FromStr for Environment { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + match s.to_lowercase().as_str() { + "development" | "dev" => Ok(Environment::Development), + "production" | "prod" => Ok(Environment::Production), + _ => Err(anyhow::anyhow!("Invalid environment: {}", s)), + } + } +} + +/// Server configuration settings +#[derive(Debug, Deserialize)] +pub struct ServerConfig { + pub host: String, + pub port: u16, + pub webhook_path: String, +} + +// impl Default for ServerConfig { +// fn default() -> Self { +// Self { +// host: "127.0.0.1".to_string(), +// port: 7878, +// webhook_path: "/webhook".to_string(), +// } +// } +// } + +/// Rate limiting configuration +#[derive(Debug, Deserialize)] +pub struct RateLimitConfig { + /// Maximum number of requests per window + pub max_requests: u32, + /// Time window in seconds + pub window_seconds: u64, +} + +// impl Default for RateLimitConfig { +// fn default() -> Self { +// Self { +// max_requests: 100, +// window_seconds: 3600, +// } +// } +// } + +/// Retry configuration for failed operations +#[derive(Debug, Deserialize)] +pub struct RetryConfig { + /// Maximum number of retry attempts + pub max_attempts: u32, + /// Initial delay between retries in milliseconds + pub initial_delay_ms: u64, + /// Maximum delay between retries in milliseconds + pub max_delay_ms: u64, +} + +// impl Default for RetryConfig { +// fn default() -> Self { +// Self { +// max_attempts: 3, +// initial_delay_ms: 1000, +// max_delay_ms: 5000, +// } +// } +// } + +/// Cache configuration +// #[derive(Debug, Deserialize)] +// pub struct CacheConfig { +// /// Enable caching +// pub enabled: bool, +// /// Cache TTL in seconds +// pub ttl_seconds: u64, +// /// Maximum cache size in items +// pub max_items: usize, +// } + +// impl Default for CacheConfig { +// fn default() -> Self { +// Self { +// enabled: true, +// ttl_seconds: 3600, +// max_items: 1000, +// } +// } +// } + +/// API timeout configuration +#[derive(Debug, Deserialize)] +pub struct TimeoutConfig { + /// Connect timeout in seconds + pub connect_seconds: u64, + /// Read timeout in seconds + pub read_seconds: u64, + /// Write timeout in seconds + pub write_seconds: u64, +} + +// impl Default for TimeoutConfig { +// fn default() -> Self { +// Self { +// connect_seconds: 10, +// read_seconds: 30, +// write_seconds: 30, +// } +// } +// } + +/// Sensitive configuration that should never be logged or displayed +#[derive(Debug,Deserialize)] +pub struct Secrets { + /// GitHub personal access token for API authentication + github_token: String, + + /// X API key for API authentication + x_api_key: String, + + /// X API secret for API authentication + x_api_secret: String, + + /// X access token for API authentication + x_access_token: String, + + /// X access secret for API authentication + x_access_secret: String, +} + +impl Display for Secrets { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + write!(f, "[REDACTED]") + } +} + +/// Secret tokens getters in a controlled manner +impl Secrets { + pub fn github_token(&self) -> &str { + &self.github_token + } + + pub fn x_api_key(&self) -> &str { + &self.x_api_key + } + + pub fn x_api_secret(&self) -> &str { + &self.x_api_secret + } + + pub fn x_access_token(&self) -> &str { + &self.x_access_token + } + + pub fn x_access_secret(&self) -> &str { + &self.x_access_secret + } + + /// Validate all secrets + pub fn validate(&self) -> anyhow::Result<()> { + if self.github_token.is_empty() { + return Err(anyhow::anyhow!("GITHUB_TOKEN must be set")); + } + if self.github_token.len() != 40 { + return Err(anyhow::anyhow!("GitHub token must be exactly 40 characters long")); + } + if self.x_api_key.is_empty() { + return Err(anyhow::anyhow!("X_API_KEY must be set")); + } + if self.x_api_key.len() < 25 { + return Err(anyhow::anyhow!("X_API_KEY must be at least 32 characters long")); + } + if self.x_api_secret.is_empty() { + return Err(anyhow::anyhow!("X_API_SECRET must be set")); + } + if self.x_api_secret.len() < 32 { + return Err(anyhow::anyhow!("X_API_SECRET must be at least 32 characters long")); + } + if self.x_access_token.is_empty() { + return Err(anyhow::anyhow!("X_ACCESS_TOKEN must be set")); + } + if self.x_access_token.len() < 32 { + return Err(anyhow::anyhow!("X_ACCESS_TOKEN must be at least 32 characters long")); + } + if self.x_access_secret.is_empty() { + return Err(anyhow::anyhow!("X_ACCESS_SECRET must be set")); + } + if self.x_access_secret.len() < 32 { + return Err(anyhow::anyhow!("X_ACCESS_SECRET must be at least 32 characters long")); + } + Ok(()) + } +} + +/// Configuration structure for the application +#[derive(Debug, Deserialize)] +pub struct Config { + /// Current runtime environment + // #[serde(default)] + pub environment: Environment, + + /// Server configuration + // #[serde(default)] + pub server: ServerConfig, + + /// Rate limiting configuration + // #[serde(default)] + pub rate_limit: RateLimitConfig, + + /// Retry configuration + // #[serde(default)] + pub retry: RetryConfig, + + /// API timeout configuration + // #[serde(default)] + pub timeout: TimeoutConfig, + + /// Sensitive configuration values + pub secrets: Secrets, + + /// GitHub repository owner (username or organization) + pub repo_owner: String, + + /// GitHub repository name + pub repo_name: String, + + /// Log level for the application + #[serde(default = "default_log_level")] + pub log_level: String, +} + +fn default_log_level() -> String { + "info".to_string() +} + +impl Config { + /// Loads configuration from environment variables + /// + /// # Returns + /// A Result containing the Config if successful, or an error if any required + /// environment variables are missing + pub fn from_env() -> anyhow::Result { + dotenv::dotenv().ok(); + + // Load environment-specific settings + let environment = var("ENVIRONMENT") + .unwrap_or_else(|_| "development".to_string()) + .parse()?; + + // Load secrets first and validate them + let secrets = Secrets { + github_token: var("GITHUB_TOKEN") + .context("GITHUB_TOKEN must be set")?, + x_api_key: var("X_API_KEY") + .context("X_API_KEY must be set")?, + x_api_secret: var("X_API_SECRET") + .context("X_API_SECRET must be set")?, + x_access_token: var("X_ACCESS_TOKEN") + .context("X_ACCESS_TOKEN must be set")?, + x_access_secret: var("X_ACCESS_SECRET") + .context("X_ACCESS_SECRET must be set")?, + }; + secrets.validate()?; + + // Load server configuration + let server = ServerConfig { + host: var("SERVER_HOST") + .unwrap_or_else(|_| "127.0.0.1".to_string()), + port: var("SERVER_PORT") + .unwrap_or_else(|_| "7878".to_string()) + .parse() + .context("SERVER_PORT must be a valid port number")?, + webhook_path: var("WEBHOOK_PATH") + .unwrap_or_else(|_| "/webhook".to_string()), + }; + + // Load rate limit configuration + let rate_limit = RateLimitConfig { + max_requests: var("RATE_LIMIT_MAX_REQUESTS") + .unwrap_or_else(|_| "100".to_string()) + .parse() + .context("RATE_LIMIT_MAX_REQUESTS must be a positive integer")?, + window_seconds: var("RATE_LIMIT_WINDOW_SECONDS") + .unwrap_or_else(|_| "3600".to_string()) + .parse() + .context("RATE_LIMIT_WINDOW_SECONDS must be a positive integer")?, + }; + + // Load retry configuration + let retry = RetryConfig { + max_attempts: var("RETRY_MAX_ATTEMPTS") + .unwrap_or_else(|_| "3".to_string()) + .parse() + .context("RETRY_MAX_ATTEMPTS must be a positive integer")?, + initial_delay_ms: var("RETRY_INITIAL_DELAY_MS") + .unwrap_or_else(|_| "1000".to_string()) + .parse() + .context("RETRY_INITIAL_DELAY_MS must be a positive integer")?, + max_delay_ms: var("RETRY_MAX_DELAY_MS") + .unwrap_or_else(|_| "5000".to_string()) + .parse() + .context("RETRY_MAX_DELAY_MS must be a positive integer")?, + }; + + // Load timeout configuration + let timeout = TimeoutConfig { + connect_seconds: var("TIMEOUT_CONNECT_SECONDS") + .unwrap_or_else(|_| "10".to_string()) + .parse() + .context("TIMEOUT_CONNECT_SECONDS must be a positive integer")?, + read_seconds: var("TIMEOUT_READ_SECONDS") + .unwrap_or_else(|_| "30".to_string()) + .parse() + .context("TIMEOUT_READ_SECONDS must be a positive integer")?, + write_seconds: var("TIMEOUT_WRITE_SECONDS") + .unwrap_or_else(|_| "30".to_string()) + .parse() + .context("TIMEOUT_WRITE_SECONDS must be a positive integer")?, + }; + + let config = Config { + environment, + server, + rate_limit, + retry, + timeout, + secrets, + repo_owner: var("REPO_OWNER") + .context("REPO_OWNER must be set")?, + repo_name: var("REPO_NAME") + .context("REPO_NAME must be set")?, + log_level: var("LOG_LEVEL") + .unwrap_or_else(|_| default_log_level()), + }; + + config.validate()?; + Ok(config) + } + + /// Get secrets safely inside Config + pub fn github_token(&self) -> &str { + self.secrets.github_token() + } + + pub fn x_api_key(&self) -> &str { + self.secrets.x_api_key() + } + + pub fn x_api_secret(&self) -> &str { + self.secrets.x_api_secret() + } + + pub fn x_access_token(&self) -> &str { + self.secrets.x_access_token() + } + + pub fn x_access_secret(&self) -> &str { + self.secrets.x_access_secret() + } + + /// Validates the configuration values + fn validate(&self) -> anyhow::Result<()> { + if self.repo_owner.is_empty() || self.repo_name.is_empty() { + return Err(anyhow::anyhow!("Repository owner and name cannot be empty")); + } + + match self.log_level.to_lowercase().as_str() { + "error" | "warn" | "info" | "debug" | "trace" => Ok(()), + _ => Err(anyhow::anyhow!("Invalid log level: {}", self.log_level)), + }?; + + // Validate rate limit configuration + if self.rate_limit.max_requests == 0 { + return Err(anyhow::anyhow!("Rate limit max requests must be greater than 0")); + } + if self.rate_limit.window_seconds == 0 { + return Err(anyhow::anyhow!("Rate limit window seconds must be greater than 0")); + } + + // Validate retry configuration + if self.retry.max_attempts == 0 { + return Err(anyhow::anyhow!("Retry max attempts must be greater than 0")); + } + if self.retry.initial_delay_ms == 0 { + return Err(anyhow::anyhow!("Retry initial delay must be greater than 0")); + } + if self.retry.max_delay_ms < self.retry.initial_delay_ms { + return Err(anyhow::anyhow!("Retry max delay must be greater than or equal to initial delay")); + } + + // Validate timeout configuration + if self.timeout.connect_seconds == 0 { + return Err(anyhow::anyhow!("Connect timeout must be greater than 0")); + } + if self.timeout.read_seconds == 0 { + return Err(anyhow::anyhow!("Read timeout must be greater than 0")); + } + if self.timeout.write_seconds == 0 { + return Err(anyhow::anyhow!("Write timeout must be greater than 0")); + } + + Ok(()) + } + + // /// Returns true if running in development mode + // pub fn is_development(&self) -> bool { + // matches!(self.environment, Environment::Development) + // } + + // /// Returns true if running in production mode + // pub fn is_production(&self) -> bool { + // matches!(self.environment, Environment::Production) + // } + + // /// Get connect timeout as Duration + // pub fn connect_timeout(&self) -> Duration { + // Duration::from_secs(self.timeout.connect_seconds) + // } + + // /// Get read timeout as Duration + // pub fn read_timeout(&self) -> Duration { + // Duration::from_secs(self.timeout.read_seconds) + // } + + // /// Get write timeout as Duration + // pub fn write_timeout(&self) -> Duration { + // Duration::from_secs(self.timeout.write_seconds) + // } + + /// Get the full webhook URL path + pub fn webhook_url(&self) -> String { + format!("http://{}{}",self.server.host, self.server.webhook_path) + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..dea962f --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1 @@ +pub mod env; \ No newline at end of file diff --git a/src/github/client.rs b/src/github/client.rs new file mode 100644 index 0000000..ffbc399 --- /dev/null +++ b/src/github/client.rs @@ -0,0 +1,66 @@ +use super::contributor::{ContributorManager, ContributorInfo}; +use octocrab::Octocrab; +use anyhow::Result; +use tracing::info; + +pub struct GitHubClient { + client: Octocrab, + repo_owner: String, + repo_name: String, + contributor_manager: ContributorManager, +} + +impl GitHubClient { + /// Creates a new instance of `GitHubClient` with the specified token and repository details. + /// + /// # Arguments + /// * `token` - A string containing the personal access token for GitHub API authentication. + /// * `repo_owner` - A string containing the owner of the repository. + /// * `repo_name` - A string containing the name of the repository. + /// + /// # Returns + /// A result containing the initialized `GitHubClient` or an error if initialization fails. + pub async fn new(token: String, repo_owner: String, repo_name: String) -> Result { + let client = Octocrab::builder() + .personal_token(token) + .build()?; + + let contributor_manager = ContributorManager::new( + client.clone(), + repo_owner.clone(), + repo_name.clone(), + 300, // 5 minutes cache TTL + ); + + info!("Github Api Client initialized"); + + Ok(Self { + client, + repo_owner, + repo_name, + contributor_manager, + }) + } + + /// Checks if the specified user is making their first contribution to the repository. + /// + /// # Arguments + /// * `username` - A string slice containing the username of the contributor. + /// + /// # Returns + /// A result containing `true` if this is the user's first contribution, or `false` otherwise. + pub async fn is_first_contribution(&self, username: &str) -> Result { + self.contributor_manager.is_first_contribution(username).await + } + + /// Gets detailed information about a contributor. + /// + /// # Arguments + /// * `username` - A string slice containing the username of the contributor. + /// + /// # Returns + /// A result containing an optional `ContributorInfo` if the contributor exists. + pub async fn get_contributor_info(&self, username: &str) -> Result> { + self.contributor_manager.get_contributor_info(username).await + } +} diff --git a/src/github/contributor.rs b/src/github/contributor.rs new file mode 100644 index 0000000..807bb2e --- /dev/null +++ b/src/github/contributor.rs @@ -0,0 +1,132 @@ +use std::{collections::HashMap,sync::Arc}; +use tokio::sync::RwLock; +use anyhow::Result; +use tracing::info; +use chrono::{DateTime, Utc}; + +/// Represents a contributor's information +#[derive(Debug, Clone)] +pub struct ContributorInfo { + pub username: String, + pub total_commits: usize, + pub first_contribution_date: DateTime, + pub latest_contribution_date: DateTime, +} + +/// Manages contributor information with caching +pub struct ContributorManager { + client: octocrab::Octocrab, + repo_owner: String, + repo_name: String, + + // Cache of contributor information + // The HashMap structure is used here because: + // Fast Lookups: We need O(1) lookups in is_first_contribution and get_contributor_info methods to quickly check contributor status + // Unique Keys: Each GitHub username (the key) maps to exactly one ContributorInfo struct + // Efficient Updates: During cache refresh, we can quickly update existing contributor information + contributors_cache: Arc>>, + // Cache TTL in seconds + cache_ttl: u64, + // Last cache refresh timestamp + last_refresh: Arc>>, +} + +impl ContributorManager { + /// Creates a new ContributorManager + pub fn new( + client: octocrab::Octocrab, + repo_owner: String, + repo_name: String, + cache_ttl: u64, + ) -> Self { + Self { + client, + repo_owner, + repo_name, + contributors_cache: Arc::new(RwLock::new(HashMap::new())), + cache_ttl, + last_refresh: Arc::new(RwLock::new(Utc::now())), + } + } + + /// Checks if a user is making their first contribution + pub async fn is_first_contribution(&self, username: &str) -> Result { + self.refresh_cache_if_needed().await?; + + let cache = self.contributors_cache.read().await; + Ok(!cache.contains_key(username)) + } + + /// Gets detailed information about a contributor + pub async fn get_contributor_info(&self, username: &str) -> Result> { + self.refresh_cache_if_needed().await?; + + let cache = self.contributors_cache.read().await; + Ok(cache.get(username).cloned()) + } + + /// Refreshes the cache if it's expired + async fn refresh_cache_if_needed(&self) -> Result<()> { + let now = Utc::now(); + let last_refresh = *self.last_refresh.read().await; + + if (now - last_refresh).num_seconds() as u64 > self.cache_ttl { + self.refresh_cache().await?; + } + + Ok(()) + } + + /// Refreshes the contributor cache + async fn refresh_cache(&self) -> Result<()> { + info!("Refreshing contributor cache for {}/{}", self.repo_owner, self.repo_name); + + let mut cache = self.contributors_cache.write().await; + let mut new_cache: HashMap = HashMap::new(); + + // Get all commits + let commits = self.client + .repos(&self.repo_owner, &self.repo_name) + .list_commits() + .per_page(100) // Maximum allowed per page + .send() + .await?; + + for commit in commits.items { + if let Some(author) = commit.author { + let username = author.login; + // Safely access the commit date through the commit author + if let Some(commit_author) = &commit.commit.author { + if let Some(date) = commit_author.date { + match new_cache.get_mut(&username) { + Some(info) => { + info.total_commits += 1; + if date < info.first_contribution_date { + info.first_contribution_date = date; + } + if date > info.latest_contribution_date { + info.latest_contribution_date = date; + } + } + None => { + new_cache.insert(username.clone(), ContributorInfo { + username, + total_commits: 1, + first_contribution_date: date, + latest_contribution_date: date, + }); + } + } + } + } + } + } + + // Update the cache + *cache = new_cache; + *self.last_refresh.write().await = Utc::now(); + + info!("Successfully refreshed contributor cache with {} contributors", cache.len()); + Ok(()) + } +} diff --git a/src/github/mod.rs b/src/github/mod.rs new file mode 100644 index 0000000..985a78e --- /dev/null +++ b/src/github/mod.rs @@ -0,0 +1,3 @@ +pub mod client; +pub mod contributor; +pub mod types; \ No newline at end of file diff --git a/src/github/types.rs b/src/github/types.rs new file mode 100644 index 0000000..b1e07ba --- /dev/null +++ b/src/github/types.rs @@ -0,0 +1,75 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum WebhookEvent { + Push(PushEvent), + Release(ReleaseEvent), + Ping(PingEvent), +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct PingEvent { + pub zen: String, + pub hook_id: u64, + pub hook: WebhookInfo, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct WebhookInfo { + pub url: String, + pub test_url: String, + pub ping_url: String, + pub id: u64, + pub active: bool, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct PushEvent { + #[serde(rename = "ref")] + pub git_ref: String, + pub commits: Vec, + pub repository: Repository, + pub sender: GitHubUser, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Commit { + pub id: String, + pub message: String, + pub author: CommitAuthor, + pub url: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct CommitAuthor { + pub name: String, + pub email: String, + pub username: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Repository { + pub full_name: String, + pub owner: GitHubUser, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct GitHubUser { + pub login: String, + pub id: u64, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct ReleaseEvent { + pub action: String, + pub release: Release, + pub repository: Repository, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Release { + pub tag_name: String, + pub name: Option, + pub html_url: String, +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..79eb38f --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,4 @@ +pub mod config; +pub mod github; +pub mod webhook; +pub mod x; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index e7a11a9..7848350 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,93 @@ -fn main() { - println!("Hello, world!"); -} +use x_bot::{ + config::env::Config, + github::client::GitHubClient, + webhook::handler::{ + WebhookHandler, + AppState, + handle_webhook, + health_check, + call_back}, + x::client::XClient}; +use std::sync::Arc; +use tokio::net::TcpListener; +use axum::{ + Router, + routing::{post, get}}; +use anyhow::Result; +use tracing::{info, debug}; +use tracing_subscriber::{ + layer::SubscriberExt, + util::SubscriberInitExt}; + +#[tokio::main] +async fn main() -> Result<()> { + + // Clear the terminal + std::process::Command::new("clear").status().unwrap();println!("\n"); + + // Load configuration + let config = Config::from_env()?; + + tracing_subscriber::registry() + .with(tracing_subscriber::EnvFilter::new( + std::env::var("LOG_LEVEL").unwrap_or_else(|_| "debug".to_string()) + )) + .with(tracing_subscriber::fmt::layer().with_target(true)) + .init(); + + info!("Starting X Bot with log level: {}", config.log_level); + debug!("Debug logging is enabled"); + + // Use rate limiting + println!("Rate limit: {} requests per {} seconds", + config.rate_limit.max_requests, + config.rate_limit.window_seconds); + + // Use retry configuration + println!("Retrying up to {} times", config.retry.max_attempts); + + // Get webhook URL + println!("Webhook URL: {}", config.webhook_url()); + + // Initialize GitHub client + let github_client = GitHubClient::new( + config.secrets.github_token().to_owned(), + config.repo_owner.clone(), + config.repo_name.clone() + ).await?; + + // Initialize X client + let x_client = Arc::new(XClient::new( + config.secrets.x_api_key().to_owned(), + config.secrets.x_api_secret().to_owned(), + config.secrets.x_access_token().to_owned(), + config.secrets.x_access_secret().to_owned() + ).await?); + + // Create webhook handler + let webhook_handler = WebhookHandler::new( + github_client, + Arc::clone(&x_client), + ); + + // Create app state + let state = Arc::new(AppState { + webhook_handler, + }); + + // Build router + let app = Router::new() + .route("/webhook", post(handle_webhook)) + .route("/health", get(health_check)) + .route("/callback", get(call_back)) + .with_state(state); + + // Start server + let addr = format!("{}:{}", config.server.host, config.server.port); + info!("Listening on {}", addr); + + let listener = TcpListener::bind(&addr).await?; + axum::serve(listener, app).await?; + + Ok(()) +} \ No newline at end of file diff --git a/src/webhook/handler.rs b/src/webhook/handler.rs new file mode 100644 index 0000000..1bef8a4 --- /dev/null +++ b/src/webhook/handler.rs @@ -0,0 +1,228 @@ +use crate::{ + github::{ + client::GitHubClient, + types::{ + PingEvent, + PushEvent, + ReleaseEvent}}, + x::client::XClient}; +use std::sync::Arc; +use axum::{ + http::{StatusCode, HeaderMap}, + extract::State}; +use anyhow::Result; +use tracing::{debug, error, info, warn}; + +/// A handler for incoming webhook events from GitHub. +pub struct WebhookHandler { + github_client: GitHubClient, + x_client: Arc, +} + +impl WebhookHandler { + /// Creates a new instance of [WebhookHandler](WebhookHandler). + /// + /// # Arguments + /// * `github_client` - An instance of `GitHubClient` for interacting with the GitHub API. + /// * `x_client` - An Arc wrapped instance of [XClient](XClient) for thread-safe posting to Twitter. + /// + /// # Returns + /// An instance of [WebhookHandler](WebhookHandler). + pub fn new(github_client: GitHubClient, x_client: Arc) -> Self { + Self { + github_client, + x_client, + } + } + + /// Handles push events from GitHub. + /// + /// # Arguments + /// * `event` - A `PushEvent` containing the details of the push event. + /// + /// # Returns + /// A result indicating success or failure. + /// Key Features + /// Event Filtering: + /// The method only processes pushes to the master or main branches. If the push is to a different branch, it returns early with Ok(()). + /// Iterating Over Commits: + /// It iterates through the commits in the push event, checking each commit for the author's username. + /// First Contribution Check: + /// For each commit, it checks if the author is making their first contribution using self.github_client.is_first_contribution(&username).await?. + /// Tweet Formatting: + /// Constructs a tweet message that includes the contributor's username, commit message, and a link to the commit. + /// Posting to X (Twitter): + /// Uses the self.x_client.post_with_retry(&tweet).await? method to post the tweet to X. + /// Logging: + /// Logs the tweet message before posting it. + pub async fn handle_push(&self, event: PushEvent) -> Result<()> { + debug!("Handling push event for ref: {}", event.git_ref); + + // Only handle pushes to master/main branch + if !event.git_ref.ends_with("/main") && !event.git_ref.ends_with("/master") { + debug!("Ignoring push to non-main branch: {}", event.git_ref); + return Ok(()); + } + + info!("Processing push to main branch with {} commits", event.commits.len()); + let repo_owner = &event.repository.owner.login; + + for commit in event.commits { + if let Some(username) = &commit.author.username { + // Skip if the committer is the repo owner + if username == repo_owner { + debug!("Skipping commit from repository owner: {}", username); + continue; + } + + debug!("Checking if {} is a first-time contributor", username); + + if self.github_client.is_first_contribution(username).await? { + info!("Found first-time contributor: {}", username); + + let tweet = format!( + "Delta got a new contributor {}!\nDetails: {}\nLink: {}", + username, + commit.message, + commit.url + ); + + info!("Posting tweet about new contributor: {}", tweet); + match self.x_client.post_with_retry(&tweet).await { + Ok(_) => info!("Successfully posted tweet about new contributor {}", username), + Err(e) => error!("Failed to post tweet about new contributor: {:?}", e), + } + } else { + debug!("Contributor {} has previous contributions", username); + } + } else { + warn!("Commit {} has no associated username", commit.id); + } + } + + Ok(()) + } + + /// Handles release events from GitHub. + /// + /// # Arguments + /// * `event` - A `ReleaseEvent` containing the details of the release event. + /// + /// # Returns + /// A result indicating success or failure. + /// Key Features + /// Event Filtering: + /// The method only processes releases that are marked as "published". If the action is not "published", it returns early with Ok(()). + /// Tweet Formatting: + /// Constructs a tweet message that includes the version tag and a link to the release notes. + /// Posting to X (Twitter): + /// Uses the self.x_client.send_tweet(&tweet).await? method to post the tweet to X. + /// Logging: + /// Logs the tweet message before posting it. + + pub async fn handle_release(&self, event: ReleaseEvent) -> Result<()> { + // Only process published releases + if event.action != "published" { + return Ok(()); + } + + let repo_name = &event.repository.full_name; + let version = &event.release.tag_name; + // let release_name = event.release.name.unwrap_or_else(|| version.clone()); + + let tweet = format!( + "New release ({}) of Delta out! 🎉\nLink to release notes: {}", + version, + event.release.html_url + ); + + info!("Posting new release tweet for {}: {}", repo_name, tweet); + if let Err(e) = self.x_client.send_tweet(&tweet).await { + error!("Failed to post tweet for new release {}: {}", version, e); + } + + Ok(()) + } + +} + +// App state that will be shared across requests +pub struct AppState { + pub webhook_handler: WebhookHandler, +} + + +// Health check endpoint +pub async fn health_check() -> &'static str { + info!("Health check debug message"); + "Health-Check-OK" +} + +pub async fn call_back() -> &'static str { + info!("Call_back debug message"); + "Callback-OK" +} + + +// Webhook handler that uses app state +pub async fn handle_webhook( + State(state): State>, + headers: HeaderMap, + body: String, +) -> Result { + debug!("Received raw webhook body: {}", body); + + // Get the event type from headers + let event_type = headers + .get("x-github-event") + .and_then(|h| h.to_str().ok()) + .ok_or_else(|| { + error!("Missing x-github-event header"); + StatusCode::BAD_REQUEST + })?; + + debug!("GitHub Event Type: {}", event_type); + + // Parse the body based on event type + let result = match event_type { + "ping" => { + debug!("Handling ping event"); + let _ping_event: PingEvent = serde_json::from_str(&body).map_err(|e| { + error!("Failed to parse ping event: {:?}", e); + StatusCode::UNPROCESSABLE_ENTITY + })?; + info!("Received ping event - webhook is configured correctly"); + Ok(StatusCode::OK) + }, + "push" => { + debug!("Handling push event"); + let push_event: PushEvent = serde_json::from_str(&body).map_err(|e| { + error!("Failed to parse push event: {:?}", e); + StatusCode::UNPROCESSABLE_ENTITY + })?; + state.webhook_handler.handle_push(push_event).await.map_err(|e| { + error!("Error handling push event: {:?}", e); + StatusCode::INTERNAL_SERVER_ERROR + })?; + Ok(StatusCode::OK) + }, + "release" => { + debug!("Handling release event"); + let release_event: ReleaseEvent = serde_json::from_str(&body).map_err(|e| { + error!("Failed to parse release event: {:?}", e); + StatusCode::UNPROCESSABLE_ENTITY + })?; + state.webhook_handler.handle_release(release_event).await.map_err(|e| { + error!("Error handling release event: {:?}", e); + StatusCode::INTERNAL_SERVER_ERROR + })?; + Ok(StatusCode::OK) + }, + _ => { + error!("Unsupported event type: {}", event_type); + Err(StatusCode::NOT_IMPLEMENTED) + } + }; + + result +} \ No newline at end of file diff --git a/src/webhook/mod.rs b/src/webhook/mod.rs new file mode 100644 index 0000000..ef7d850 --- /dev/null +++ b/src/webhook/mod.rs @@ -0,0 +1 @@ +pub mod handler; \ No newline at end of file diff --git a/src/x/client.rs b/src/x/client.rs new file mode 100644 index 0000000..7741c43 --- /dev/null +++ b/src/x/client.rs @@ -0,0 +1,120 @@ +use std::sync::{atomic::{AtomicU64, Ordering}, Arc}; +use tokio::time::{sleep, Duration}; +use twitter_v2::{ + authorization::Oauth1aToken, + TwitterApi}; +use anyhow::{Result, anyhow}; +use tracing::{info, warn, error, debug}; +use chrono::Utc; + +const MAX_RETRIES: u32 = 3; +const RATE_LIMIT_WINDOW: u64 = 15 * 60; // 15 minutes in seconds +const TWEETS_PER_WINDOW: u64 = 50; // X API allows 50 tweets per 15 minutes + +pub struct XClient { + client: TwitterApi, + tweet_count: Arc, + window_start: Arc, +} + +impl XClient { + /// Creates a new instance of `XClient` with OAuth 1.0a credentials. + /// + /// # Arguments + /// * `api_key` - The API key (Consumer Key) + /// * `api_secret` - The API secret (Consumer Secret) + /// * `access_token` - The access token + /// * `access_secret` - The access token secret + /// + /// # Returns + /// A result containing the initialized `XClient` or an error if initialization fails. + pub async fn new( + api_key: String, + api_secret: String, + access_token: String, + access_secret: String, + ) -> Result { + let auth = Oauth1aToken::new( + api_key, + api_secret, + access_token, + access_secret, + ); + let client = TwitterApi::new(auth); + + info!("X Api Client initialized"); + + Ok(Self { + client, + tweet_count: Arc::new(AtomicU64::new(0)), + window_start: Arc::new(AtomicU64::new(Utc::now().timestamp() as u64)), + }) + } + + /// Posts a tweet with retry mechanism and rate limiting + pub async fn post_with_retry(&self, text: &str) -> Result { + info!("Attempting to post tweet: {}", text); + + for attempt in 1..=MAX_RETRIES { + match self.send_tweet(text).await { + Ok(id) => { + info!("Successfully posted tweet with ID: {}", id); + return Ok(id); + } + Err(e) => { + error!("Failed to post tweet (attempt {}/{}): {:?}", attempt, MAX_RETRIES, e); + if attempt < MAX_RETRIES { + warn!("Retrying in {} seconds...", attempt * 2); + sleep(Duration::from_secs(attempt as u64 * 2)).await; + } + } + } + } + + Err(anyhow!("Failed to post tweet after {} attempts", MAX_RETRIES)) + } + + /// Posts a tweet with the specified text to Twitter. + /// + /// # Arguments + /// * `text` - A string slice containing the text of the tweet. + /// + /// # Returns + /// A result containing the tweet ID as a string if successful, or an error if the posting fails. + pub async fn send_tweet(&self, text: &str) -> Result { + debug!("Checking rate limits before sending tweet"); + + // Rate limiting check + let now = Utc::now().timestamp() as u64; + let window_start = self.window_start.load(Ordering::Relaxed); + let tweet_count = self.tweet_count.load(Ordering::Relaxed); + + if now - window_start >= RATE_LIMIT_WINDOW { + debug!("Rate limit window expired, resetting counts"); + self.window_start.store(now, Ordering::Relaxed); + self.tweet_count.store(0, Ordering::Relaxed); + } else if tweet_count >= TWEETS_PER_WINDOW { + let wait_time = RATE_LIMIT_WINDOW - (now - window_start); + warn!("Rate limit reached. Waiting {} seconds", wait_time); + sleep(Duration::from_secs(wait_time)).await; + self.window_start.store(now, Ordering::Relaxed); + self.tweet_count.store(0, Ordering::Relaxed); + } + + debug!("Sending tweet to X API"); + match self.client.post_tweet().text(text.to_owned()).send().await { + Ok(response) => { + info!("Tweet posted successfully"); + self.tweet_count.fetch_add(1, Ordering::Relaxed); + match &response.data { + Some(tweet) => Ok(tweet.id.to_string()), + None => Err(anyhow!("No tweet data in response")) + } + } + Err(e) => { + error!("Error from X API: {:?}", e); + Err(anyhow!("Failed to post tweet: {:?}", e)) + } + } + } +} diff --git a/src/x/mod.rs b/src/x/mod.rs new file mode 100644 index 0000000..3935517 --- /dev/null +++ b/src/x/mod.rs @@ -0,0 +1 @@ +pub mod client; \ No newline at end of file