For push events, publish one message per commit pushed, up to a limit of 15.
241 lines
8.0 KiB
Rust
241 lines
8.0 KiB
Rust
#[macro_use(anyhow)]
|
|
extern crate anyhow;
|
|
#[macro_use]
|
|
extern crate log;
|
|
#[macro_use]
|
|
extern crate serde;
|
|
|
|
mod config;
|
|
mod event;
|
|
|
|
use std::{env, net::IpAddr, process::exit, sync::Arc, time::Duration};
|
|
|
|
use anyhow::Context;
|
|
use constant_time_eq::constant_time_eq;
|
|
use event::{GitlabEvent, GitlabEventExt};
|
|
use http::StatusCode;
|
|
use matrix_sdk::{
|
|
config::SyncSettings,
|
|
room::Joined,
|
|
ruma::{events::room::message::RoomMessageEventContent, OwnedRoomOrAliasId},
|
|
BaseRoom, Client,
|
|
};
|
|
use tokio::sync::mpsc;
|
|
use warp::Filter;
|
|
|
|
use crate::event::{MergeRequestAction, PipelineStatus};
|
|
|
|
async fn build_sync_settings(matrix_client: &Client) -> SyncSettings {
|
|
let mut settings = SyncSettings::default().timeout(Duration::from_secs(30));
|
|
if let Some(token) = matrix_client.sync_token().await {
|
|
settings = settings.token(token);
|
|
}
|
|
settings
|
|
}
|
|
|
|
async fn matrix_connect(config: &config::Config) -> anyhow::Result<Client> {
|
|
let client = Client::builder()
|
|
.server_name(config.user_id.server_name())
|
|
.user_agent(format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")))
|
|
.build()
|
|
.await?;
|
|
client
|
|
.login_username(&config.user_id, &config.password)
|
|
.initial_device_display_name("Bebot")
|
|
.send()
|
|
.await?;
|
|
info!("Connected to matrix as {}; waiting for first sync", config.user_id);
|
|
|
|
let settings = build_sync_settings(&client).await;
|
|
client.sync_once(settings).await?;
|
|
info!("First matrix sync complete");
|
|
|
|
let sync_client = client.clone();
|
|
tokio::spawn(async move {
|
|
let settings = build_sync_settings(&sync_client).await;
|
|
if let Err(err) = sync_client.sync(settings).await {
|
|
error!("Matrix sync failed: {}", err);
|
|
exit(1);
|
|
}
|
|
});
|
|
|
|
Ok(client)
|
|
}
|
|
|
|
async fn ensure_matrix_room_joined(matrix_client: &Client, room_id: &OwnedRoomOrAliasId) -> anyhow::Result<Joined> {
|
|
fn room_matches(a_room: &BaseRoom, our_room: &OwnedRoomOrAliasId) -> bool {
|
|
let our_room_str = our_room.as_str();
|
|
a_room.room_id().as_str() == our_room_str
|
|
|| a_room
|
|
.canonical_alias()
|
|
.iter()
|
|
.find(|alias| alias.as_str() == our_room_str)
|
|
.is_some()
|
|
|| a_room
|
|
.alt_aliases()
|
|
.iter()
|
|
.find(|alias| alias.as_str() == our_room_str)
|
|
.is_some()
|
|
}
|
|
|
|
let mut room = matrix_client
|
|
.joined_rooms()
|
|
.iter()
|
|
.find(|a_room| room_matches(*a_room, room_id))
|
|
.map(|room| room.clone());
|
|
if room.is_none() {
|
|
if let Some(invited) = matrix_client
|
|
.invited_rooms()
|
|
.iter()
|
|
.find(|a_room| room_matches(*a_room, room_id))
|
|
{
|
|
invited.accept_invitation().await?;
|
|
} else {
|
|
matrix_client.join_room_by_id_or_alias(room_id, &[]).await?;
|
|
}
|
|
let settings = build_sync_settings(&matrix_client).await;
|
|
matrix_client.sync_once(settings).await?;
|
|
room = matrix_client
|
|
.joined_rooms()
|
|
.iter()
|
|
.find(|a_room| room_matches(*a_room, room_id))
|
|
.map(|room| room.clone());
|
|
}
|
|
|
|
room.ok_or_else(|| anyhow!("Unable to join room {}", room_id))
|
|
}
|
|
|
|
fn build_gitlab_messages(event: &GitlabEvent) -> Vec<String> {
|
|
let project = event.project();
|
|
let refname = event::parse_ref(event.r#ref());
|
|
event
|
|
.titles()
|
|
.iter()
|
|
.map(|title| {
|
|
format!(
|
|
"\\[{}\\] `{}` *{}* {}",
|
|
project.path_with_namespace,
|
|
refname,
|
|
event.user(),
|
|
title,
|
|
)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
async fn handle_gitlab_event(
|
|
event: GitlabEvent,
|
|
room_id: &OwnedRoomOrAliasId,
|
|
matrix_client: &Client,
|
|
) -> anyhow::Result<()> {
|
|
if let GitlabEvent::MergeRequest { object_attributes, .. } = &event {
|
|
if object_attributes.action == MergeRequestAction::Other {
|
|
return Ok(());
|
|
}
|
|
} else if let GitlabEvent::Pipeline { object_attributes, .. } = &event {
|
|
if object_attributes.status == PipelineStatus::Other {
|
|
return Ok(());
|
|
}
|
|
}
|
|
|
|
let room = ensure_matrix_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, None).await?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
async fn run() -> anyhow::Result<()> {
|
|
let config_path = env::args()
|
|
.nth(1)
|
|
.ok_or_else(|| anyhow!("Config file should be passed as only parameter"))?;
|
|
let config = Arc::new(config::load(config_path).await?);
|
|
let addr = config
|
|
.bind_address
|
|
.as_ref()
|
|
.map(|ba| ba.clone())
|
|
.unwrap_or_else(|| "127.0.0.1".to_string())
|
|
.parse::<IpAddr>()
|
|
.context("Failed to parse bind_address")?;
|
|
let port = config.bind_port.unwrap_or(3000);
|
|
|
|
let matrix_client = matrix_connect(&config).await.context("Failed to connect to Matrix")?;
|
|
|
|
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 gitlab = gitlab_root_path
|
|
.and(warp::path!("hooks" / "gitlab"))
|
|
.and(warp::post())
|
|
.and(warp::header::<String>("x-gitlab-token"))
|
|
.and(warp::body::json())
|
|
.then(move |token: String, event: event::GitlabEvent| {
|
|
let config = Arc::clone(&config);
|
|
let event_tx = event_tx.clone();
|
|
|
|
async move {
|
|
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()) {
|
|
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 '{}' unconfigured", config_key);
|
|
warp::reply::with_status("Repo not configured", StatusCode::NOT_FOUND)
|
|
}
|
|
}
|
|
});
|
|
|
|
let routes = gitlab.with(warp::log("bebot"));
|
|
|
|
warp::serve(routes).run((addr, port)).await;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() {
|
|
let lenv = env_logger::Env::new()
|
|
.filter("BEBOT_LOG")
|
|
.write_style("BEBOT_LOG_STYLE");
|
|
env_logger::init_from_env(lenv);
|
|
|
|
if let Err(err) = run().await {
|
|
error!("{}", err);
|
|
exit(1);
|
|
}
|
|
}
|