diff --git a/Cargo.toml b/Cargo.toml index 288bc61..0ffe8fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ license = "AGPL-3.0+" [dependencies] clap = { version = "4.5", features = [ "cargo" ] } forgejo-hooks = "*" -gitlab = "0.1706" +gitlab = { version = "0.1706", optional = true } hyper = { version = "1.6", default-features = false, features = [ "http1", "server" ] } hyper-util = { version = "0.1", features = [ "tokio" ] } http-body-util = "0.1" @@ -19,16 +19,21 @@ tokio = { version = "1", default-features = false, features = [ "rt", "net", "sy pretty_env_logger = "0.5" serde = { version = "1.0", features = [ "derive" ] } serde_json = "1.0" -toml = "0.9" -xmpp = { git = "https://gitlab.com/xmpp-rs/xmpp-rs", branch = "xmpp-join-resync", default-features = false, features = [ "serde", "starttls", "rustls-native-certs", "aws_lc_rs" ] } -hmac = "0.12" -sha2 = "0.10" +toml = "1.1" +xmpp = { git = "https://gitlab.com/xmppftw/xmpp-rs", branch = "feat-openssl-vendored", default-features = false, features = [ "serde", "starttls", "rustls-native-certs", "aws_lc_rs" ] } +hmac = "0.13" +sha2 = "0.11" hex = "0.4" camino = { version = "1.2", features = ["serde1"] } chrono = { version = "0.4", default-features = false } +[build-dependencies] +git2 = "0.20" + [patch.crates-io] forgejo-hooks = { path = "forgejo-hooks" } [features] +gitlab = [ "dep:gitlab" ] syntax-highlighting = ["xmpp/syntax-highlighting"] +vendored-openssl = [ "xmpp/vendored-openssl", "git2/vendored-openssl" ] diff --git a/README.md b/README.md index 5109c74..b3ba21b 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,12 @@ be somewhat of a testbed for xmpp-rs features. Originally based on tokio-webhook2muc. +# Cross-compile musl + +``` +cross build --release --target x86_64-unknown-linux-musl --features vendored-openssl +``` + ## License AGPL-3.0-or-later. See the LICENSE file. diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..da0edcf --- /dev/null +++ b/build.rs @@ -0,0 +1,41 @@ +// Copyright (C) 2026-2099 The crate authors. +// +// This program is free software: you can redistribute it and/or modify it +// under the terms of the GNU Affero 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 Affero General Public License +// for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use std::env; +use std::fs; +use std::path::Path; + +use git2::Repository; + +fn main() { + let oid = { + match Repository::open(".") { + Ok(repo) => { + let head = repo.head().unwrap(); + format!("{}", repo.refname_to_id(head.name().unwrap()).unwrap()) + } + Err(e) => { + println!("cargo::warning={}", e); + String::new() + } + } + }; + + let out_dir = env::var_os("OUT_DIR").unwrap(); + let dest_path = Path::new(&out_dir).join("GIT_COMMIT"); + fs::write(&dest_path, oid).unwrap(); + + println!("cargo::rerun-if-changed=build.rs"); +} diff --git a/config.sample.toml b/config.sample.toml new file mode 100644 index 0000000..5fed1de --- /dev/null +++ b/config.sample.toml @@ -0,0 +1,8 @@ +admins = [ "foo@localhost" ] +jid = "bar@localhost" +password = "bonjourxmpp" + +rooms = [ "room@muc.localhost" ] +secret = "bonjourforgejo" + +addr = "[::1]:5225" diff --git a/src/bot.rs b/src/bot.rs index deec1ff..4d09ffa 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -14,6 +14,7 @@ // along with this program. If not, see . use crate::Error; +use crate::config::Config; use crate::hooks::{Hook, IssueAction, MergeRequestAction, format_hook}; use chrono::{TimeDelta, Utc}; @@ -23,34 +24,28 @@ use xmpp::jid::{BareJid, Jid, ResourcePart}; use xmpp::parsers::message::MessageType; use xmpp::{ Agent, ClientBuilder, ClientFeature, ClientType, Config as AgentConfig, Event, RoomNick, - message::send::RawMessageSettings, muc::room::JoinRoomSettings, + message::send::{MessageSettings, RawMessageSettings}, + muc::room::JoinRoomSettings, }; pub struct XmppClient { is_online: bool, agent: Agent, - rooms: Vec, nickname: ResourcePart, - admins: Vec, + config: Config, /// Keep around messages we've sent recently so we're able to prevent spamming the same type of /// messages recent_hooks: Vec, } impl XmppClient { - pub fn new( - jid: BareJid, - password: &str, - rooms: Vec, - nickname: ResourcePart, - admins: Vec, - ) -> XmppClient { - let config = AgentConfig { + pub fn new(jid: BareJid, password: &str, nickname: ResourcePart, config: Config) -> XmppClient { + let agent_config = AgentConfig { bookmarks_autojoin: false, ..AgentConfig::default() }; let agent = ClientBuilder::new(jid, password) - .set_config(config) + .set_config(agent_config) .set_client(ClientType::Bot, "xmpp-rs") .set_website("https://gitlab.com/xmpp-rs/xmpp-rs") .set_default_nick(&nickname) @@ -60,9 +55,8 @@ impl XmppClient { XmppClient { is_online: false, agent, - rooms, nickname, - admins, + config, recent_hooks: Vec::new(), } } @@ -73,7 +67,7 @@ impl XmppClient { Event::Online => { self.is_online = true; debug!("XMPP Online"); - for room in &self.rooms { + for room in &self.config.rooms { self.agent .join_room(JoinRoomSettings { nick: Some(RoomNick::from_resource_ref(self.nickname.as_ref())), @@ -83,20 +77,43 @@ impl XmppClient { } } Event::ChatMessage(_id, bare, message, _timeinfo) => { - if !self.admins.contains(&bare) { + if !self.config.admins.contains(&bare) { debug!("Received chat message from {}, not in admins", bare); continue; } debug!("Received chat message from {}: {}", bare, message); - if message == "rejoin" { - for room in &self.rooms { + match message.as_str() { + "rejoin" => { + for room in &self.config.rooms { + self.agent + .join_room(JoinRoomSettings { + nick: Some(RoomNick::from_resource_ref( + self.nickname.as_ref(), + )), + force_resync: true, + ..JoinRoomSettings::new(room.clone()) + }) + .await + } + } + "version" => { self.agent - .join_room(JoinRoomSettings { - nick: Some(RoomNick::from_resource_ref(self.nickname.as_ref())), - force_resync: true, - ..JoinRoomSettings::new(room.clone()) + .send_message(MessageSettings { + recipient: bare, + message: self.config.version.unwrap_or("No version specified."), + lang: Some("en"), + }) + .await + } + _ => { + let message = "Help:\nrejoin: Re-join all rooms.\nversion: Display software version."; + self.agent + .send_message(MessageSettings { + recipient: bare, + message, + lang: Some("en"), }) .await } @@ -209,7 +226,7 @@ impl XmppClient { } if let Some(display) = format_hook(&wh) { debug!("Hook: {}", display); - for room in &self.rooms { + for room in &self.config.rooms { self.agent .send_raw_message(RawMessageSettings { recipient: Jid::from(room.clone()), diff --git a/src/config.rs b/src/config.rs index 81a8e82..f5b7f5c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -23,7 +23,15 @@ use xmpp::jid::{BareJid, ResourcePart}; use crate::error::Error; -#[derive(Debug, Serialize, Deserialize)] +const MAYBE_VERSION: &'static str = include_str!(concat!(env!("OUT_DIR"), "/GIT_COMMIT")); +const fn version() -> Option<&'static str> { + match MAYBE_VERSION { + v if v.is_empty() => None, + v => Some(v), + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { /// Accounts trusted for admin tasks #[serde(default = "Vec::new")] @@ -50,6 +58,11 @@ pub struct Config { /// HTTP Webhook listening address and port, e.g., 127.0.0.1:1234 or [::1]:1234 #[serde(default = "Config::default_addr")] pub addr: SocketAddr, + + /// Software version + #[serde(skip)] + #[serde(default = "version")] + pub version: Option<&'static str>, } impl Config { diff --git a/src/hooks/forgejo.rs b/src/hooks/forgejo.rs index 9a32686..47b9bad 100644 --- a/src/hooks/forgejo.rs +++ b/src/hooks/forgejo.rs @@ -38,7 +38,11 @@ impl From for Issue { action: Some(other.action.into()), title: other.issue.title, author: User { - name: other.issue.user.login, + name: other + .comment + .as_ref() + .map(|c| c.user.login.clone()) + .unwrap_or(other.issue.user.login), }, repository: Repository { name: other.repository.name, @@ -60,7 +64,11 @@ impl From for Note { merge_request: None, snippet: false, author: User { - name: other.issue.user.login, + name: other + .comment + .as_ref() + .map(|c| c.user.login.clone()) + .unwrap_or(other.issue.user.login), }, repository: Repository { name: other.repository.name, @@ -69,6 +77,7 @@ impl From for Note { .comment .map(|c| c.html_url) .unwrap_or(other.issue.html_url), + is_update: other.action == FjIssueAction::Edited, } } } diff --git a/src/hooks/mod.rs b/src/hooks/mod.rs index 8fcd544..3a2fce3 100644 --- a/src/hooks/mod.rs +++ b/src/hooks/mod.rs @@ -14,11 +14,12 @@ // along with this program. If not, see . mod forgejo; +#[cfg(feature = "gitlab")] mod gitlab; mod types; pub use crate::hooks::forgejo::ForgejoHook; -#[allow(deprecated)] +#[cfg(feature = "gitlab")] pub use crate::hooks::gitlab::GitlabHook; pub use crate::hooks::types::Hook; pub use crate::hooks::types::{IssueAction, MergeRequestAction, WikiAction}; @@ -108,19 +109,26 @@ pub fn format_hook(hook: &Hook) -> Option { if note.snippet { return None; } + + let updated_fmt = if note.is_update { + "updated comment" + } else { + "commented" + }; + if let Some(commit) = ¬e.commit { format!( - "[{}] {} commented on commit {:?} <{}>", + "[{}] {} {updated_fmt} on commit {:?} <{}>", note.repository.name, note.author.name, commit.ref_, commit.url, ) } else if let Some(issue) = ¬e.issue { format!( - "[{}] {} commented on issue {}: {} <{}>", + "[{}] {} {updated_fmt} on issue {}: {} <{}>", note.repository.name, note.author.name, issue.id, issue.title, note.url, ) } else if let Some(mr) = ¬e.merge_request { format!( - "[{}] {} commented on merge request {}: {} <{}>", + "[{}] {} {updated_fmt} on merge request {}: {} <{}>", note.repository.name, note.author.name, mr.id, mr.title, note.url, ) } else { diff --git a/src/hooks/types.rs b/src/hooks/types.rs index 6525fe2..b828ebe 100644 --- a/src/hooks/types.rs +++ b/src/hooks/types.rs @@ -116,6 +116,7 @@ pub struct Note { pub repository: Repository, pub author: User, pub url: String, + pub is_update: bool, } #[derive(Debug, Clone, PartialEq)] diff --git a/src/main.rs b/src/main.rs index e9c22d6..8f2f0a3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -56,11 +56,10 @@ async fn main() -> Result<(), Error> { let (value_tx, value_rx) = mpsc::unbounded_channel::(); let mut bot = XmppClient::new( - config.jid, + config.jid.clone(), config.password.as_str(), - config.rooms, - config.nickname, - config.admins, + config.nickname.clone(), + config.clone(), ); let xmpp_handle = tokio::task::spawn(async move { diff --git a/src/web.rs b/src/web.rs index 50fbc0d..3dc01f2 100644 --- a/src/web.rs +++ b/src/web.rs @@ -14,7 +14,7 @@ // along with this program. If not, see . use crate::error::Error; -#[allow(deprecated)] +#[cfg(feature = "gitlab")] use crate::hooks::GitlabHook; use crate::hooks::{ForgejoHook, Hook}; @@ -22,7 +22,7 @@ use std::convert::Infallible; use std::io::Read; use bytes::{Buf, Bytes}; -use hmac::{Hmac, Mac}; +use hmac::{Hmac, KeyInit, Mac}; use http_body_util::{BodyExt, Full}; use hyper::{Method, Request, Response, body::Incoming, header}; use log::{debug, error, trace}; @@ -62,6 +62,7 @@ async fn hooks_inner(req: Request, secret: &str) -> Result