diff --git a/aries/src/service/relay_server.rs b/aries/src/service/relay_server.rs index 4914309d7..31fd6c3fc 100644 --- a/aries/src/service/relay_server.rs +++ b/aries/src/service/relay_server.rs @@ -2,13 +2,14 @@ use std::net::SocketAddr; use std::str::FromStr; use std::time::{Duration, SystemTime}; -use axum::extract::{Query, State}; +use axum::extract::{Path, Query, State}; use axum::http::StatusCode; use axum::response::IntoResponse; use axum::routing::{get, post}; use axum::{Json, Router}; use callisto::{ztm_node, ztm_repo_info}; use clap::Parser; +use serde_json::Value; use common::config::Config; use gemini::ztm::hub::{LocalHub, ZTMUserPermit, ZTMCA}; use gemini::{Node, RelayGetParams, RelayResultRes, RepoInfo}; @@ -104,7 +105,8 @@ pub fn routers() -> Router { .route("/ping", get(ping)) .route("/node_list", get(node_list)) .route("/repo_provide", post(repo_provide)) - .route("/repo_list", get(repo_list)); + .route("/repo_list", get(repo_list)) + .route("/github/webhook/:peer_id", post(github_webhook)); Router::new().merge(router) } @@ -220,6 +222,15 @@ pub async fn repo_list( Ok(Json(repo_info_list_result)) } +/// Forwards the GitHub webhook to the corresponding peer. +async fn github_webhook( + Path(peer_id): Path, + Json(payload): Json, +) -> Result { + tracing::info!("GitHub Webhook Event: {:?} \n {:?}", peer_id, payload); + unimplemented!(); +} + async fn loop_running(context: Context) { let mut interval = tokio::time::interval(Duration::from_secs(60)); diff --git a/gateway/src/api/api_router.rs b/gateway/src/api/api_router.rs index 3fff4acda..a788d702f 100644 --- a/gateway/src/api/api_router.rs +++ b/gateway/src/api/api_router.rs @@ -15,7 +15,7 @@ use ceres::model::{ }; use common::model::CommonResult; -use crate::api::mr_router; +use crate::api::{github_router, mr_router}; use crate::api::ApiServiceState; use super::ztm_router; @@ -34,6 +34,7 @@ pub fn routers() -> Router { .merge(router) .merge(mr_router::routers()) .merge(ztm_router::routers()) + .merge(github_router::routers()) } async fn get_blob_object( diff --git a/gateway/src/api/github_router.rs b/gateway/src/api/github_router.rs new file mode 100644 index 000000000..159a68748 --- /dev/null +++ b/gateway/src/api/github_router.rs @@ -0,0 +1,82 @@ +use axum::{Json, Router}; +use axum::http::{HeaderMap, StatusCode}; +use axum::response::IntoResponse; +use axum::routing::post; +use lazy_static::lazy_static; +use reqwest::Client; +use serde_json::Value; +use taurus::event::github_webhook::{GithubWebhookEvent, WebhookType}; +use crate::api::ApiServiceState; + +lazy_static! { + static ref CLIENT: Client = Client::builder() + .user_agent("Mega/0.0.1") // IMPORTANT, or 403 Forbidden + .build() + .unwrap(); +} + +pub fn routers() -> Router { + Router::new() + .route("/github/webhook", post(webhook)) +} + +/// Handle the GitHub webhook event.
+/// For more details, see https://docs.github.com/zh/webhooks/webhook-events-and-payloads. +async fn webhook( + headers: HeaderMap, + Json(mut payload): Json +) -> Result { + let event_type = headers + .get("X-GitHub-Event") + .and_then(|v| v.to_str().ok()) + .expect("Missing X-GitHub-Event header"); + payload["event_type"] = event_type.into(); + + let event_type = WebhookType::from(event_type); + match event_type { + WebhookType::PullRequest => { + let action = payload["action"].as_str().unwrap(); + tracing::debug!("PR action: {}", action); + + if ["opened", "reopened", "synchronize"].contains(&action) { // contents changed + let url = payload["pull_request"]["url"].as_str().unwrap(); + let files = get_pr_files(url).await; + let commits = get_pr_commits(url).await; + // Add details to the payload + payload["files"] = files; + payload["commits"] = commits; + } else if action == "edited" { // PR title or body edited + let _ = payload["pull_request"]["title"].as_str().unwrap(); + let _ = payload["pull_request"]["body"].as_str().unwrap(); + } + + GithubWebhookEvent::notify(WebhookType::PullRequest, payload); + } + WebhookType::Issues => { + GithubWebhookEvent::notify(WebhookType::Issues, payload); + } + WebhookType::Unknown(_type) => { + tracing::warn!("Unknown event type: {}", _type); + GithubWebhookEvent::notify(WebhookType::Unknown(_type), payload); + } + } + + Ok("WebHook OK") +} + +/// GitHub API: Get the files change of a pull request.
+/// For read-only operation of public repos, no authentication is required. +pub async fn get_pr_files(pr_url: &str) -> Value { + get_request(&format!("{}/files", pr_url)).await +} + +/// GitHub API: Get the commits of a pull request. +pub async fn get_pr_commits(pr_url: &str) -> Value { + get_request(&format!("{}/commits", pr_url)).await +} + +/// Send a GET request to the given URL and return the JSON response. +async fn get_request(url: &str) -> Value { + let resp = CLIENT.get(url).send().await.unwrap(); + resp.json().await.unwrap() +} \ No newline at end of file diff --git a/gateway/src/api/mod.rs b/gateway/src/api/mod.rs index 070abfda8..073d5ee32 100644 --- a/gateway/src/api/mod.rs +++ b/gateway/src/api/mod.rs @@ -11,6 +11,7 @@ pub mod api_router; pub mod mr_router; pub mod oauth; pub mod ztm_router; +mod github_router; #[derive(Clone)] pub struct ApiServiceState { diff --git a/taurus/src/event/github_webhook.rs b/taurus/src/event/github_webhook.rs new file mode 100644 index 000000000..84bbb9fc5 --- /dev/null +++ b/taurus/src/event/github_webhook.rs @@ -0,0 +1,68 @@ +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use crate::event::{EventBase, EventType}; +use crate::queue::get_mq; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GithubWebhookEvent { + pub _type: WebhookType, + pub payload: Value, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum WebhookType { + PullRequest, + Issues, + Unknown(String), +} + +impl From<&str> for WebhookType { + fn from(value: &str) -> Self { + match value { + "pull_request" => WebhookType::PullRequest, + "issues" => WebhookType::Issues, + _ => WebhookType::Unknown(value.to_string()), + } + } +} + +impl std::fmt::Display for GithubWebhookEvent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "GitHub Webhook Event: {:?}", self._type) + } +} + +#[async_trait] +impl EventBase for GithubWebhookEvent { + async fn process(&self) { + tracing::info!("Processing: [{}]", &self); + tracing::info!("Payload: {:#?}", &self.payload); + } +} + +impl GithubWebhookEvent { + // Create and enqueue this event. + pub fn notify(_type: WebhookType, payload: Value) { + get_mq().send(EventType::GithubWebhook(GithubWebhookEvent { + _type, + payload, + })); + } +} + +// For storing the data into database. +impl From for Value { + fn from(value: GithubWebhookEvent) -> Self { + serde_json::to_value(value).unwrap() + } +} + +impl TryFrom for GithubWebhookEvent { + type Error = crate::event::Error; + + fn try_from(value: Value) -> Result { + let res: GithubWebhookEvent = serde_json::from_value(value)?; + Ok(res) + } +} \ No newline at end of file diff --git a/taurus/src/event/mod.rs b/taurus/src/event/mod.rs index 455b555a7..8cb3cf688 100644 --- a/taurus/src/event/mod.rs +++ b/taurus/src/event/mod.rs @@ -7,13 +7,16 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_json::Value; use thiserror::Error; +use github_webhook::GithubWebhookEvent; pub mod api_request; +pub mod github_webhook; #[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, Serialize, Deserialize)] pub enum EventType { ApiRequest(ApiRequestEvent), + GithubWebhook(GithubWebhookEvent), // Reserved ErrorEvent, @@ -54,6 +57,8 @@ impl EventType { EventType::ApiRequest(evt) => evt.process().await, // EventType::SomeOtherEvent(xxx) => xxx.process().await, + EventType::GithubWebhook(evt) => evt.process().await, + // This won't happen unless failed to load events from database. // And that's because of a event conversion error. // You should recheck yout conversion code logic.