Skip to content
Snippets Groups Projects
Commit 542150e5 authored by Éloïs's avatar Éloïs
Browse files

initial commit

parents
No related branches found
No related tags found
No related merge requests found
/target
**/*.rs.bk
stages:
- build
rust-latest:
stage: build
image: rust:latest
script:
- cargo build --verbose
- cargo test --verbose
rust-nightly:
stage: build
image: rustlang/rust:nightly
script:
- cargo build --verbose
- cargo test --verbose
nix:
stage: build
image: nixos/nix:latest
script:
- nix-build
This diff is collapsed.
[package]
name = "gitbot"
version = "0.0.1"
description = "Gitlab webhook to XMPP MUC bot"
authors = ["librelois <elois@duniter.org>"]
edition = "2018"
license = "AGPL-3.0"
[dependencies]
futures = "0.1"
gitlab = "0.1312.0"
hyper = "0.12"
log = "0.4"
parking_lot = "0.11.1"
pretty_env_logger = "0.3"
serde_json = "1.0"
structopt = "0.3"
tokio = "0.1"
xmpp-parsers = "0.15"
xmpp = "0.3.0"
This diff is collapsed.
use futures::{
future::FutureResult,
sync::mpsc::{self, UnboundedSender},
Stream,
};
use gitlab::webhooks::{IssueAction, MergeRequestAction, WebHook, WikiPageAction};
use hyper::{
header::HeaderValue, rt::Future, service::service_fn, Body, Request, Response, Server,
};
use log::{debug, error, info, trace, warn};
use parking_lot::Mutex;
use std::{
net::{IpAddr, SocketAddr},
str::FromStr,
sync::Arc,
};
use structopt::StructOpt;
use tokio::runtime::current_thread::Runtime;
use xmpp::{Agent as XMPPAgent, ClientBuilder, ClientType, Event};
use xmpp_parsers::{message::MessageType, BareJid, Jid};
mod webhook;
mod webserver;
mod xmpp_bot;
#[derive(StructOpt, Debug)]
#[structopt(name = "Webhook to XMPP MUC bot")]
struct Opt {
/// Jabber-ID (bare or full)
#[structopt(short, long)]
jid: String,
/// Jabber password
#[structopt(short, long, env("GITBOT_XMPP_PASSWORD"))]
password: String,
/// Full MUC Jabber-Id, ie. room@conference.example.com/BotNickName
#[structopt(short, long)]
muc: String,
/// Listen on 0.0.0.0 (IPv4) for HTTP instead of :: (IPv6)
#[structopt(long, default_value = "0.0.0.0")]
ip: IpAddr,
/// HTTP listening port
#[structopt(short = "P", long, default_value = "8080")]
port: u16,
/// Gitlab token ("X-Gitlab-Token")
#[structopt(short = "H", long = "header", env("GITLAB_TOKEN"))]
gitlab_token: String,
}
fn main() -> std::io::Result<()> {
let opt = Opt::from_args();
let jid = opt.jid;
let password = opt.password;
let muc = opt.muc;
let ip = opt.ip;
let port = opt.port;
let gitlab_token = opt.gitlab_token;
pretty_env_logger::init();
let (sender, receiver) = mpsc::unbounded();
let srv = webserver::start(SocketAddr::new(ip, port), gitlab_token, sender);
let (client, mut agent) = xmpp_bot::XMPPBot::new(&jid, &password, &muc);
let forwarder = receiver.for_each(|wh| {
if let Some(text) = webhook::format_webhook(&wh) {
agent.send_room_text(text);
} else {
warn!("Unhandled webhook payload: {:?}", wh);
}
Ok(())
});
// Error type is ()
let _ = Runtime::new()?.block_on(client.join3(srv, forwarder).map(|_| ()));
Ok(())
}
use crate::*;
pub(super) fn format_webhook(wh: &WebHook) -> Option<String> {
Some(match wh {
WebHook::Build(build) => {
println!("Build: {:?}", build);
return None;
}
WebHook::Issue(issue) => {
// Error("invalid value: web hook, expected
// Error(\"invalid value: hook date, expected
// ParseError(Invalid)\", line: 0, column: 0)", line: 0,
// column: 0)
let action = match issue.object_attributes.action {
Some(IssueAction::Update) => "updated",
Some(IssueAction::Open) => "opened",
Some(IssueAction::Close) => "closed",
Some(IssueAction::Reopen) => "reopened",
None => return None,
};
format!(
"{} {} issue {} in {}: {}{}",
issue.user.name,
action,
issue.object_attributes.iid,
issue.project.name,
issue.object_attributes.title,
issue
.object_attributes
.url
.as_ref()
.map(|url| format!(" <{}>", url))
.unwrap_or_default()
)
}
WebHook::MergeRequest(merge_req) => {
let action = match merge_req.object_attributes.action {
Some(MergeRequestAction::Approved) => "approved",
Some(MergeRequestAction::Update) => "updated",
Some(MergeRequestAction::Open) => "opened",
Some(MergeRequestAction::Close) => "closed",
Some(MergeRequestAction::Merge) => "merged",
Some(MergeRequestAction::Reopen) => "reopened",
Some(MergeRequestAction::Unapproved) => "unapproved",
None => return None,
};
format!(
"{} {} merge request {} in {}: {}{}",
merge_req.user.name,
action,
merge_req.object_attributes.iid,
merge_req.project.name,
merge_req.object_attributes.title,
merge_req
.object_attributes
.url
.as_ref()
.map(|url| format!(" <{}>", url))
.unwrap_or_default()
)
}
WebHook::Note(note) => {
println!("Note: {:?}", note);
return None;
}
WebHook::Pipeline(_pipeline) => {
// TODO create text for pipeline
return None;
}
WebHook::Push(push) => {
let mut text = format!(
"{} pushed {} commits to {} branch {}",
push.user_name,
push.commits.len(),
push.project.name,
push.ref_
);
for commit in &push.commits {
if let Some(subject) = commit.message.lines().next() {
text = format!("{}\n• {} <{}>", text, subject, commit.url);
}
}
text
}
WebHook::WikiPage(page) => {
let action = match page.object_attributes.action {
WikiPageAction::Update => "updated",
WikiPageAction::Create => "created",
};
format!(
"{} {} {} wiki page {} <{}>",
page.user.name,
action,
page.project.name,
page.object_attributes.title,
page.object_attributes.url,
)
}
})
}
use crate::*;
static OK_STR: &[u8] = b"Ok";
#[allow(clippy::clippy::unnecessary_wraps)]
fn error_res<E: std::fmt::Debug, X>(e: E) -> Result<Response<Body>, X> {
error!("error response: {:?}", e);
let text = format!("{:?}", e);
let res = Response::builder()
.status(400)
.body(Body::from(Vec::from(text.as_bytes())))
.unwrap();
Ok(res)
}
fn verify_gitlab_token(gitlab_token: &str, req: &Request<Body>) -> bool {
if let Some(header_value) = req.headers().get("X-Gitlab-Token") {
if let Ok(header_value_str) = header_value.to_str() {
header_value_str == gitlab_token
} else {
false
}
} else {
false
}
}
pub fn start(
addr: SocketAddr,
gitlab_token: String,
value_tx: UnboundedSender<WebHook>,
) -> Box<dyn Future<Item = (), Error = ()> + Send> {
let value_tx = Arc::new(Mutex::new(value_tx));
let service = move || {
let value_tx = value_tx.clone();
let gitlab_token = gitlab_token.clone();
service_fn(move |req: Request<Body>| {
let mime_json = HeaderValue::from_static(&"application/json");
debug!("request: {} {}", req.method(), req.uri());
let is_json = req.headers().get("content-type") == Some(&mime_json);
let headers_ok = verify_gitlab_token(&gitlab_token, &req);
let value_tx = value_tx.clone();
req.into_body().concat2().and_then(move |body| {
let res = if !headers_ok {
Ok(Response::builder()
.status(400)
.body(Body::from(Vec::from(&b"Required headers are missing"[..])))
.unwrap())
} else if !is_json {
Ok(Response::builder()
.status(400)
.body(Body::from(Vec::from(&b"No JSON submitted"[..])))
.unwrap())
} else {
std::str::from_utf8(&*body)
.and_then(|body| {
serde_json::from_str(body)
.map(|wh: WebHook| {
trace!("body: {:?}", body);
value_tx.lock().unbounded_send(wh).unwrap();
Response::new(Body::from(OK_STR))
})
.or_else(error_res)
})
.or_else(error_res)
};
FutureResult::from(res)
})
})
};
let srv = Server::bind(&addr)
.serve(service)
.map_err(|e| error!("http server error: {}", e));
info!("Listening on http://{}", addr);
Box::new(srv)
}
use crate::*;
#[derive(Clone)]
#[allow(clippy::clippy::upper_case_acronyms)]
pub struct XMPPBot {
agent: XMPPAgent,
muc_jid: BareJid,
}
impl XMPPBot {
pub fn new<'a>(
jid: &'a str,
password: &'a str,
muc_jid: &'a str,
) -> (Box<dyn Future<Item = (), Error = ()> + 'a>, Self) {
let muc_jid: BareJid = match BareJid::from_str(muc_jid) {
Ok(jid) => jid,
Err(err) => panic!("MUC Jid invalid: {:?}", err),
};
let (agent, stream) = ClientBuilder::new(jid, password)
.set_client(ClientType::Bot, "gitbot")
.set_website("https://git.duniter.org/tools/gitbot")
.set_default_nick("gitbot")
.build()
.unwrap();
let handler = {
let mut agent = agent.clone();
let muc_jid = muc_jid.clone();
let jid = jid;
stream
.map_err(|e| {
log::error!("XMPP ERROR: {}", e);
})
.for_each(move |evt: Event| {
match evt {
Event::Online => {
info!("XMPP client now online at {}", jid);
agent.join_room(
muc_jid.clone(),
None,
None,
"en",
"Your friendly hook bot.",
);
}
Event::Disconnected => {
info!("XMPP client disconnected");
}
Event::RoomJoined(jid) => {
info!("Entered MUC {}", jid);
}
Event::RoomLeft(jid) => {
info!("Left MUC {}", jid);
}
_ => (),
}
Ok(())
})
.map(|_| ())
};
(Box::new(handler), XMPPBot { agent, muc_jid })
}
pub fn send_room_text(&mut self, text: String) {
self.agent.send_message(
Jid::Bare(self.muc_jid.clone()),
MessageType::Groupchat,
"en",
&text,
);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment