From ec9571a182598291a95357778870c8216b5cd7d8 Mon Sep 17 00:00:00 2001 From: Paul Campbell Date: Mon, 15 Apr 2024 22:44:56 +0100 Subject: [PATCH] feat(repo/webhook): Replace webhook if it already exists Closes kemitix/git-next#45 --- src/server/actors/repo/webhook.rs | 64 +++++++++++++++++++++++++++---- src/server/config.rs | 1 + 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/src/server/actors/repo/webhook.rs b/src/server/actors/repo/webhook.rs index 8d4677d..a0b2274 100644 --- a/src/server/actors/repo/webhook.rs +++ b/src/server/actors/repo/webhook.rs @@ -3,14 +3,14 @@ use kxio::network::{self, json}; use tracing::{debug, info, warn}; use ulid::DecodeError; -use std::{fmt::Display, ops::Deref, str::FromStr}; +use std::{collections::HashMap, fmt::Display, ops::Deref, str::FromStr}; use crate::server::{ actors::{ repo::{RepoActor, ValidateRepo, WebhookRegistered}, webhook::WebhookMessage, }, - config::{RepoBranches, Webhook}, + config::{RepoBranches, Webhook, WebhookUrl}, forge, }; @@ -75,14 +75,14 @@ pub async fn unregister( url, network::NetRequestHeaders::new(), network::RequestBody::None, - network::ResponseType::Json, + network::ResponseType::None, None, network::NetRequestLogging::None, ); let result = net.delete(request).await; match result { Ok(_) => info!(?webhook_id, "unregistered webhook"), - Err(_) => warn!(?webhook_id, "Failed to unregister webhook"), + Err(err) => warn!(?webhook_id, ?err, "Failed to unregister webhook"), } } @@ -92,9 +92,17 @@ pub async fn register( addr: actix::prelude::Addr, net: network::Network, ) { - let Some(repo_config) = repo_details.config else { + let Some(repo_config) = repo_details.config.clone() else { return; }; + + let webhook_url = webhook.url(); + // remove any lingering webhooks for the same URL + let existing_webhook_ids = find_existing_webhooks(&repo_details, &webhook_url, &net).await; + for webhook_id in existing_webhook_ids { + unregister(webhook_id, repo_details.clone(), net.clone()).await; + } + info!("Registering webhook"); let hostname = &repo_details.forge.hostname; let path = repo_details.repo; @@ -103,8 +111,7 @@ pub async fn register( let url = network::NetUrl::new(format!( "https://{hostname}/api/v1/repos/{path}/hooks?token={token}" )); - let webhook_url = webhook.url(); - let repo_alias = repo_details.name; + let repo_alias = &repo_details.name; let headers = network::NetRequestHeaders::new().with("Content-Type", "application/json"); let authorisation = WebhookAuth::generate(); let body = json!({ @@ -139,9 +146,52 @@ pub async fn register( } } +async fn find_existing_webhooks( + repo_details: &crate::server::config::RepoDetails, + webhook_url: &WebhookUrl, + net: &network::Network, +) -> Vec { + let mut ids: Vec = vec![]; + let hostname = &repo_details.forge.hostname; + let path = &repo_details.repo; + let mut page = 1; + loop { + use secrecy::ExposeSecret; + let token = &repo_details.forge.token.expose_secret(); + let url = format!("https://{hostname}/api/v1/repos/{path}/hooks?page={page}&token={token}"); + let net_url = network::NetUrl::new(url); + let request = network::NetRequest::new( + network::RequestMethod::Get, + net_url, + network::NetRequestHeaders::new(), + network::RequestBody::None, + network::ResponseType::Json, + None, + network::NetRequestLogging::None, + ); + let result = net.get::>(request).await; + if let Ok(response) = result { + if let Some(list) = response.response_body() { + if list.is_empty() { + return ids; + } + for hook in list { + if let Some(existing_url) = hook.config.get("url") { + if existing_url.starts_with(webhook_url.as_ref()) { + ids.push(hook.id()); + } + } + } + } + } + page += 1; + } +} + #[derive(Debug, serde::Deserialize)] struct Hook { id: i64, + config: HashMap, } impl Hook { fn id(&self) -> WebhookId { diff --git a/src/server/config.rs b/src/server/config.rs index 80a174c..a357d8e 100644 --- a/src/server/config.rs +++ b/src/server/config.rs @@ -45,6 +45,7 @@ impl Webhook { } /// The URL for the webhook where forges should send their updates +#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] pub struct WebhookUrl(String); impl AsRef for WebhookUrl { fn as_ref(&self) -> &str {