// Dergchat, a free XMPP client. // Copyright (C) 2023 Werner Kroneman // // 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::types::{DirectMessage, DirectMessageRoom, LoginCredentials, MucMessage, MucRoom, Nickname}; use dioxus::hooks::{UnboundedReceiver, UseRef, UseState}; use futures_util::stream::StreamExt; use jid::BareJid; use log::{debug, error, info, warn}; use std::collections::HashMap; use std::future::Future; use crate::widgets::login_screen::LoginStatus; use tokio::select; use xmpp::parsers::message::MessageType; use xmpp::{Agent, ClientBuilder, ClientType}; use crate::types::room::{Room, WrongRoomType}; use crate::types::message::MessageInOut; /// An enum of commands that can be sent to the XMPP interface. /// /// These very loosely correspond to XMPP stanzas, but are more high level. #[derive(Debug)] pub enum NetworkCommand { /// Start a new login attempt, resulting in either a successful login or an error. TryLogin { credentials: LoginCredentials }, /// Join a room. JoinRoom { room: BareJid }, /// Leave a room. LeaveRoom { room: BareJid }, /// Send a message to a recipient. SendMessage { recipient: BareJid, message: String }, Logout, } #[derive(Debug, Clone, PartialEq)] pub struct Messages { pub messages: HashMap, } impl Messages { pub fn new() -> Self { Self { messages: HashMap::new(), } } pub fn get(&self, room: &BareJid) -> Option<&Room> { self.messages.get(room) } pub fn get_or_create_mut_direct(&mut self, room: &BareJid) -> Result<&mut DirectMessageRoom, WrongRoomType> { self.messages.entry(room.clone()).or_insert(Room::Direct(DirectMessageRoom { messages: vec![], })).try_into() } pub fn get_or_create_mut_muc(&mut self, room: &BareJid) -> Result<&mut MucRoom, WrongRoomType> { self.messages.entry(room.clone()).or_insert(Room::Muc(MucRoom { messages: vec![], })).try_into() } } async fn handle_event( event: xmpp::Event, agent: &mut xmpp::Agent, messages: &mut UseRef, ) { match event { xmpp::Event::JoinRoom(jid, _conference) => { debug!("Will auto-join room: {}", &jid); agent.join_room(jid, None, None, "en/us", "online").await; } xmpp::Event::RoomJoined(room_jid) => { debug!("Joined room: {}", &room_jid); messages.with_mut(move |m| { m.messages.entry(room_jid.clone()).or_insert(Room::Muc(MucRoom { messages: vec![], })); }); } xmpp::Event::RoomLeft(room_jid) => { debug!("Left room: {}", &room_jid); messages.with_mut(move |m| { m.messages.remove(&room_jid); }); } xmpp::Event::ChatMessage(_id, sender, body, timestamp) => { debug!("Message from {}: {}", &sender, &body.0); messages.with_mut(move |m| { let dms : &mut DirectMessageRoom = m.messages.entry(sender.clone()).or_insert(Room::Direct(DirectMessageRoom { messages: vec![], })).try_into().expect("Received direct message with a JID from a MUC"); dms.messages.push(DirectMessage { in_out: MessageInOut::Incoming, body: body.0, timestamp, }); }); } xmpp::Event::RoomMessage(_id, room_jid, sender_nick, body, timestamp) => { debug!( "Message in {} from {}: {}", &room_jid, &sender_nick, &body.0 ); messages.with_mut(move |m| { let muc : &mut MucRoom = m.get_or_create_mut_muc(&room_jid) .expect("Received direct message with a JID from a MUC"); muc.messages.push(MucMessage { in_out: MessageInOut::Incoming, sender_nick: Nickname(sender_nick.to_string()), body: body.0, timestamp, is_private: false, }); }); } xmpp::Event::RoomPrivateMessage(_id, room_jid, sender_nick, body, timestamp) => { debug!( "Private message in {} from {}: {}", &room_jid, &sender_nick, &body.0 ); messages.with_mut(move |m| { let muc : &mut MucRoom = m.get_or_create_mut_muc(&room_jid) .expect("Received direct message with a JID from a MUC"); muc.messages.push(MucMessage { in_out: MessageInOut::Incoming, sender_nick: Nickname(sender_nick.to_string()), body: body.0, timestamp, is_private: true, }); }); } _ => { warn!("Received unsupported event {:?}", event); } } } pub fn xmpp_mainloop<'a>( agent: &'a mut Agent, mut room_data: &'a mut UseRef, commands: &'a mut UnboundedReceiver, ) -> impl Future + Sized + 'a { async move { loop { select! { events = agent.wait_for_events() => { if let Some(events) = events { for event in events { handle_event(event, agent, &mut room_data).await; } } else { info!("Disconnected"); } }, command = commands.next() => { if let Some(command) = command { match command { NetworkCommand::JoinRoom { room } => { agent.join_room(room.clone(), None, None, "en-us", "online").await; }, NetworkCommand::LeaveRoom { room } => { agent.leave_room( room, "dergchat".to_string(), "en-us", "User left the room.").await; }, NetworkCommand::SendMessage { recipient, message } => { agent.send_message(recipient.into(), MessageType::Groupchat, "en-us", &message).await; }, NetworkCommand::TryLogin { credentials: _ } => { panic!("Already logged in."); }, NetworkCommand::Logout => { if let Err(e) = agent.disconnect().await { error!("Error while disconnecting: {}", e); } break; } } } else { info!("Command channel closed"); break; } }, } } } } /// Wait for the agent to go online. /// /// This function mutably borrows an Agent and waits for either an Online or Disconnected event. /// /// The future resolves to Ok(()) if the agent went online, or Err(String) if the agent disconnected. async fn await_online(agent: &mut Agent) -> Result<(), String> { // Wait for the next batch of events. let events = agent .wait_for_events() .await .ok_or("Stream closed unexpectedly".to_string())?; // We expect exactly one event; we operate under the assumption that xmpp-rs emits // exactly one events when it receives a connect/disconnect stanza from the server, // and that that is the first stanza it receives. // TODO: This is brittle; need to talk to the xmpp-rs to get stronger guarantees. assert_eq!(events.len(), 1); // We expect the first event to be either Online or Disconnected. match events.into_iter().next().expect("No events") { xmpp::Event::Online => { info!("Login attempt successful; connected."); Ok(()) } xmpp::Event::Disconnected => { error!("Login attempt resulted in disconnect."); Err("Disconnected".to_string()) } // We don't expect any other events; if we get one, we panic. (The assumption we made must be wrong.) // TODO: This is ugly. e => { panic!("Unexpected event: {:?}", e); } } } pub async fn run_xmpp_toplevel( connection_status: UseState, mut messages: UseRef, mut commands: UnboundedReceiver, ) { // Await a login attempt: loop { let cmd = commands.next().await; if let Some(NetworkCommand::TryLogin { credentials }) = cmd { println!("Received credentials: {:?}", credentials); let LoginCredentials { username, default_nick, password, } = credentials; connection_status.set(LoginStatus::LoggingIn); let mut agent = ClientBuilder::new(username.clone(), &password.0) .set_client(ClientType::Pc, "dergchat") .build(); match await_online(&mut agent).await { Ok(_) => { connection_status.set(LoginStatus::LoggedIn(username.clone())); info!("Connected"); xmpp_mainloop(&mut agent, &mut messages, &mut commands).await; } Err(e) => { error!("Failed to connect: {}", e); connection_status.set(LoginStatus::Error("Failed to connect".to_string())); } } } else { panic!("Expected TryLogin command"); } } }