diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 139c737..745ef73 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -12,8 +12,18 @@ "containerEnv": { "SHELL": "/bin/bash" }, + "customizations": { + "vscode": { + "extensions": ["rust-lang.rust-analyzer"], + "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/docs/webhook_pr_body.json b/docs/webhook_pr_body.json new file mode 100644 index 0000000..1a7fec3 --- /dev/null +++ b/docs/webhook_pr_body.json @@ -0,0 +1,224 @@ +{ + "action": "created", + "issue": { + "id": 1, + "url": "https://gitea.example.com/api/v1/repos/username/repo-name/issues/1", + "html_url": "https://gitea.example.com/username/repo-name/pulls/1", + "number": 1, + "user": { + "id": 1, + "login": "username", + "login_name": "", + "source_id": 0, + "full_name": "", + "email": "user@example.com", + "avatar_url": "https://gitea.example.com/avatars/aabbccdd", + "html_url": "https://gitea.example.com/username", + "language": "en-US", + "is_admin": true, + "last_login": "2026-01-01T00:00:00+02:00", + "created": "2025-01-01T00:00:00+02:00", + "restricted": false, + "active": true, + "prohibit_login": false, + "location": "", + "website": "", + "description": "", + "visibility": "public", + "followers_count": 0, + "following_count": 0, + "starred_repos_count": 0, + "username": "username" + }, + "original_author": "", + "original_author_id": 0, + "title": "impl webhook route", + "body": "", + "ref": "", + "assets": [], + "labels": [], + "milestone": null, + "assignee": null, + "assignees": null, + "state": "open", + "is_locked": false, + "comments": 1, + "created_at": "2026-01-01T00:00:00+02:00", + "updated_at": "2026-01-01T00:00:00+02:00", + "closed_at": null, + "due_date": null, + "time_estimate": 0, + "pull_request": { + "merged": false, + "merged_at": null, + "draft": false, + "html_url": "https://gitea.example.com/username/repo-name/pulls/1" + }, + "repository": { + "id": 8, + "name": "repo-name", + "owner": "username", + "full_name": "username/repo-name" + }, + "pin_order": 0, + "content_version": 0 + }, + "pull_request": { + "id": 1, + "url": "https://gitea.example.com/username/repo-name/pulls/1", + "number": 1, + "user": { + "id": 1, + "login": "username", + "login_name": "", + "source_id": 0, + "full_name": "", + "email": "user@example.com", + "avatar_url": "https://gitea.example.com/avatars/aabbccdd", + "html_url": "https://gitea.example.com/username", + "language": "en-US", + "is_admin": true, + "last_login": "2026-01-01T00:00:00+02:00", + "created": "2025-01-01T00:00:00+02:00", + "restricted": false, + "active": true, + "prohibit_login": false, + "location": "", + "website": "", + "description": "", + "visibility": "public", + "followers_count": 0, + "following_count": 0, + "starred_repos_count": 0, + "username": "username" + }, + "title": "impl webhook route", + "body": "", + "labels": [], + "milestone": null, + "assignee": null, + "assignees": [], + "requested_reviewers": [], + "requested_reviewers_teams": [], + "state": "open", + "draft": false, + "is_locked": false, + "comments": 1, + "review_comments": 0, + "additions": 3, + "deletions": 3, + "changed_files": 2, + "html_url": "https://gitea.example.com/username/repo-name/pulls/1", + "diff_url": "https://gitea.example.com/username/repo-name/pulls/1.diff", + "patch_url": "https://gitea.example.com/username/repo-name/pulls/1.patch", + "mergeable": true, + "merged": false, + "merged_at": null, + "merge_commit_sha": null, + "merged_by": null, + "allow_maintainer_edit": false, + "base": { + "label": "main", + "ref": "main", + "sha": "aabbccdd00112233445566778899aabbccdd0011", + "repo_id": 8, + "repo": { + "id": 8, + "owner": { + "id": 1, + "login": "username", + "email": "user@example.com", + "avatar_url": "https://gitea.example.com/avatars/aabbccdd", + "html_url": "https://gitea.example.com/username", + "username": "username" + }, + "name": "repo-name", + "full_name": "username/repo-name", + "description": "A self-hosted Gitea bot.", + "html_url": "https://gitea.example.com/username/repo-name", + "url": "https://gitea.example.com/api/v1/repos/username/repo-name", + "ssh_url": "git@gitea.example.com:username/repo-name.git", + "clone_url": "https://gitea.example.com/username/repo-name.git", + "default_branch": "main" + } + }, + "head": { + "label": "webhook", + "ref": "webhook", + "sha": "eeff00112233445566778899aabbccddeeff0011", + "repo_id": 8, + "repo": { + "id": 8, + "owner": { + "id": 1, + "login": "username", + "email": "user@example.com", + "avatar_url": "https://gitea.example.com/avatars/aabbccdd", + "html_url": "https://gitea.example.com/username", + "username": "username" + }, + "name": "repo-name", + "full_name": "username/repo-name", + "description": "A self-hosted Gitea bot.", + "html_url": "https://gitea.example.com/username/repo-name", + "url": "https://gitea.example.com/api/v1/repos/username/repo-name", + "ssh_url": "git@gitea.example.com:username/repo-name.git", + "clone_url": "https://gitea.example.com/username/repo-name.git", + "default_branch": "main" + } + }, + "merge_base": "aabbccdd00112233445566778899aabbccdd0011", + "due_date": null, + "created_at": "2026-01-01T00:00:00+02:00", + "updated_at": "2026-01-01T00:00:00+02:00", + "closed_at": null + }, + "comment": { + "id": 3, + "html_url": "https://gitea.example.com/username/repo-name/pulls/1#issuecomment-3", + "pull_request_url": "https://gitea.example.com/username/repo-name/pulls/1", + "issue_url": "", + "user": { + "id": 1, + "login": "username", + "email": "user@example.com", + "avatar_url": "https://gitea.example.com/avatars/aabbccdd", + "html_url": "https://gitea.example.com/username", + "username": "username" + }, + "original_author": "", + "original_author_id": 0, + "body": "Test comment", + "assets": [], + "created_at": "2026-01-01T00:00:00+02:00", + "updated_at": "2026-01-01T00:00:00+02:00" + }, + "repository": { + "id": 8, + "owner": { + "id": 1, + "login": "username", + "email": "user@example.com", + "avatar_url": "https://gitea.example.com/avatars/aabbccdd", + "html_url": "https://gitea.example.com/username", + "username": "username" + }, + "name": "repo-name", + "full_name": "username/repo-name", + "description": "A self-hosted Gitea bot.", + "html_url": "https://gitea.example.com/username/repo-name", + "url": "https://gitea.example.com/api/v1/repos/username/repo-name", + "ssh_url": "git@gitea.example.com:username/repo-name.git", + "clone_url": "https://gitea.example.com/username/repo-name.git", + "default_branch": "main" + }, + "sender": { + "id": 1, + "login": "username", + "email": "user@example.com", + "avatar_url": "https://gitea.example.com/avatars/aabbccdd", + "html_url": "https://gitea.example.com/username", + "username": "username" + }, + "is_pull": true +} \ No newline at end of file diff --git a/src/api.rs b/src/api.rs index d77a738..a3647a0 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,10 +1,15 @@ -use axum::Router; -use axum::routing::get; +use axum::response::{Response, IntoResponse}; +use axum::{Json, Router}; +use axum::extract::FromRequest; +use axum::routing::{post, get}; +use reqwest::StatusCode; +use serde_json::Value; -use crate::env; +use crate::env::{EnvConfig}; +use crate::gitea::WebhookType; -pub async fn start_api(config: env::EnvConfig) -> anyhow::Result<()> { - let app = Router::new().route("/webhook", get(webhook)); +pub async fn start_api(config: EnvConfig) -> anyhow::Result<()> { + let app = Router::new().route("/", get(root)).route("/webhook", post(webhook)); let listerner = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", config.http_port)).await?; axum::serve(listerner, app) @@ -12,6 +17,38 @@ pub async fn start_api(config: env::EnvConfig) -> anyhow::Result<()> { .map_err(|e| anyhow::anyhow!(e)) } -async fn webhook() -> &'static str { - "Hello, World!" +async fn root() -> &'static str { + "Hi, i'm Herald :)" +} + +async fn webhook(WebhookExtract(wb): WebhookExtract) -> Response { + match wb { + WebhookType::Review(id, _) => format!("Received {} pr id", id), + _ => String::from("Nothing to see :/") + }.into_response() +} + +pub struct WebhookExtract(pub WebhookType); + +impl FromRequest for WebhookExtract +where + S: Send + Sync, +{ + type Rejection = Response; + + async fn from_request( + req: axum::extract::Request, + state: &S, + ) -> Result { + let Json(value) = Json::::from_request(req, state) + .await + .map_err(|e| e.into_response())?; + + let webhook = WebhookType::try_from(value) + .map_err(|e| { + (StatusCode::UNPROCESSABLE_ENTITY, e.to_string()).into_response() + })?; + + Ok(WebhookExtract(webhook)) + } } diff --git a/src/bot.rs b/src/bot.rs new file mode 100644 index 0000000..4e4fa76 --- /dev/null +++ b/src/bot.rs @@ -0,0 +1,5 @@ +use crate::gitea::WebhookType; + +pub async fn exec(webhook: WebhookType) { + +} \ No newline at end of file diff --git a/src/gitea.rs b/src/gitea.rs index e69de29..113e76d 100644 --- a/src/gitea.rs +++ b/src/gitea.rs @@ -0,0 +1,35 @@ +use anyhow::anyhow; +use serde_json::Value; + + +pub enum WebhookType { + Review(u64, String) +} + +impl TryFrom for WebhookType { + type Error = anyhow::Error; + + fn try_from(json: Value) -> Result { + let pull_request = json.get("pull_request"); + let comment = json.get("comment"); + + if let (Some(pull_request), Some(comment)) = (pull_request, comment) { + let comment_body = comment + .get("body") + .ok_or(anyhow!("comment body not found"))? + .as_str() + .ok_or(anyhow!("error while get pr comment"))? + .to_string(); + + let pr_id = pull_request + .get("id") + .ok_or(anyhow!("pr id not found"))? + .as_u64() + .ok_or(anyhow!("error while get pr id"))?; + + return Ok(WebhookType::Review(pr_id, comment_body)); + } + + anyhow::bail!("unknow webhook type") + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index d5f4d79..1dd3444 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,7 @@ mod api; mod env; +mod gitea; +mod bot; #[tokio::main] async fn main() -> anyhow::Result<()> { diff --git a/src/claude.rs b/src/open_router.rs similarity index 100% rename from src/claude.rs rename to src/open_router.rs