diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 60f004e..328bc9f 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -12,18 +12,8 @@ "containerEnv": { "SHELL": "/bin/bash" }, - "customizations": { - "vscode": { - "extensions": ["rust-lang.rust-analyzer", "tamasfe.even-better-toml", "fill-labs.dependi"], - "settings": { - "[rust]": { - "editor.defaultFormatter": "rust-lang.rust-analyzer", - "editor.formatOnSave": true - } - } - } - }, "workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/herald,type=bind", "workspaceFolder": "/workspaces/herald", + "runArgs": ["--userns=keep-id", "--security-opt", "label=disable"], "appPort": [3000] } diff --git a/Cargo.lock b/Cargo.lock index cc29c98..da87dd3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,28 +14,6 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "aws-lc-rs" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", -] - [[package]] name = "axum" version = "0.8.9" @@ -128,8 +106,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ "find-msvc-tools", - "jobserver", - "libc", "shlex", ] @@ -145,63 +121,18 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" -[[package]] -name = "cmake" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" -dependencies = [ - "cc", -] - [[package]] name = "cmov" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - [[package]] name = "const-oid" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - [[package]] name = "cpufeatures" version = "0.3.0" @@ -229,6 +160,72 @@ dependencies = [ "cmov", ] +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn 2.0.117", +] + [[package]] name = "digest" version = "0.11.3" @@ -249,7 +246,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -259,25 +256,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] -name = "dunce" -version = "1.0.5" +name = "dotenvy_macro" +version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +checksum = "cb0235d912a8c749f4e0c9f18ca253b4c28cfefc1d2518096016d6e3230b6424" dependencies = [ - "cfg-if", + "dotenvy", + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] -name = "equivalent" -version = "1.0.2" +name = "dyn-clone" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "errno" @@ -310,12 +304,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - [[package]] name = "futures-channel" version = "0.3.32" @@ -331,6 +319,23 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "futures-sink" version = "0.3.32" @@ -350,7 +355,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", + "futures-io", + "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "slab", ] @@ -382,31 +391,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "h2" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - [[package]] name = "herald" version = "0.1.0" @@ -415,15 +399,19 @@ dependencies = [ "axum", "bytes", "dotenvy", + "futures-util", "hex", "hmac", + "openrouter-rs", "reqwest", "serde", "serde_json", "sha2", "subtle", - "thiserror", + "thiserror 2.0.18", "tokio", + "tokio-stream", + "tokio-util", ] [[package]] @@ -505,7 +493,6 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "h2", "http", "http-body", "httparse", @@ -530,6 +517,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", + "webpki-roots", ] [[package]] @@ -550,11 +538,9 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2", - "system-configuration", "tokio", "tower-service", "tracing", - "windows-registry", ] [[package]] @@ -639,6 +625,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.1.0" @@ -660,16 +652,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown", -] - [[package]] name = "ipnet" version = "2.12.0" @@ -682,65 +664,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" -[[package]] -name = "jni" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" -dependencies = [ - "cfg-if", - "combine", - "jni-macros", - "jni-sys", - "log", - "simd_cesu8", - "thiserror", - "walkdir", - "windows-link", -] - -[[package]] -name = "jni-macros" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" -dependencies = [ - "proc-macro2", - "quote", - "rustc_version", - "simd_cesu8", - "syn", -] - -[[package]] -name = "jni-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" -dependencies = [ - "jni-sys-macros", -] - -[[package]] -name = "jni-sys-macros" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" -dependencies = [ - "quote", - "syn", -] - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - [[package]] name = "js-sys" version = "0.3.98" @@ -822,10 +745,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] -name = "openssl-probe" -version = "0.2.1" +name = "openrouter-rs" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" +checksum = "7f7101578df2f54d9013594e94367adbe656521ef6002f03e4577f1e00e57cb4" +dependencies = [ + "derive_builder", + "dotenvy_macro", + "futures-util", + "http", + "reqwest", + "schemars", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tokio-util", + "urlencoding", +] [[package]] name = "parking_lot" @@ -903,7 +840,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -915,7 +852,6 @@ version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ - "aws-lc-rs", "bytes", "getrandom 0.3.4", "lru-slab", @@ -925,7 +861,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -999,16 +935,35 @@ dependencies = [ ] [[package]] -name = "reqwest" -version = "0.13.3" +name = "ref-cast" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64", "bytes", - "encoding_rs", "futures-core", - "h2", + "futures-util", "http", "http-body", "http-body-util", @@ -1017,25 +972,27 @@ dependencies = [ "hyper-util", "js-sys", "log", - "mime", "percent-encoding", "pin-project-lite", "quinn", "rustls", "rustls-pki-types", - "rustls-platform-verifier", "serde", "serde_json", + "serde_urlencoded", "sync_wrapper", "tokio", "tokio-rustls", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", + "webpki-roots", ] [[package]] @@ -1058,41 +1015,20 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - [[package]] name = "rustls" version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ - "aws-lc-rs", "once_cell", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", "zeroize", ] -[[package]] -name = "rustls-native-certs" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" -dependencies = [ - "openssl-probe", - "rustls-pki-types", - "schannel", - "security-framework", -] - [[package]] name = "rustls-pki-types" version = "1.14.1" @@ -1103,40 +1039,12 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-platform-verifier" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" -dependencies = [ - "core-foundation 0.10.1", - "core-foundation-sys", - "jni", - "log", - "once_cell", - "rustls", - "rustls-native-certs", - "rustls-platform-verifier-android", - "rustls-webpki", - "security-framework", - "security-framework-sys", - "webpki-root-certs", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls-platform-verifier-android" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" - [[package]] name = "rustls-webpki" version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ - "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -1155,21 +1063,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] -name = "same-file" -version = "1.0.6" +name = "schemars" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ - "winapi-util", + "dyn-clone", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", ] [[package]] -name = "schannel" -version = "0.1.29" +name = "schemars_derive" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" dependencies = [ - "windows-sys 0.61.2", + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", ] [[package]] @@ -1178,35 +1093,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "security-framework" -version = "3.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" -dependencies = [ - "bitflags", - "core-foundation 0.10.1", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - [[package]] name = "serde" version = "1.0.228" @@ -1234,7 +1120,18 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -1300,22 +1197,6 @@ dependencies = [ "libc", ] -[[package]] -name = "simd_cesu8" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" -dependencies = [ - "rustc_version", - "simdutf8", -] - -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - [[package]] name = "slab" version = "0.4.12" @@ -1344,12 +1225,29 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.117" @@ -1378,28 +1276,16 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] -name = "system-configuration" -version = "0.7.0" +name = "thiserror" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "bitflags", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", + "thiserror-impl 1.0.69", ] [[package]] @@ -1408,7 +1294,18 @@ version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.18", +] + +[[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.117", ] [[package]] @@ -1419,7 +1316,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1472,7 +1369,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1485,6 +1382,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -1600,22 +1508,18 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - [[package]] name = "want" version = "0.3.1" @@ -1682,7 +1586,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -1695,6 +1599,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.98" @@ -1716,58 +1633,20 @@ dependencies = [ ] [[package]] -name = "webpki-root-certs" +name = "webpki-roots" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" dependencies = [ "rustls-pki-types", ] -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-registry" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" -dependencies = [ - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -1955,7 +1834,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -1976,7 +1855,7 @@ checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1996,7 +1875,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -2036,7 +1915,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 3893df5..d036f9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,10 +4,14 @@ version = "0.1.0" edition = "2024" [dependencies] -reqwest = { version = "0.13", features = ["json"] } +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } tokio = { version = "1.52", features = ["full"] } +tokio-stream = "0.1" +tokio-util = "0.7" +futures-util = "0.3" serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } +openrouter-rs = "0.10" dotenvy = "0.15" axum = "0.8" anyhow = "1.0" @@ -16,4 +20,4 @@ hmac = "0.13" sha2 = "0.11" hex = "0.4" subtle = "2.6" -bytes = "1.11" \ No newline at end of file +bytes = "1.11" diff --git a/src/api.rs b/src/api.rs index a62d234..e98f736 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,9 +1,10 @@ use axum::body::{Bytes, to_bytes}; -use axum::extract::{FromRef, FromRequest}; -use axum::response::{IntoResponse, Response}; +use axum::extract::{FromRef, FromRequest, State}; +use axum::response::IntoResponse; use axum::routing::{get, post}; use axum::{Json, Router}; use hmac::{Hmac, KeyInit, Mac}; +use reqwest::StatusCode; use serde_json::Value; use sha2::Sha256; use subtle::ConstantTimeEq; @@ -31,8 +32,17 @@ async fn root() -> &'static str { "Hi, i'm Herald :)" } -async fn webhook(WebhookExtract(wb): WebhookExtract) -> Result { - Ok("lol".into_response()) +async fn webhook( + State(app_state): State, + WebhookExtract(wb): WebhookExtract, +) -> Result { + app_state + .bot_tx + .send(wb) + .await + .map_err(anyhow::Error::from)?; + + Ok((StatusCode::CREATED, "Task started")) } pub struct WebhookExtract(pub WebhookType); diff --git a/src/bot.rs b/src/bot.rs index 6e9ed0f..bd9b62c 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -1,13 +1,117 @@ -use crate::{env::EnvConfig, gitea::WebhookType}; +use serde::Deserialize; +use std::time::Duration; + +use crate::{ + env::EnvConfig, + gitea::{GiteaAPI, WebhookType}, + open_router::OpenRouterClient, +}; + +#[derive(Deserialize, Debug)] +pub struct ReviewResult { + pub reviews: Vec, + pub comment: String, + pub cost: Option, +} + +#[derive(Deserialize, Debug)] +pub struct ReviewItem { + pub filename: String, + pub line: Option, + pub code: String, + pub message: String, +} + +/// Map a filename to a markdown language identifier for syntax highlighting. +fn lang_from_filename(filename: &str) -> &str { + match std::path::Path::new(filename) + .extension() + .and_then(|e| e.to_str()) + .unwrap_or("") + { + "rs" => "rust", + "py" => "python", + "js" | "mjs" => "javascript", + "ts" => "typescript", + "jsx" => "jsx", + "tsx" => "tsx", + "go" => "go", + "java" => "java", + "kt" | "kts" => "kotlin", + "scala" => "scala", + "c" | "h" => "c", + "cpp" | "cc" | "cxx" | "hpp" | "hxx" => "cpp", + "rb" => "ruby", + "php" => "php", + "swift" => "swift", + "sh" | "bash" | "zsh" => "bash", + "sql" => "sql", + "html" | "htm" => "html", + "css" => "css", + "scss" | "sass" => "scss", + "json" => "json", + "yaml" | "yml" => "yaml", + "xml" => "xml", + "toml" => "toml", + "md" | "mdx" => "markdown", + "dockerfile" | "Dockerfile" => "dockerfile", + "Makefile" => "makefile", + _ => "", + } +} pub struct Bot { config: EnvConfig, + gitea_api: GiteaAPI, + open_router_client: OpenRouterClient, + http_client: reqwest::Client, } impl Bot { - pub fn new(config: EnvConfig) -> Self { - Self { config } + pub fn new(config: EnvConfig) -> anyhow::Result { + let gitea_timeout = config.gitea_timeout; + let open_router_timeout = config.open_router_timeout; + + Ok(Self { + gitea_api: GiteaAPI::new(&config.gitea_url, &config.gitea_token, gitea_timeout)?, + open_router_client: OpenRouterClient::new( + &config.open_router_api_key, + &config.open_router_model, + open_router_timeout, + )?, + config, + http_client: reqwest::Client::builder() + .timeout(Duration::from_secs(gitea_timeout)) + .build()?, + }) } - pub async fn exec(&self, webhook: WebhookType) {} + pub async fn start( + &self, + mut rx: tokio::sync::mpsc::Receiver, + ) -> anyhow::Result<()> { + while let Some(wb) = rx.recv().await { + self.exec(wb).await; + } + + Ok(()) + } + + pub async fn exec(&self, webhook: WebhookType) { + let exec_result = match webhook { + WebhookType::Review(review_payload) => crate::bot_actions::review::exec_review( + &self.gitea_api, + &self.open_router_client, + &self.http_client, + &self.config.open_router_model, + review_payload, + ), + } + .await; + + match exec_result { + Ok(_) => println!("Task completed"), + Err(err) => println!("{}", err), + } + } } diff --git a/src/bot_actions/mod.rs b/src/bot_actions/mod.rs new file mode 100644 index 0000000..0755bd4 --- /dev/null +++ b/src/bot_actions/mod.rs @@ -0,0 +1 @@ +pub mod review; diff --git a/src/bot_actions/review.rs b/src/bot_actions/review.rs new file mode 100644 index 0000000..8ffc3b1 --- /dev/null +++ b/src/bot_actions/review.rs @@ -0,0 +1,201 @@ +use futures_util::stream::TryStreamExt; +use tokio::io::AsyncReadExt; +use tokio_util::io::StreamReader; + +use crate::{ + bot::ReviewResult, + consts::{BOT_PROCESS_MSG, MAX_DIFF_SIZE, REVIEW_PROMPT}, + errors::AppError, + gitea::{GiteaAPI, ReviewPayload}, + open_router::OpenRouterClient, +}; + +pub async fn exec_review( + gitea_api: &GiteaAPI, + open_router_client: &OpenRouterClient, + http_client: &reqwest::Client, + model: &str, + review_payload: ReviewPayload, +) -> Result<(), AppError> { + let new_comment = gitea_api + .comment( + &BOT_PROCESS_MSG.replace("{model}", model), + &review_payload.repository.full_name, + review_payload.pull_request.number, + ) + .await?; + + let bot_result: Result = async { + let git_diff = + download_git_diff(&http_client, &review_payload.pull_request.diff_url).await?; + + let diff_for_llm = format_diff_for_review(&git_diff); + + let bot_request = REVIEW_PROMPT + .replace("{subject}", &review_payload.pull_request.title) + .replace("{comment}", &review_payload.comment.body) + .replace("{diff}", &diff_for_llm); + + let chat_result = open_router_client.chat(&bot_request).await?; + let mut review_result = serde_json::from_str::(&chat_result.message)?; + + review_result.cost = chat_result.cost; + + gitea_api + .post_pull_request_review( + &review_result, + &review_payload.repository.full_name, + review_payload.pull_request.number, + ) + .await?; + + Ok(review_result) + } + .await; + + let edit_msg = match bot_result { + Ok(bot_result) => review_result_to_markdown(&bot_result), + Err(e) => format!("Error while reviewing: {}", e), + }; + + gitea_api + .edit_comment( + &edit_msg, + &review_payload.repository.full_name, + new_comment.id, + ) + .await?; + + Ok(()) +} + +fn review_result_to_markdown(review_result: &ReviewResult) -> String { + if review_result.reviews.is_empty() { + return String::from("No issues found. ✅"); + } + + let mut md = String::from("## Review Feedback\n\n"); + + md.push_str(&format!( + "### {} issues found.\n\n", + review_result.reviews.len() + )); + + if !review_result.comment.is_empty() { + md.push_str("\n---\n\n"); + md.push_str("### Summary\n\n"); + md.push_str(&review_result.comment); + md.push('\n'); + } + + if let Some(cost) = review_result.cost { + md.push_str("\n---\n\n"); + md.push_str(&format!("### Cost: ${}", cost)); + md.push('\n'); + } + + md +} + +async fn download_git_diff(http_client: &reqwest::Client, url: &str) -> anyhow::Result { + let response = http_client.get(url).send().await?; + let stream = response.bytes_stream().map_err(std::io::Error::other); + + let mut buf = Vec::with_capacity(MAX_DIFF_SIZE); + StreamReader::new(stream) + .take((MAX_DIFF_SIZE + 1) as u64) + .read_to_end(&mut buf) + .await?; + + if buf.len() > MAX_DIFF_SIZE { + anyhow::bail!("Git diff exceeds the maximum allowed size of 1 Mo"); + } + + Ok(String::from_utf8_lossy(&buf).into_owned()) +} + +fn format_diff_for_review(git_diff: &str) -> String { + let mut output = String::new(); + let mut current_file: Option<&str> = None; + let mut new_line: u64 = 0; + + for line in git_diff.lines() { + if let Some(rest) = line.strip_prefix("diff --git a/") { + if let Some(end) = rest.find(' ') { + current_file = Some(&rest[..end]); + } + new_line = 0; + continue; + } + + if line.starts_with("---") || line.starts_with("+++") { + continue; + } + + if line.starts_with("@@") && line.contains('+') { + if let Some(start) = parse_hunk_new_start(line) { + new_line = start; + } + continue; + } + + let Some(filename) = current_file else { + continue; + }; + + if line.starts_with(' ') { + new_line += 1; + continue; + } + + if let Some(code) = line.strip_prefix('+') { + use std::fmt::Write; + let _ = writeln!(output, "{filename}:{new_line}:{code}"); + new_line += 1; + } + } + + output +} + +fn parse_hunk_new_start(hunk_header: &str) -> Option { + let plus_part = hunk_header.split('+').nth(1)?; + let num_str = plus_part.split(|c: char| !c.is_ascii_digit()).next()?; + num_str.parse::().ok() +} + +#[cfg(test)] +#[test] +fn test_format_diff_for_review() { + let diff = concat!( + "diff --git a/src/foo.rs b/src/foo.rs\n", + "--- a/src/foo.rs\n", + "+++ b/src/foo.rs\n", + "@@ -1,3 +1,6 @@\n", + " fn main() {\n", + "+ let x = 1;\n", + " println!(\"hello\");\n", + "+ let y = 2;\n", + "+ let z = 3;\n", + " }\n", + "diff --git a/src/bar.rs b/src/bar.rs\n", + "--- a/src/bar.rs\n", + "+++ b/src/bar.rs\n", + "@@ -10,4 +10,6 @@\n", + " old context\n", + "+ let a = 10;\n", + " more context\n", + "+ let b = 20;\n", + ); + + let result = format_diff_for_review(diff); + let expected = concat!( + "src/foo.rs:2: let x = 1;\n", + "src/foo.rs:4: let y = 2;\n", + "src/foo.rs:5: let z = 3;\n", + "src/bar.rs:11: let a = 10;\n", + "src/bar.rs:13: let b = 20;\n", + ); + + assert_eq!(result, expected); +} diff --git a/src/consts.rs b/src/consts.rs index 75ebdab..9a38974 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -1,3 +1,44 @@ pub const GITEA_SIG_HEADER_NAME: &str = "x-gitea-signature"; pub const GITEA_EVENT_TYPE_HEADER_NAME: &str = "x-gitea-event-type"; + pub const MAX_WEBHOOK_BODY_SIZE: usize = 1024 * 1024; // 1 MiB +pub const MAX_DIFF_SIZE: usize = 1024 * 1024; // 1 MiB + +pub const BOT_PROCESS_MSG: &str = " + Review in progress with the model \"{model}\"... +"; + +pub const REVIEW_PROMPT: &str = " + You are a senior software engineer reviewing code changes. + + Check good practices and code quality. + + This is the pull request subject: \"{subject}\" + + This is the user comment: \"{comment}\" + + The code changes (only added lines, with line numbers): + + {diff} + + Please review the code changes and provide feedback. + + IMPORTANT: the `line` field must be the line number shown before each line. + The provided code has the format: `filename:line:code` + + Return your feedback, in french, with only this json format, reviews must contain each review + All fields are mandatory. + (filename field must contain the full path with extension) and comment must contain a final summary: + + { + \"reviews\": [ + { + \"filename\": \"\", + \"line\": , + \"code\": \"\", + \"message\": \"\" + } + ], + \"comment\": \"\" + } +"; diff --git a/src/env.rs b/src/env.rs index e4a8d1a..6d013be 100644 --- a/src/env.rs +++ b/src/env.rs @@ -6,7 +6,12 @@ pub struct EnvConfig { pub http_port: u16, pub webhook_secret: String, pub open_router_api_key: String, + pub open_router_model: String, + pub open_router_timeout: u64, pub bot_name: String, + pub gitea_url: String, + pub gitea_token: String, + pub gitea_timeout: u64, } pub fn load_config() -> anyhow::Result { @@ -16,12 +21,22 @@ pub fn load_config() -> anyhow::Result { let bot_name = try_get_env("BOT_NAME")?; let webhook_secret = try_get_env("WEBHOOK_SIG_HEADER_SECRET")?; let open_router_api_key = try_get_env("OPEN_ROUTER_API_KEY")?; + let open_router_model = try_get_env("OPEN_ROUTER_MODEL")?; + let open_router_timeout = try_get_env("OPEN_ROUTER_TIMEOUT")?.parse()?; + let gitea_url = try_get_env("GITEA_URL")?; + let gitea_token = try_get_env("GITEA_TOKEN")?; + let gitea_timeout = try_get_env("GITEA_TIMEOUT")?.parse()?; Ok(EnvConfig { http_port, webhook_secret, bot_name, open_router_api_key, + open_router_model, + open_router_timeout, + gitea_url, + gitea_token, + gitea_timeout, }) } diff --git a/src/gitea.rs b/src/gitea.rs index b5ab824..5a725ea 100644 --- a/src/gitea.rs +++ b/src/gitea.rs @@ -1,7 +1,135 @@ -use serde::Deserialize; -use serde_json::Value; +use std::time::Duration; -use crate::errors::AppError; +use serde::Deserialize; +use serde_json::{Value, json}; + +use crate::{ + bot::{ReviewItem, ReviewResult}, + errors::AppError, +}; + +pub struct GiteaAPI { + base_url: String, + client: reqwest::Client, +} + +impl GiteaAPI { + pub fn new(base_url: &str, token: &str, timeout: u64) -> anyhow::Result { + let mut default_headers = reqwest::header::HeaderMap::new(); + default_headers.insert( + reqwest::header::HeaderName::from_static("authorization"), + reqwest::header::HeaderValue::from_str(&format!("Bearer {}", token))?, + ); + + Ok(Self { + base_url: String::from(base_url), + client: reqwest::Client::builder() + .timeout(Duration::from_secs(timeout)) + .default_headers(default_headers) + .build()?, + }) + } + + pub async fn comment( + &self, + body: &str, + full_name: &str, + index: u64, + ) -> anyhow::Result { + let url = format!( + "{}/api/v1/repos/{}/issues/{}/comments", + self.base_url, full_name, index + ); + + let res = self + .client + .post(url) + .json(&json!({ + "body": body + })) + .send() + .await?; + + if !res.status().is_success() { + return Err(anyhow::anyhow!("Failed to comment: {}", res.status())); + } + + res.json::().await.map_err(anyhow::Error::from) + } + + pub async fn edit_comment( + &self, + body: &str, + full_name: &str, + comment_id: u64, + ) -> anyhow::Result<()> { + let url = format!( + "{}/api/v1/repos/{}/issues/comments/{}", + self.base_url, full_name, comment_id + ); + + let res = self + .client + .patch(url) + .json(&json!({ + "body": body + })) + .send() + .await?; + + if !res.status().is_success() { + return Err(anyhow::anyhow!("Failed to comment: {}", res.status())); + } + + Ok(()) + } + + pub async fn post_pull_request_review( + &self, + review_result: &ReviewResult, + full_name: &str, + index: u64, + ) -> anyhow::Result<()> { + let url = format!( + "{}/api/v1/repos/{}/pulls/{}/reviews", + self.base_url, full_name, index + ); + + let comments = &&review_result + .reviews + .iter() + .filter(|r| r.line.is_some()) + .map(|r| { + let path = r.filename.clone(); + let line = r.line.unwrap_or(0); + let body = r.message.clone(); + + json!({ + "path": path, + "new_position": line, + "body": body + }) + }) + .collect::>(); + + let res = self + .client + .post(url) + .json(&json!({ + "event": "COMMENT", + "body": review_result.comment, + "comments": comments + })) + .send() + .await?; + + if !res.status().is_success() { + return Err(anyhow::anyhow!("Failed to post review: {}", res.status())); + } + + Ok(()) + } +} #[derive(Debug)] pub enum WebhookType { @@ -12,6 +140,7 @@ pub enum WebhookType { pub struct ReviewPayload { pub action: String, pub pull_request: PullRequest, + pub repository: Repository, pub comment: Comment, } @@ -19,6 +148,8 @@ pub struct ReviewPayload { pub struct PullRequest { pub id: u64, pub diff_url: String, + pub number: u64, + pub title: String, } #[derive(Deserialize, Debug)] @@ -33,6 +164,11 @@ pub struct User { pub id: u64, } +#[derive(Deserialize, Debug)] +pub struct Repository { + pub full_name: String, +} + impl WebhookType { pub fn from_event(event: &str, bot_name: &str, json: Value) -> Result { let wb = match event { diff --git a/src/main.rs b/src/main.rs index 83830ed..f1a8ad1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,25 +1,25 @@ -use std::sync::Arc; - -use tokio::sync::Mutex; - -use crate::{bot::Bot, state::AppState}; +use crate::{bot::Bot, gitea::WebhookType, state::AppState}; mod api; mod bot; +mod bot_actions; mod consts; mod env; mod errors; mod gitea; +mod open_router; mod state; #[tokio::main] async fn main() -> anyhow::Result<()> { let config = env::load_config()?; + let bot = Bot::new(config.clone())?; - let app_state = AppState { - bot: Arc::new(Mutex::new(Bot::new(config.clone()))), - config: config, - }; + let (tx, rx) = tokio::sync::mpsc::channel::(1); - api::start(app_state).await + let app_state = AppState { bot_tx: tx, config }; + + tokio::try_join!(bot.start(rx), api::start(app_state))?; + + Ok(()) } diff --git a/src/open_router.rs b/src/open_router.rs index e69de29..b5602d5 100644 --- a/src/open_router.rs +++ b/src/open_router.rs @@ -0,0 +1,47 @@ +use std::time::Duration; + +use openrouter_rs::{Message, api::chat::ChatCompletionRequest}; + +pub struct ChatResult { + pub message: String, + pub cost: Option, +} + +pub struct OpenRouterClient { + client: openrouter_rs::OpenRouterClient, + model: String, +} + +impl OpenRouterClient { + pub fn new(token: &str, model: &str, timeout: u64) -> anyhow::Result { + Ok(Self { + client: openrouter_rs::OpenRouterClient::builder() + .api_key(token) + .http_client( + reqwest::Client::builder() + .timeout(Duration::from_secs(timeout)) + .build()?, + ) + .build()?, + model: String::from(model), + }) + } + + pub async fn chat(&self, msg: &str) -> anyhow::Result { + let request = ChatCompletionRequest::builder() + .model(&self.model) + .enable_reasoning() + .messages(vec![Message::new(openrouter_rs::types::Role::User, msg)]) + .build()?; + + let response = self.client.chat().create(&request).await?; + + Ok(ChatResult { + message: response.choices[0] + .content() + .map(|msg| String::from(msg)) + .ok_or(anyhow::anyhow!("No content"))?, + cost: response.usage.and_then(|u| u.cost), + }) + } +} diff --git a/src/state.rs b/src/state.rs index 28c3bfe..4b9cc02 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,10 +1,7 @@ -use std::sync::Arc; -use tokio::sync::Mutex; - -use crate::{bot::Bot, env::EnvConfig}; +use crate::{env::EnvConfig, gitea::WebhookType}; #[derive(Clone)] pub struct AppState { - pub bot: Arc>, + pub bot_tx: tokio::sync::mpsc::Sender, pub config: EnvConfig, }