// 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 . use crate::Error; use crate::hooks::{Hook, format_hook}; use log::debug; use tokio::{signal::ctrl_c, sync::mpsc}; 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, LeaveRoomSettings}, }; pub struct XmppClient { is_online: bool, agent: Agent, rooms: Vec, nickname: ResourcePart, admins: Vec, } impl XmppClient { pub fn new( jid: BareJid, password: &str, rooms: Vec, nickname: ResourcePart, admins: Vec, ) -> XmppClient { let config = AgentConfig { bookmarks_autojoin: false, ..AgentConfig::default() }; let agent = ClientBuilder::new(jid, password) .set_config(config) .set_client(ClientType::Bot, "xmpp-rs") .set_website("https://gitlab.com/xmpp-rs/xmpp-rs") .set_default_nick(&nickname) .enable_feature(ClientFeature::JoinRooms) .build(); XmppClient { is_online: false, agent, rooms, nickname, admins, } } pub async fn next(&mut self) { for event in self.agent.wait_for_events().await { match event { Event::Online => { self.is_online = true; debug!("XMPP Online"); self.join_rooms().await } Event::ChatMessage(_id, bare, message, _timeinfo) => { if !self.admins.contains(&bare) { debug!("Received chat message from {}, not in admins", bare); continue; } debug!("Received chat message from {}: {}", bare, message); if message == "rejoin" { self.leave_rooms().await; self.join_rooms().await } } Event::Disconnected(e) => { self.is_online = false; debug!("XMPP Disconnected: {e}"); } _ => { debug!("XMPP Event not supported") } } } } pub async fn join_rooms(&mut self) { for room in &self.rooms { self.agent .join_room(JoinRoomSettings { room: room.clone(), nick: Some(RoomNick::from_resource_ref(self.nickname.as_ref())), password: None, status: Some(("en", "Hi there!")), }) .await } } pub async fn leave_rooms(&mut self) { for room in &self.rooms { self.agent .leave_room(LeaveRoomSettings { room: room.clone(), status: Some(("en", "See you!")), }) .await } } pub async fn receive(&mut self, mut rx: mpsc::UnboundedReceiver) { loop { tokio::select! { _ = ctrl_c() => { return; // Disconnecting }, _ = self.next() => (), wh = rx.recv() => { if let Some(hook) = wh { debug!("XMPP Bot Received Hook"); self.hook(hook).await } } } } } pub async fn hook(&mut self, wh: Hook) { debug!("XMPP Bot Processing Hook"); if let Some(display) = format_hook(&wh) { debug!("Hook: {}", display); for room in &self.rooms { self.agent .send_raw_message(RawMessageSettings { recipient: Jid::from(room.clone()), message_type: MessageType::Groupchat, message: &display, lang: Some("en"), payloads: Vec::new(), }) .await } } debug!("XMPP Bot Processed Hook"); } pub async fn disconnect(self) -> Result<(), Error> { log::info!("Disconnecting..."); Ok(self.agent.disconnect().await?) } }