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..e04ae8a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -229,6 +229,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 +315,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -258,12 +324,30 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "dotenvy_macro" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0235d912a8c749f4e0c9f18ca253b4c28cfefc1d2518096016d6e3230b6424" +dependencies = [ + "dotenvy", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "dunce" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -331,6 +415,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 +451,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", ] @@ -417,12 +522,13 @@ dependencies = [ "dotenvy", "hex", "hmac", - "reqwest", + "openrouter-rs", + "reqwest 0.13.3", "serde", "serde_json", "sha2", "subtle", - "thiserror", + "thiserror 2.0.18", "tokio", ] @@ -530,6 +636,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", + "webpki-roots", ] [[package]] @@ -639,6 +746,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" @@ -694,7 +807,7 @@ dependencies = [ "jni-sys", "log", "simd_cesu8", - "thiserror", + "thiserror 2.0.18", "walkdir", "windows-link", ] @@ -709,7 +822,7 @@ dependencies = [ "quote", "rustc_version", "simd_cesu8", - "syn", + "syn 2.0.117", ] [[package]] @@ -728,7 +841,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -821,6 +934,26 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "openrouter-rs" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7101578df2f54d9013594e94367adbe656521ef6002f03e4577f1e00e57cb4" +dependencies = [ + "derive_builder", + "dotenvy_macro", + "futures-util", + "http", + "reqwest 0.12.28", + "schemars", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tokio-util", + "urlencoding", +] + [[package]] name = "openssl-probe" version = "0.2.1" @@ -903,7 +1036,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -925,7 +1058,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -998,6 +1131,67 @@ dependencies = [ "bitflags", ] +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +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", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "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]] name = "reqwest" version = "0.13.3" @@ -1075,6 +1269,7 @@ checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "aws-lc-rs", "once_cell", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -1172,6 +1367,31 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1234,7 +1454,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]] @@ -1344,12 +1575,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,7 +1626,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1402,13 +1650,33 @@ dependencies = [ "libc", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.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 +1687,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1472,7 +1740,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1600,6 +1868,12 @@ 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" @@ -1682,7 +1956,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -1695,6 +1969,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" @@ -1724,6 +2011,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi-util" version = "0.1.11" @@ -1955,7 +2251,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -1976,7 +2272,7 @@ checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1996,7 +2292,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -2036,7 +2332,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 3893df5..26a473d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ reqwest = { version = "0.13", features = ["json"] } tokio = { version = "1.52", features = ["full"] } 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 +17,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/bot.rs b/src/bot.rs index ac104eb..c10bc39 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -1,22 +1,81 @@ -use std::time::Duration; +use serde::Deserialize; use crate::{ + consts::{BOT_PROCESS_MSG, REVIEW_PROMPT}, env::EnvConfig, errors::AppError, gitea::{GiteaAPI, ReviewPayload, WebhookType}, + open_router::OpenRouterClient, }; +#[derive(Deserialize, Debug)] +struct ReviewResult { + reviews: Vec, + comment: String, +} + +#[derive(Deserialize, Debug)] +struct ReviewItem { + filename: String, + line: Option, + code: String, + 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, } impl Bot { - pub fn new(config: EnvConfig) -> Self { - Self { + pub fn new(config: EnvConfig) -> anyhow::Result { + Ok(Self { gitea_api: GiteaAPI::new(&config.gitea_url, &config.gitea_token), + open_router_client: OpenRouterClient::new( + &config.open_router_api_key, + &config.open_router_model, + )?, config, - } + }) } pub async fn start( @@ -46,17 +105,87 @@ impl Bot { let new_comment = self .gitea_api .comment( + &BOT_PROCESS_MSG.replace("{model}", &self.config.open_router_model), &review_payload.repository.full_name, review_payload.pull_request.number, ) .await?; - tokio::time::sleep(Duration::from_secs(10)).await; + let bot_result: Result = async { + let git_diff = self + .download_git_diff(&review_payload.pull_request.diff_url) + .await?; + + let bot_request = &&REVIEW_PROMPT + .replace("{subject}", &review_payload.pull_request.title) + .replace("{comment}", &review_payload.comment.body) + .replace("{diff}", &git_diff); + + self.open_router_client.chat(&bot_request).await + } + .await; + + let edit_msg = match bot_result { + Ok(bot_result) => self.review_result_to_markdown(&bot_result), + Err(e) => format!("Error while reviewing: {}", e), + }; self.gitea_api - .edit_comment(&review_payload.repository.full_name, new_comment.id) + .edit_comment( + &edit_msg, + &review_payload.repository.full_name, + new_comment.id, + ) .await?; Ok(()) } + + fn review_result_to_markdown(&self, result: &str) -> String { + let review_result: ReviewResult = match serde_json::from_str(result) { + Ok(review_result) => review_result, + Err(_) => { + return format!( + "Failed to parse review result. Raw output:\n\n```json\n{}\n```", + result + ); + } + }; + + if review_result.reviews.is_empty() { + return String::from("No issues found. ✅"); + } + + let mut md = String::from("## Review Feedback\n\n"); + for (i, item) in review_result.reviews.iter().enumerate() { + if i > 0 { + md.push_str("\n---\n\n"); + } + md.push_str(&format!("### `{}`\n\n", item.filename)); + if let Some(line) = item.line { + md.push_str(&format!("> **Line {}**\n\n", line)); + } + if !item.code.is_empty() { + let lang = lang_from_filename(&item.filename); + md.push_str(&format!("```{}\n{}\n```\n\n", lang, item.code)); + } + md.push_str(&item.message); + md.push('\n'); + } + + 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'); + } + + md + } + + async fn download_git_diff(&self, url: &str) -> anyhow::Result { + let response = reqwest::get(url).await?; + let body = response.text().await?; + Ok(body) + } } diff --git a/src/consts.rs b/src/consts.rs index 75ebdab..9d1dce1 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -1,3 +1,36 @@ 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 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}\" + + This is the git diff of the code changes: + + \"{diff}\" + + Please review the code changes and provide feedback. + Return your feedback with only this json format, reviews must contain each review (filename field must contain the full path with extension) and comment msut contain a final summary: + + { + \"reviews\": [ + { + \"filename\": \"\", + \"line\": , + \"code\": \"\", + \"message\": \"\" + } + ], + \"comment\": \"\" + } +"; diff --git a/src/env.rs b/src/env.rs index 1001e62..9ad2fa4 100644 --- a/src/env.rs +++ b/src/env.rs @@ -6,6 +6,7 @@ pub struct EnvConfig { pub http_port: u16, pub webhook_secret: String, pub open_router_api_key: String, + pub open_router_model: String, pub bot_name: String, pub gitea_url: String, pub gitea_token: String, @@ -18,6 +19,7 @@ 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 gitea_url = try_get_env("GITEA_URL")?; let gitea_token = try_get_env("GITEA_TOKEN")?; @@ -26,6 +28,7 @@ pub fn load_config() -> anyhow::Result { webhook_secret, bot_name, open_router_api_key, + open_router_model, gitea_url, gitea_token, }) diff --git a/src/gitea.rs b/src/gitea.rs index 63a816e..c551b16 100644 --- a/src/gitea.rs +++ b/src/gitea.rs @@ -16,7 +16,12 @@ impl GiteaAPI { } } - pub async fn comment(&self, full_name: &str, index: u64) -> anyhow::Result { + pub async fn comment( + &self, + body: &str, + full_name: &str, + index: u64, + ) -> anyhow::Result { let url = format!( "{}/api/v1/repos/{}/issues/{}/comments?access_token={}", self.base_url, full_name, index, self.token @@ -26,17 +31,20 @@ impl GiteaAPI { let res = client .post(url) .json(&json!({ - "body": "Hello world :)" + "body": body })) .send() .await?; - println!("{}", res.status()); - res.json::().await.map_err(anyhow::Error::from) } - pub async fn edit_comment(&self, full_name: &str, comment_id: u64) -> anyhow::Result<()> { + pub async fn edit_comment( + &self, + body: &str, + full_name: &str, + comment_id: u64, + ) -> anyhow::Result<()> { let url = format!( "{}/api/v1/repos/{}/issues/comments/{}?access_token={}", self.base_url, full_name, comment_id, self.token @@ -46,13 +54,11 @@ impl GiteaAPI { let res = client .patch(url) .json(&json!({ - "body": "Updated Hello world :)" + "body": body })) .send() .await?; - println!("{}", res.status()); - Ok(()) } } @@ -75,6 +81,7 @@ pub struct PullRequest { pub id: u64, pub diff_url: String, pub number: u64, + pub title: String, } #[derive(Deserialize, Debug)] diff --git a/src/main.rs b/src/main.rs index 00b02c1..ad78bc0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,21 +6,19 @@ 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 bot = Bot::new(config.clone())?; let (tx, rx) = tokio::sync::mpsc::channel::(1); - let app_state = AppState { - bot_tx: tx, - config, - }; + let app_state = AppState { bot_tx: tx, config }; tokio::try_join!(bot.start(rx), api::start(app_state))?; Ok(()) -} \ No newline at end of file +} diff --git a/src/open_router.rs b/src/open_router.rs index e69de29..017b9b2 100644 --- a/src/open_router.rs +++ b/src/open_router.rs @@ -0,0 +1,34 @@ +use openrouter_rs::{Message, api::chat::ChatCompletionRequest}; + +pub struct OpenRouterClient { + client: openrouter_rs::OpenRouterClient, + model: String, +} + +impl OpenRouterClient { + pub fn new(token: &str, model: &str) -> anyhow::Result { + Ok(Self { + client: openrouter_rs::OpenRouterClient::builder() + .api_key(token) + .build()?, + model: String::from(model), + }) + } + + pub async fn chat(&self, msg: &str) -> anyhow::Result { + let request = ChatCompletionRequest::builder() + .model(&self.model) + .messages(vec![Message::new( + openrouter_rs::types::Role::Developer, + msg, + )]) + .build()?; + + let response = self.client.chat().create(&request).await?; + + response.choices[0] + .content() + .map(|msg| String::from(msg)) + .ok_or(anyhow::anyhow!("No content")) + } +}