Compare commits
11 changed files with 41 additions and 149 deletions
15
Cargo.toml
15
Cargo.toml
|
|
@ -9,7 +9,7 @@ license = "AGPL-3.0+"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4.5", features = [ "cargo" ] }
|
clap = { version = "4.5", features = [ "cargo" ] }
|
||||||
forgejo-hooks = "*"
|
forgejo-hooks = "*"
|
||||||
gitlab = { version = "0.1706", optional = true }
|
gitlab = "0.1706"
|
||||||
hyper = { version = "1.6", default-features = false, features = [ "http1", "server" ] }
|
hyper = { version = "1.6", default-features = false, features = [ "http1", "server" ] }
|
||||||
hyper-util = { version = "0.1", features = [ "tokio" ] }
|
hyper-util = { version = "0.1", features = [ "tokio" ] }
|
||||||
http-body-util = "0.1"
|
http-body-util = "0.1"
|
||||||
|
|
@ -19,21 +19,16 @@ tokio = { version = "1", default-features = false, features = [ "rt", "net", "sy
|
||||||
pretty_env_logger = "0.5"
|
pretty_env_logger = "0.5"
|
||||||
serde = { version = "1.0", features = [ "derive" ] }
|
serde = { version = "1.0", features = [ "derive" ] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
toml = "1.1"
|
toml = "0.9"
|
||||||
xmpp = { git = "https://gitlab.com/xmppftw/xmpp-rs", branch = "feat-openssl-vendored", default-features = false, features = [ "serde", "starttls", "rustls-native-certs", "aws_lc_rs" ] }
|
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.13"
|
hmac = "0.12"
|
||||||
sha2 = "0.11"
|
sha2 = "0.10"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
camino = { version = "1.2", features = ["serde1"] }
|
camino = { version = "1.2", features = ["serde1"] }
|
||||||
chrono = { version = "0.4", default-features = false }
|
chrono = { version = "0.4", default-features = false }
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
git2 = "0.20"
|
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
forgejo-hooks = { path = "forgejo-hooks" }
|
forgejo-hooks = { path = "forgejo-hooks" }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
gitlab = [ "dep:gitlab" ]
|
|
||||||
syntax-highlighting = ["xmpp/syntax-highlighting"]
|
syntax-highlighting = ["xmpp/syntax-highlighting"]
|
||||||
vendored-openssl = [ "xmpp/vendored-openssl", "git2/vendored-openssl" ]
|
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,6 @@ be somewhat of a testbed for xmpp-rs features.
|
||||||
|
|
||||||
Originally based on tokio-webhook2muc.
|
Originally based on tokio-webhook2muc.
|
||||||
|
|
||||||
# Cross-compile musl
|
|
||||||
|
|
||||||
```
|
|
||||||
cross build --release --target x86_64-unknown-linux-musl --features vendored-openssl
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
AGPL-3.0-or-later. See the LICENSE file.
|
AGPL-3.0-or-later. See the LICENSE file.
|
||||||
|
|
|
||||||
41
build.rs
41
build.rs
|
|
@ -1,41 +0,0 @@
|
||||||
// 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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
admins = [ "foo@localhost" ]
|
|
||||||
jid = "bar@localhost"
|
|
||||||
password = "bonjourxmpp"
|
|
||||||
|
|
||||||
rooms = [ "room@muc.localhost" ]
|
|
||||||
secret = "bonjourforgejo"
|
|
||||||
|
|
||||||
addr = "[::1]:5225"
|
|
||||||
57
src/bot.rs
57
src/bot.rs
|
|
@ -14,7 +14,6 @@
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
use crate::config::Config;
|
|
||||||
use crate::hooks::{Hook, IssueAction, MergeRequestAction, format_hook};
|
use crate::hooks::{Hook, IssueAction, MergeRequestAction, format_hook};
|
||||||
|
|
||||||
use chrono::{TimeDelta, Utc};
|
use chrono::{TimeDelta, Utc};
|
||||||
|
|
@ -24,28 +23,34 @@ use xmpp::jid::{BareJid, Jid, ResourcePart};
|
||||||
use xmpp::parsers::message::MessageType;
|
use xmpp::parsers::message::MessageType;
|
||||||
use xmpp::{
|
use xmpp::{
|
||||||
Agent, ClientBuilder, ClientFeature, ClientType, Config as AgentConfig, Event, RoomNick,
|
Agent, ClientBuilder, ClientFeature, ClientType, Config as AgentConfig, Event, RoomNick,
|
||||||
message::send::{MessageSettings, RawMessageSettings},
|
message::send::RawMessageSettings, muc::room::JoinRoomSettings,
|
||||||
muc::room::JoinRoomSettings,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct XmppClient {
|
pub struct XmppClient {
|
||||||
is_online: bool,
|
is_online: bool,
|
||||||
agent: Agent,
|
agent: Agent,
|
||||||
|
rooms: Vec<BareJid>,
|
||||||
nickname: ResourcePart,
|
nickname: ResourcePart,
|
||||||
config: Config,
|
admins: Vec<BareJid>,
|
||||||
/// Keep around messages we've sent recently so we're able to prevent spamming the same type of
|
/// Keep around messages we've sent recently so we're able to prevent spamming the same type of
|
||||||
/// messages
|
/// messages
|
||||||
recent_hooks: Vec<Hook>,
|
recent_hooks: Vec<Hook>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl XmppClient {
|
impl XmppClient {
|
||||||
pub fn new(jid: BareJid, password: &str, nickname: ResourcePart, config: Config) -> XmppClient {
|
pub fn new(
|
||||||
let agent_config = AgentConfig {
|
jid: BareJid,
|
||||||
|
password: &str,
|
||||||
|
rooms: Vec<BareJid>,
|
||||||
|
nickname: ResourcePart,
|
||||||
|
admins: Vec<BareJid>,
|
||||||
|
) -> XmppClient {
|
||||||
|
let config = AgentConfig {
|
||||||
bookmarks_autojoin: false,
|
bookmarks_autojoin: false,
|
||||||
..AgentConfig::default()
|
..AgentConfig::default()
|
||||||
};
|
};
|
||||||
let agent = ClientBuilder::new(jid, password)
|
let agent = ClientBuilder::new(jid, password)
|
||||||
.set_config(agent_config)
|
.set_config(config)
|
||||||
.set_client(ClientType::Bot, "xmpp-rs")
|
.set_client(ClientType::Bot, "xmpp-rs")
|
||||||
.set_website("https://gitlab.com/xmpp-rs/xmpp-rs")
|
.set_website("https://gitlab.com/xmpp-rs/xmpp-rs")
|
||||||
.set_default_nick(&nickname)
|
.set_default_nick(&nickname)
|
||||||
|
|
@ -55,8 +60,9 @@ impl XmppClient {
|
||||||
XmppClient {
|
XmppClient {
|
||||||
is_online: false,
|
is_online: false,
|
||||||
agent,
|
agent,
|
||||||
|
rooms,
|
||||||
nickname,
|
nickname,
|
||||||
config,
|
admins,
|
||||||
recent_hooks: Vec::new(),
|
recent_hooks: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -67,7 +73,7 @@ impl XmppClient {
|
||||||
Event::Online => {
|
Event::Online => {
|
||||||
self.is_online = true;
|
self.is_online = true;
|
||||||
debug!("XMPP Online");
|
debug!("XMPP Online");
|
||||||
for room in &self.config.rooms {
|
for room in &self.rooms {
|
||||||
self.agent
|
self.agent
|
||||||
.join_room(JoinRoomSettings {
|
.join_room(JoinRoomSettings {
|
||||||
nick: Some(RoomNick::from_resource_ref(self.nickname.as_ref())),
|
nick: Some(RoomNick::from_resource_ref(self.nickname.as_ref())),
|
||||||
|
|
@ -77,47 +83,24 @@ impl XmppClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::ChatMessage(_id, bare, message, _timeinfo) => {
|
Event::ChatMessage(_id, bare, message, _timeinfo) => {
|
||||||
if !self.config.admins.contains(&bare) {
|
if !self.admins.contains(&bare) {
|
||||||
debug!("Received chat message from {}, not in admins", bare);
|
debug!("Received chat message from {}, not in admins", bare);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("Received chat message from {}: {}", bare, message);
|
debug!("Received chat message from {}: {}", bare, message);
|
||||||
|
|
||||||
match message.as_str() {
|
if message == "rejoin" {
|
||||||
"rejoin" => {
|
for room in &self.rooms {
|
||||||
for room in &self.config.rooms {
|
|
||||||
self.agent
|
self.agent
|
||||||
.join_room(JoinRoomSettings {
|
.join_room(JoinRoomSettings {
|
||||||
nick: Some(RoomNick::from_resource_ref(
|
nick: Some(RoomNick::from_resource_ref(self.nickname.as_ref())),
|
||||||
self.nickname.as_ref(),
|
|
||||||
)),
|
|
||||||
force_resync: true,
|
force_resync: true,
|
||||||
..JoinRoomSettings::new(room.clone())
|
..JoinRoomSettings::new(room.clone())
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"version" => {
|
|
||||||
self.agent
|
|
||||||
.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Event::Disconnected(e) => {
|
Event::Disconnected(e) => {
|
||||||
self.is_online = false;
|
self.is_online = false;
|
||||||
|
|
@ -226,7 +209,7 @@ impl XmppClient {
|
||||||
}
|
}
|
||||||
if let Some(display) = format_hook(&wh) {
|
if let Some(display) = format_hook(&wh) {
|
||||||
debug!("Hook: {}", display);
|
debug!("Hook: {}", display);
|
||||||
for room in &self.config.rooms {
|
for room in &self.rooms {
|
||||||
self.agent
|
self.agent
|
||||||
.send_raw_message(RawMessageSettings {
|
.send_raw_message(RawMessageSettings {
|
||||||
recipient: Jid::from(room.clone()),
|
recipient: Jid::from(room.clone()),
|
||||||
|
|
|
||||||
|
|
@ -23,15 +23,7 @@ use xmpp::jid::{BareJid, ResourcePart};
|
||||||
|
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
|
|
||||||
const MAYBE_VERSION: &'static str = include_str!(concat!(env!("OUT_DIR"), "/GIT_COMMIT"));
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
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 {
|
pub struct Config {
|
||||||
/// Accounts trusted for admin tasks
|
/// Accounts trusted for admin tasks
|
||||||
#[serde(default = "Vec::new")]
|
#[serde(default = "Vec::new")]
|
||||||
|
|
@ -58,11 +50,6 @@ pub struct Config {
|
||||||
/// HTTP Webhook listening address and port, e.g., 127.0.0.1:1234 or [::1]:1234
|
/// HTTP Webhook listening address and port, e.g., 127.0.0.1:1234 or [::1]:1234
|
||||||
#[serde(default = "Config::default_addr")]
|
#[serde(default = "Config::default_addr")]
|
||||||
pub addr: SocketAddr,
|
pub addr: SocketAddr,
|
||||||
|
|
||||||
/// Software version
|
|
||||||
#[serde(skip)]
|
|
||||||
#[serde(default = "version")]
|
|
||||||
pub version: Option<&'static str>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
|
|
||||||
|
|
@ -38,11 +38,7 @@ impl From<FjIssue> for Issue {
|
||||||
action: Some(other.action.into()),
|
action: Some(other.action.into()),
|
||||||
title: other.issue.title,
|
title: other.issue.title,
|
||||||
author: User {
|
author: User {
|
||||||
name: other
|
name: other.issue.user.login,
|
||||||
.comment
|
|
||||||
.as_ref()
|
|
||||||
.map(|c| c.user.login.clone())
|
|
||||||
.unwrap_or(other.issue.user.login),
|
|
||||||
},
|
},
|
||||||
repository: Repository {
|
repository: Repository {
|
||||||
name: other.repository.name,
|
name: other.repository.name,
|
||||||
|
|
@ -64,11 +60,7 @@ impl From<FjIssue> for Note {
|
||||||
merge_request: None,
|
merge_request: None,
|
||||||
snippet: false,
|
snippet: false,
|
||||||
author: User {
|
author: User {
|
||||||
name: other
|
name: other.issue.user.login,
|
||||||
.comment
|
|
||||||
.as_ref()
|
|
||||||
.map(|c| c.user.login.clone())
|
|
||||||
.unwrap_or(other.issue.user.login),
|
|
||||||
},
|
},
|
||||||
repository: Repository {
|
repository: Repository {
|
||||||
name: other.repository.name,
|
name: other.repository.name,
|
||||||
|
|
@ -77,7 +69,6 @@ impl From<FjIssue> for Note {
|
||||||
.comment
|
.comment
|
||||||
.map(|c| c.html_url)
|
.map(|c| c.html_url)
|
||||||
.unwrap_or(other.issue.html_url),
|
.unwrap_or(other.issue.html_url),
|
||||||
is_update: other.action == FjIssueAction::Edited,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,11 @@
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
mod forgejo;
|
mod forgejo;
|
||||||
#[cfg(feature = "gitlab")]
|
|
||||||
mod gitlab;
|
mod gitlab;
|
||||||
mod types;
|
mod types;
|
||||||
|
|
||||||
pub use crate::hooks::forgejo::ForgejoHook;
|
pub use crate::hooks::forgejo::ForgejoHook;
|
||||||
#[cfg(feature = "gitlab")]
|
#[allow(deprecated)]
|
||||||
pub use crate::hooks::gitlab::GitlabHook;
|
pub use crate::hooks::gitlab::GitlabHook;
|
||||||
pub use crate::hooks::types::Hook;
|
pub use crate::hooks::types::Hook;
|
||||||
pub use crate::hooks::types::{IssueAction, MergeRequestAction, WikiAction};
|
pub use crate::hooks::types::{IssueAction, MergeRequestAction, WikiAction};
|
||||||
|
|
@ -109,26 +108,19 @@ pub fn format_hook(hook: &Hook) -> Option<String> {
|
||||||
if note.snippet {
|
if note.snippet {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let updated_fmt = if note.is_update {
|
|
||||||
"updated comment"
|
|
||||||
} else {
|
|
||||||
"commented"
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(commit) = ¬e.commit {
|
if let Some(commit) = ¬e.commit {
|
||||||
format!(
|
format!(
|
||||||
"[{}] {} {updated_fmt} on commit {:?} <{}>",
|
"[{}] {} commented on commit {:?} <{}>",
|
||||||
note.repository.name, note.author.name, commit.ref_, commit.url,
|
note.repository.name, note.author.name, commit.ref_, commit.url,
|
||||||
)
|
)
|
||||||
} else if let Some(issue) = ¬e.issue {
|
} else if let Some(issue) = ¬e.issue {
|
||||||
format!(
|
format!(
|
||||||
"[{}] {} {updated_fmt} on issue {}: {} <{}>",
|
"[{}] {} commented on issue {}: {} <{}>",
|
||||||
note.repository.name, note.author.name, issue.id, issue.title, note.url,
|
note.repository.name, note.author.name, issue.id, issue.title, note.url,
|
||||||
)
|
)
|
||||||
} else if let Some(mr) = ¬e.merge_request {
|
} else if let Some(mr) = ¬e.merge_request {
|
||||||
format!(
|
format!(
|
||||||
"[{}] {} {updated_fmt} on merge request {}: {} <{}>",
|
"[{}] {} commented on merge request {}: {} <{}>",
|
||||||
note.repository.name, note.author.name, mr.id, mr.title, note.url,
|
note.repository.name, note.author.name, mr.id, mr.title, note.url,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,6 @@ pub struct Note {
|
||||||
pub repository: Repository,
|
pub repository: Repository,
|
||||||
pub author: User,
|
pub author: User,
|
||||||
pub url: String,
|
pub url: String,
|
||||||
pub is_update: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
|
|
||||||
|
|
@ -56,10 +56,11 @@ async fn main() -> Result<(), Error> {
|
||||||
let (value_tx, value_rx) = mpsc::unbounded_channel::<Hook>();
|
let (value_tx, value_rx) = mpsc::unbounded_channel::<Hook>();
|
||||||
|
|
||||||
let mut bot = XmppClient::new(
|
let mut bot = XmppClient::new(
|
||||||
config.jid.clone(),
|
config.jid,
|
||||||
config.password.as_str(),
|
config.password.as_str(),
|
||||||
config.nickname.clone(),
|
config.rooms,
|
||||||
config.clone(),
|
config.nickname,
|
||||||
|
config.admins,
|
||||||
);
|
);
|
||||||
|
|
||||||
let xmpp_handle = tokio::task::spawn(async move {
|
let xmpp_handle = tokio::task::spawn(async move {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
#[cfg(feature = "gitlab")]
|
#[allow(deprecated)]
|
||||||
use crate::hooks::GitlabHook;
|
use crate::hooks::GitlabHook;
|
||||||
use crate::hooks::{ForgejoHook, Hook};
|
use crate::hooks::{ForgejoHook, Hook};
|
||||||
|
|
||||||
|
|
@ -22,7 +22,7 @@ use std::convert::Infallible;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
|
||||||
use bytes::{Buf, Bytes};
|
use bytes::{Buf, Bytes};
|
||||||
use hmac::{Hmac, KeyInit, Mac};
|
use hmac::{Hmac, Mac};
|
||||||
use http_body_util::{BodyExt, Full};
|
use http_body_util::{BodyExt, Full};
|
||||||
use hyper::{Method, Request, Response, body::Incoming, header};
|
use hyper::{Method, Request, Response, body::Incoming, header};
|
||||||
use log::{debug, error, trace};
|
use log::{debug, error, trace};
|
||||||
|
|
@ -62,7 +62,6 @@ async fn hooks_inner(req: Request<Incoming>, secret: &str) -> Result<Hook, Error
|
||||||
let whole_body = req.collect().await?.aggregate();
|
let whole_body = req.collect().await?.aggregate();
|
||||||
whole_body.reader().read_to_end(&mut payload)?;
|
whole_body.reader().read_to_end(&mut payload)?;
|
||||||
|
|
||||||
#[cfg(feature = "gitlab")]
|
|
||||||
if let Some(val) = headers.get("X-Gitlab-Token") {
|
if let Some(val) = headers.get("X-Gitlab-Token") {
|
||||||
if secret != val {
|
if secret != val {
|
||||||
return Err(Error::InvalidSecret);
|
return Err(Error::InvalidSecret);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue