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