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};
|
2023-12-10 09:54:31 -05:00
|
|
|
use dioxus::hooks::{UnboundedReceiver, UseRef, UseState};
|
2023-12-09 10:40:03 -05:00
|
|
|
use futures_util::stream::StreamExt;
|
|
|
|
use jid::BareJid;
|
2023-12-10 09:54:31 -05:00
|
|
|
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;
|
2023-12-10 09:54:31 -05:00
|
|
|
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 },
|
|
|
|
}
|
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.");
|
|
|
|
},
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
info!("Command channel closed");
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-12-10 09:54:31 -05:00
|
|
|
|
|
|
|
/// 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:
|
|
|
|
|
|
|
|
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)
|
|
|
|
.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");
|
|
|
|
}
|
|
|
|
}
|