dergchat/src/xmpp_interface.rs
Werner Kroneman 837dca3e15 Added dedicated types to Room/MucRoom/Direct messages, etc...
It's still a bit of a mess though.
2023-12-11 16:51:45 +01:00

296 lines
10 KiB
Rust

// 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 <https://www.gnu.org/licenses/>.
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<BareJid, Room>,
}
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<Messages>,
) {
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<Messages>,
commands: &'a mut UnboundedReceiver<NetworkCommand>,
) -> impl Future<Output = ()> + 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<LoginStatus>,
mut messages: UseRef<Messages>,
mut commands: UnboundedReceiver<NetworkCommand>,
) {
// 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");
}
}
}