// bebot // Copyright (C) 2023 Brian Tarricone // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . use std::sync::Arc; use constant_time_eq::constant_time_eq; use matrix_sdk::{ ruma::{events::room::message::RoomMessageEventContent, OwnedRoomOrAliasId}, Client, }; use tokio::sync::mpsc; use warp::{filters::BoxedFilter, http::StatusCode, reply::Reply, Filter}; use crate::{ config::GitlabWebhookConfig, gitlab_event::{parse_ref, GitlabEvent, GitlabEventExt}, matrix, }; pub fn build_gitlab_messages(event: &GitlabEvent) -> Vec { let project = event.project(); let refname = event.r#ref().map(parse_ref); event .titles() .iter() .map(|title| { format!( "\\[{}\\] {}*{}* {}", project.path_with_namespace, refname.as_ref().map(|rn| format!("`{rn}` ")).unwrap_or_default(), event.user(), title, ) }) .collect() } pub async fn handle_gitlab_event( event: GitlabEvent, room_id: &OwnedRoomOrAliasId, matrix_client: &Client, ) -> anyhow::Result<()> { let room = matrix::ensure_room_joined(matrix_client, room_id).await?; for msg in build_gitlab_messages(&event) { debug!("Sending message to {room_id}: {msg}"); let msg_content = RoomMessageEventContent::text_markdown(&msg); room.send(msg_content).await?; } Ok(()) } pub fn build_route(config: GitlabWebhookConfig, matrix_client: Client) -> anyhow::Result> { let (event_tx, mut event_rx) = mpsc::channel::<(GitlabEvent, OwnedRoomOrAliasId)>(100); tokio::spawn(async move { while let Some((event, room)) = event_rx.recv().await { if let Err(err) = handle_gitlab_event(event, &room, &matrix_client).await { warn!("Failed to handle payload: {err}"); } } }); let gitlab_root_path = if let Some(url_prefix) = config.url_prefix.as_ref() { url_prefix.split('/').fold(warp::any().boxed(), |last, segment| { if segment.is_empty() { last } else { last.and(warp::path(segment.to_string())).boxed() } }) } else { warp::any().boxed() }; let config = Arc::new(config); let gitlab = gitlab_root_path .and(warp::path!("hooks" / "gitlab")) .and(warp::post()) .and(warp::header::("x-gitlab-token")) .and(warp::body::json()) .then(move |token: String, event: GitlabEvent| { let event_tx = event_tx.clone(); let config = Arc::clone(&config); async move { match event { GitlabEvent::Other => { warp::reply::with_status("Unsupported Gitlab event type", StatusCode::BAD_REQUEST) } _ => { let project = event.project(); let config_key = project.web_url.replace("http://", "").replace("https://", ""); if let Some(repo_config) = config.repo_configs.get(&config_key) { if !constant_time_eq(token.as_bytes(), repo_config.token.as_bytes()) { warn!("Invalid token for repo '{config_key}'"); warp::reply::with_status("Invalid token", StatusCode::FORBIDDEN) } else { debug!("payload: {event:?}"); if let Some(room) = &repo_config.room.as_ref().or(config.default_room.as_ref()) { let publish_events = repo_config .publish_events .as_ref() .or(config.default_publish_events.as_ref()); if publish_events.map(|ecs| event.should_publish(ecs)).unwrap_or(true) { if let Err(err) = event_tx.send((event, (*room).clone())).await { warn!("Failed to enqueue payload: {err}"); } } warp::reply::with_status("OK", StatusCode::OK) } else { info!("Channel not configured for repo '{config_key}'"); warp::reply::with_status( "Matrix room not configured for repo", StatusCode::NOT_FOUND, ) } } } else { info!("Repo '{config_key}' unconfigured"); warp::reply::with_status("Repo not configured", StatusCode::NOT_FOUND) } } } } }) .boxed(); Ok(gitlab) }