sourcehut-mirror-bridge/src/main.rs

140 lines
4.1 KiB
Rust

use anyhow::{anyhow, Result};
use gitea::GiteaAPI;
use log::{debug, error, info};
use serde::Deserialize;
use std::{collections::HashMap, process::exit, sync::Arc};
use tiny_http::{Request, Response, Server};
use crate::sourcehut::SourcehutAPI;
mod gitea;
mod sourcehut;
fn default_bind() -> String {
"0.0.0.0:8000".to_string()
}
fn default_sourcehut_api() -> String {
"https://git.sr.ht/query".to_string()
}
#[derive(Deserialize, Debug)]
struct Config {
/// Bind address
#[serde(default = "default_bind")]
bind: String,
/// Sourcehut API endpoint
#[serde(default = "default_sourcehut_api")]
sourcehut_api: String,
/// Sourcehut API token (from https://meta.sr.ht/oauth2, needs `git.sr.ht/REPOSITORY` grant)
#[serde(default = "default_sourcehut_api")]
sourcehut_token: String,
/// Base URL for Gitea API (without the /v1)
gitea_api_base: String,
/// username:password for basic auth
gitea_auth: String,
/// URL to set as sourcehut webhook target where this service is reacheable
url: String,
/// Comma separated list of repository map from sourcehut to Gitea e.g. "sourcehutrepo:owner/gitearepo"
repositories: String,
}
fn main() -> Result<()> {
let _ = dotenvy::dotenv();
env_logger::init();
let config = envy::prefixed("MIRROR_").from_env::<Config>()?;
// Parse repository mapping into a hashmap
let repository_map: HashMap<&str, &str> = config
.repositories
.split(",")
.map(|s| s.split_once(":").unwrap())
.collect();
if repository_map.is_empty() {
return Err(anyhow!("No repositories provided"));
}
debug!("Repository map: {:?}", repository_map);
// Set up Gitea API and check that provided repositories exist
let gitea = GiteaAPI::new(&config.gitea_api_base, &config.gitea_auth);
let repositories = repository_map.values().map(|&s| s).collect::<Vec<_>>();
gitea.check_repositories(&repositories)?;
// Set up webhooks via GQL API to sourcehut
let mut srht = SourcehutAPI::new(&config.sourcehut_api, &config.sourcehut_token);
srht.init_webhook(&config.url)?;
let api = Arc::new(srht);
let r = api.clone();
// Set up termination handler to delete webhook
ctrlc::set_handler(move || {
_ = r.delete_webhook();
exit(0)
})
.expect("Error setting termination handler");
// Listen for webhooks and handle them
let server = Server::http(config.bind).map_err(|err| anyhow!(err))?;
for request in server.incoming_requests() {
debug!("Received request from {:?}", request.remote_addr());
match handle_request(request) {
Ok(Some(repository)) => {
// Get mapped repository, if it exists
if let Some(mapped) = repository_map.get(repository.as_str()) {
info!("Syncing repository {}", mapped);
if let Err(err) = gitea.sync(&mapped) {
error!("Error syncing repository: {}", err);
}
}
}
Err(err) => {
error!("Error handling webhook: {}", err);
}
_ => {
// Invalid request, ignore
}
}
}
Ok(())
}
fn handle_request(mut request: Request) -> Result<Option<String>> {
// Make sure the content type is JSON
let is_json = request
.headers()
.iter()
.find(|h| h.field.as_str() == "Content-Type" && h.value == "application/json")
.ok_or_else(|| anyhow!("Invalid Content-Type header"));
match is_json {
Ok(_) => {
let mut content = String::new();
request.as_reader().read_to_string(&mut content)?;
let repository = SourcehutAPI::read_webhook_payload(&content)?;
request.respond(Response::empty(204))?;
return Ok(Some(repository.name));
}
Err(err) => {
debug!(
"Rejecting request because of invalid Content-Type header: {}",
err
);
request.respond(Response::from_string(err.to_string()).with_status_code(400))?
}
}
Ok(None)
}