dergchat/src/xmpp_interface.rs

233 lines
8.4 KiB
Rust
Raw Normal View History

2023-12-09 15:24:57 -05:00
// 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/>.
2023-12-09 10:40:03 -05:00
use crate::types::{LoginCredentials, Message};
use dioxus::hooks::{UnboundedReceiver, UseRef, UseState};
2023-12-09 10:40:03 -05:00
use futures_util::stream::StreamExt;
use jid::BareJid;
use log::{error, info};
2023-12-09 10:40:03 -05:00
use std::collections::HashMap;
use std::future::Future;
2023-12-10 07:21:59 -05:00
2023-12-10 10:30:29 -05:00
use crate::widgets::login_screen::LoginStatus;
2023-12-09 10:40:03 -05:00
use tokio::select;
use xmpp::parsers::message::MessageType;
use xmpp::{Agent, ClientBuilder, ClientType};
/// 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,
}
2023-12-09 10:40:03 -05:00
async fn handle_event(
event: xmpp::Event,
agent: &mut xmpp::Agent,
messages: &mut UseRef<HashMap<BareJid, Vec<Message>>>,
) {
match event {
2023-12-10 07:21:59 -05:00
xmpp::Event::JoinRoom(jid, _conference) => {
2023-12-09 10:40:03 -05:00
println!("Will auto-join room: {}", &jid);
agent.join_room(jid, None, None, "en/us", "online").await;
}
xmpp::Event::RoomJoined(room_jid) => {
println!("Joined room: {}", &room_jid);
messages.with_mut(move |m| {
m.entry(room_jid.clone()).or_insert(vec![]);
});
}
xmpp::Event::RoomLeft(room_jid) => {
println!("Left room: {}", &room_jid);
messages.with_mut(move |m| {
m.remove(&room_jid);
});
}
2023-12-10 07:21:59 -05:00
xmpp::Event::ChatMessage(_id, sender, body) => {
2023-12-09 10:40:03 -05:00
println!("Message from {}: {}", &sender, &body.0);
messages.with_mut(move |m| {
m.entry(sender.clone()).or_insert(vec![]).push(Message {
sender: sender.to_string(),
body: body.0,
});
});
}
2023-12-10 07:21:59 -05:00
xmpp::Event::RoomMessage(_id, room_jid, sender_nick, body) => {
2023-12-09 10:40:03 -05:00
println!(
"Message in {} from {}: {}",
&room_jid, &sender_nick, &body.0
);
messages.with_mut(move |m| {
m.entry(room_jid.clone()).or_insert(vec![]).push(Message {
sender: sender_nick,
body: body.0,
})
});
}
2023-12-10 07:21:59 -05:00
xmpp::Event::RoomPrivateMessage(_id, room_jid, sender_nick, body) => {
2023-12-09 10:40:03 -05:00
println!(
"Private message in {} from {}: {}",
&room_jid, &sender_nick, &body.0
);
messages.with_mut(move |m| {
m.entry(room_jid.clone()).or_insert(vec![]).push(Message {
sender: sender_nick,
body: body.0,
})
});
}
_ => {
log::debug!("Received unsupported event {:?}", event);
}
}
}
pub fn xmpp_mainloop<'a>(
agent: &'a mut Agent,
mut room_data: &'a mut UseRef<HashMap<BareJid, Vec<Message>>>,
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;
},
2023-12-10 07:21:59 -05:00
NetworkCommand::TryLogin { credentials: _ } => {
2023-12-09 10:40:03 -05:00
panic!("Already logged in.");
},
NetworkCommand::Logout => {
agent.disconnect().await;
break;
}
2023-12-09 10:40:03 -05:00
}
} 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<HashMap<BareJid, Vec<Message>>>,
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);
connection_status.set(LoginStatus::LoggingIn);
let mut agent = ClientBuilder::new(credentials.username, &credentials.password.0)
.set_client(ClientType::Pc, "dergchat")
.build();
match await_online(&mut agent).await {
Ok(_) => {
connection_status.set(LoginStatus::LoggedIn);
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");
}
}
}