Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 732f77f141 | |||
| 0763ce13c9 | |||
| 367e90931e | |||
| d54ef5f265 | |||
|
|
5fc65c7636 |
||
|
|
43106fc657 |
||
|
|
be1379f13f |
||
|
|
6647a9ae92 | ||
|
|
457c40bffe | ||
|
|
849ecbd5dc |
11 changed files with 149 additions and 41 deletions
15
Cargo.toml
15
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" ]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
41
build.rs
Normal file
41
build.rs
Normal file
|
|
@ -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 <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");
|
||||
}
|
||||
8
config.sample.toml
Normal file
8
config.sample.toml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
admins = [ "foo@localhost" ]
|
||||
jid = "bar@localhost"
|
||||
password = "bonjourxmpp"
|
||||
|
||||
rooms = [ "room@muc.localhost" ]
|
||||
secret = "bonjourforgejo"
|
||||
|
||||
addr = "[::1]:5225"
|
||||
63
src/bot.rs
63
src/bot.rs
|
|
@ -14,6 +14,7 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
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<BareJid>,
|
||||
nickname: ResourcePart,
|
||||
admins: Vec<BareJid>,
|
||||
config: Config,
|
||||
/// Keep around messages we've sent recently so we're able to prevent spamming the same type of
|
||||
/// messages
|
||||
recent_hooks: Vec<Hook>,
|
||||
}
|
||||
|
||||
impl XmppClient {
|
||||
pub fn new(
|
||||
jid: BareJid,
|
||||
password: &str,
|
||||
rooms: Vec<BareJid>,
|
||||
nickname: ResourcePart,
|
||||
admins: Vec<BareJid>,
|
||||
) -> 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()),
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -38,7 +38,11 @@ impl From<FjIssue> 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<FjIssue> 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<FjIssue> for Note {
|
|||
.comment
|
||||
.map(|c| c.html_url)
|
||||
.unwrap_or(other.issue.html_url),
|
||||
is_update: other.action == FjIssueAction::Edited,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,11 +14,12 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
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<String> {
|
|||
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 {
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ pub struct Note {
|
|||
pub repository: Repository,
|
||||
pub author: User,
|
||||
pub url: String,
|
||||
pub is_update: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
|
|
|
|||
|
|
@ -56,11 +56,10 @@ async fn main() -> Result<(), Error> {
|
|||
let (value_tx, value_rx) = mpsc::unbounded_channel::<Hook>();
|
||||
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
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<Incoming>, secret: &str) -> Result<Hook, Error
|
|||
let whole_body = req.collect().await?.aggregate();
|
||||
whole_body.reader().read_to_end(&mut payload)?;
|
||||
|
||||
#[cfg(feature = "gitlab")]
|
||||
if let Some(val) = headers.get("X-Gitlab-Token") {
|
||||
if secret != val {
|
||||
return Err(Error::InvalidSecret);
|
||||
|
|
|
|||
Loading…
Reference in a new issue