2023-05-20 20:30:56 +02:00
|
|
|
// Copyright (C) 2023-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/>.
|
|
|
|
|
|
2025-05-03 13:29:43 +02:00
|
|
|
use crate::Error;
|
2026-01-08 22:23:55 +01:00
|
|
|
use crate::config::Config;
|
2026-01-08 13:47:23 +01:00
|
|
|
use crate::hooks::{Hook, IssueAction, MergeRequestAction, format_hook};
|
2023-05-20 20:30:56 +02:00
|
|
|
|
2026-01-08 14:09:31 +01:00
|
|
|
use chrono::{TimeDelta, Utc};
|
2023-05-20 20:30:56 +02:00
|
|
|
use log::debug;
|
2025-05-03 13:29:43 +02:00
|
|
|
use tokio::{signal::ctrl_c, sync::mpsc};
|
2024-12-19 23:58:47 +01:00
|
|
|
use xmpp::jid::{BareJid, Jid, ResourcePart};
|
2023-08-21 13:13:47 +02:00
|
|
|
use xmpp::parsers::message::MessageType;
|
2024-12-19 23:58:47 +01:00
|
|
|
use xmpp::{
|
2025-12-27 14:30:54 +01:00
|
|
|
Agent, ClientBuilder, ClientFeature, ClientType, Config as AgentConfig, Event, RoomNick,
|
2026-01-08 23:41:35 +01:00
|
|
|
message::send::{MessageSettings, RawMessageSettings},
|
|
|
|
|
muc::room::JoinRoomSettings,
|
2024-12-19 23:58:47 +01:00
|
|
|
};
|
2023-05-20 20:30:56 +02:00
|
|
|
|
|
|
|
|
pub struct XmppClient {
|
|
|
|
|
is_online: bool,
|
2024-12-19 23:58:47 +01:00
|
|
|
agent: Agent,
|
|
|
|
|
nickname: ResourcePart,
|
2026-01-08 22:23:55 +01:00
|
|
|
config: Config,
|
2026-01-08 13:47:23 +01:00
|
|
|
/// Keep around messages we've sent recently so we're able to prevent spamming the same type of
|
|
|
|
|
/// messages
|
|
|
|
|
recent_hooks: Vec<Hook>,
|
2023-05-20 20:30:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl XmppClient {
|
2026-01-08 22:23:55 +01:00
|
|
|
pub fn new(jid: BareJid, password: &str, nickname: ResourcePart, config: Config) -> XmppClient {
|
|
|
|
|
let agent_config = AgentConfig {
|
2025-11-15 14:13:38 +01:00
|
|
|
bookmarks_autojoin: false,
|
|
|
|
|
..AgentConfig::default()
|
|
|
|
|
};
|
2023-05-20 20:30:56 +02:00
|
|
|
let agent = ClientBuilder::new(jid, password)
|
2026-01-08 22:23:55 +01:00
|
|
|
.set_config(agent_config)
|
2023-05-20 20:30:56 +02:00
|
|
|
.set_client(ClientType::Bot, "xmpp-rs")
|
|
|
|
|
.set_website("https://gitlab.com/xmpp-rs/xmpp-rs")
|
2024-12-19 23:58:47 +01:00
|
|
|
.set_default_nick(&nickname)
|
2023-05-20 20:30:56 +02:00
|
|
|
.enable_feature(ClientFeature::JoinRooms)
|
2023-08-21 13:13:47 +02:00
|
|
|
.build();
|
2023-05-20 20:30:56 +02:00
|
|
|
|
|
|
|
|
XmppClient {
|
|
|
|
|
is_online: false,
|
|
|
|
|
agent,
|
2023-05-21 23:47:54 +02:00
|
|
|
nickname,
|
2026-01-08 22:23:55 +01:00
|
|
|
config,
|
2026-01-08 13:47:23 +01:00
|
|
|
recent_hooks: Vec::new(),
|
2023-05-20 20:30:56 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn next(&mut self) {
|
2024-08-30 19:24:17 +02:00
|
|
|
for event in self.agent.wait_for_events().await {
|
|
|
|
|
match event {
|
|
|
|
|
Event::Online => {
|
|
|
|
|
self.is_online = true;
|
|
|
|
|
debug!("XMPP Online");
|
2026-01-08 22:23:55 +01:00
|
|
|
for room in &self.config.rooms {
|
2025-12-28 19:08:19 +01:00
|
|
|
self.agent
|
|
|
|
|
.join_room(JoinRoomSettings {
|
|
|
|
|
nick: Some(RoomNick::from_resource_ref(self.nickname.as_ref())),
|
|
|
|
|
..JoinRoomSettings::new(room.clone())
|
|
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
}
|
2025-12-27 14:30:54 +01:00
|
|
|
}
|
|
|
|
|
Event::ChatMessage(_id, bare, message, _timeinfo) => {
|
2026-01-08 22:23:55 +01:00
|
|
|
if !self.config.admins.contains(&bare) {
|
2025-12-27 14:30:54 +01:00
|
|
|
debug!("Received chat message from {}, not in admins", bare);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
debug!("Received chat message from {}: {}", bare, message);
|
2023-05-20 20:30:56 +02:00
|
|
|
|
2026-01-08 23:41:35 +01:00
|
|
|
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
|
|
|
|
|
.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.";
|
2025-12-28 19:08:19 +01:00
|
|
|
self.agent
|
2026-01-08 23:41:35 +01:00
|
|
|
.send_message(MessageSettings {
|
|
|
|
|
recipient: bare,
|
|
|
|
|
message,
|
|
|
|
|
lang: Some("en"),
|
2025-12-28 19:08:19 +01:00
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
}
|
2023-05-20 20:30:56 +02:00
|
|
|
}
|
|
|
|
|
}
|
2024-08-30 19:24:17 +02:00
|
|
|
Event::Disconnected(e) => {
|
|
|
|
|
self.is_online = false;
|
|
|
|
|
debug!("XMPP Disconnected: {e}");
|
|
|
|
|
}
|
|
|
|
|
_ => {
|
|
|
|
|
debug!("XMPP Event not supported")
|
|
|
|
|
}
|
2023-05-20 20:30:56 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-21 17:11:43 +01:00
|
|
|
pub async fn receive(&mut self, mut rx: mpsc::UnboundedReceiver<Hook>) {
|
|
|
|
|
loop {
|
|
|
|
|
tokio::select! {
|
2025-05-03 13:29:43 +02:00
|
|
|
_ = ctrl_c() => {
|
|
|
|
|
return; // Disconnecting
|
|
|
|
|
},
|
2024-11-21 17:11:43 +01:00
|
|
|
_ = self.next() => (),
|
|
|
|
|
wh = rx.recv() => {
|
|
|
|
|
if let Some(hook) = wh {
|
|
|
|
|
debug!("XMPP Bot Received Hook");
|
|
|
|
|
self.hook(hook).await
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 13:47:23 +01:00
|
|
|
/// Compare incoming hook with recently sent hooks and update the list.
|
|
|
|
|
/// Compare only hook type and author. If they match, check how long ago it was.
|
|
|
|
|
/// Returns true if the hook is recent enough and thus not to be sent.
|
|
|
|
|
fn update_recent_hooks(&mut self, new_hook: Hook) -> bool {
|
|
|
|
|
// Do we need to remove/update the hook?
|
|
|
|
|
let mut removal: Option<usize> = None;
|
|
|
|
|
|
2026-01-08 14:09:31 +01:00
|
|
|
let datetime = Utc::now() - TimeDelta::minutes(5);
|
|
|
|
|
|
|
|
|
|
// Remove expired hooks (more than time delta)
|
|
|
|
|
self.recent_hooks.retain(|hook| match hook {
|
|
|
|
|
Hook::MergeRequest(hook) if hook.updated_at > datetime => false,
|
|
|
|
|
Hook::Issue(hook) if hook.updated_at > datetime => false,
|
|
|
|
|
_ => true,
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-08 13:47:23 +01:00
|
|
|
for (i, old_hook) in self.recent_hooks.iter().enumerate() {
|
|
|
|
|
match (old_hook, &new_hook) {
|
|
|
|
|
(_, &Hook::MergeRequest(ref new))
|
|
|
|
|
if new.action != Some(MergeRequestAction::Update) =>
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
(_, &Hook::Issue(ref new)) if new.action != Some(IssueAction::Update) => {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
(&Hook::MergeRequest(ref old), &Hook::MergeRequest(ref new)) => {
|
|
|
|
|
// Action is MergeRequestAction::Update, otherwise it would have matched the other branch
|
|
|
|
|
// and the method would have returned.
|
|
|
|
|
if old.id == new.id {
|
2026-01-08 14:09:31 +01:00
|
|
|
if old.author.name == new.author.name {
|
2026-01-08 13:47:23 +01:00
|
|
|
// If everything matches and we're still within the time frame, let the old hook
|
|
|
|
|
// expire, don't update it.
|
|
|
|
|
return true;
|
|
|
|
|
} else {
|
|
|
|
|
// The old hook either doesn't match the new author (we want to announce messages
|
2026-01-08 14:09:31 +01:00
|
|
|
// from different authors), let it be replaced by the new hook.
|
2026-01-08 13:47:23 +01:00
|
|
|
removal = Some(i);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
(&Hook::Issue(ref old), &Hook::Issue(ref new)) => {
|
|
|
|
|
// See the MergeRequest branch for comments
|
|
|
|
|
if old.id == new.id {
|
2026-01-08 14:09:31 +01:00
|
|
|
if old.author.name == new.author.name {
|
2026-01-08 13:47:23 +01:00
|
|
|
return true;
|
|
|
|
|
} else {
|
|
|
|
|
removal = Some(i);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ => (),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(index) = removal {
|
|
|
|
|
self.recent_hooks.swap_remove(index);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The new hook hasn't been matched and needs to be added to the list, or it has and will
|
|
|
|
|
// replace the matched hook.
|
|
|
|
|
match new_hook {
|
|
|
|
|
Hook::Issue(_) | Hook::MergeRequest(_) => self.recent_hooks.push(new_hook),
|
|
|
|
|
_ => (),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-31 00:36:05 +02:00
|
|
|
pub async fn hook(&mut self, wh: Hook) {
|
2024-11-21 17:11:43 +01:00
|
|
|
debug!("XMPP Bot Processing Hook");
|
2026-01-08 13:47:23 +01:00
|
|
|
if self.update_recent_hooks(wh.clone()) {
|
|
|
|
|
debug!("Hook already sent recently");
|
|
|
|
|
return;
|
|
|
|
|
}
|
2024-07-10 14:02:35 +02:00
|
|
|
if let Some(display) = format_hook(&wh) {
|
|
|
|
|
debug!("Hook: {}", display);
|
2026-01-08 22:23:55 +01:00
|
|
|
for room in &self.config.rooms {
|
2023-05-21 23:47:54 +02:00
|
|
|
self.agent
|
2024-12-19 23:58:47 +01:00
|
|
|
.send_raw_message(RawMessageSettings {
|
|
|
|
|
recipient: Jid::from(room.clone()),
|
|
|
|
|
message_type: MessageType::Groupchat,
|
|
|
|
|
message: &display,
|
|
|
|
|
lang: Some("en"),
|
|
|
|
|
payloads: Vec::new(),
|
|
|
|
|
})
|
2023-05-21 23:47:54 +02:00
|
|
|
.await
|
|
|
|
|
}
|
2023-05-20 20:30:56 +02:00
|
|
|
}
|
2025-12-17 16:01:23 +01:00
|
|
|
debug!("XMPP Bot Processed Hook");
|
2023-05-20 20:30:56 +02:00
|
|
|
}
|
2025-05-03 13:29:43 +02:00
|
|
|
|
|
|
|
|
pub async fn disconnect(self) -> Result<(), Error> {
|
|
|
|
|
log::info!("Disconnecting...");
|
|
|
|
|
Ok(self.agent.disconnect().await?)
|
|
|
|
|
}
|
2023-05-20 20:30:56 +02:00
|
|
|
}
|