From 18b64ef030f9f70d250d5df4c1fb623c10908e79 Mon Sep 17 00:00:00 2001 From: Werner Kroneman Date: Sat, 9 Dec 2023 16:40:03 +0100 Subject: [PATCH] Basic login screen working. --- src/main.rs | 386 ++++---------------------------- src/types.rs | 29 +++ src/widgets/login_screen.rs | 136 +++++++++++ src/widgets/mod.rs | 181 +++++++++++++++ src/widgets/room_join_widget.rs | 31 +++ src/widgets/room_list.rs | 45 ++++ src/widgets/room_view.rs | 46 ++++ src/widgets/send_message.rs | 34 +++ src/xmpp_interface.rs | 127 +++++++++++ 9 files changed, 669 insertions(+), 346 deletions(-) create mode 100644 src/types.rs create mode 100644 src/widgets/login_screen.rs create mode 100644 src/widgets/mod.rs create mode 100644 src/widgets/room_join_widget.rs create mode 100644 src/widgets/room_list.rs create mode 100644 src/widgets/room_view.rs create mode 100644 src/widgets/send_message.rs create mode 100644 src/xmpp_interface.rs diff --git a/src/main.rs b/src/main.rs index 9e3dab3..e93034b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,18 @@ #![allow(non_snake_case)] -use std::collections::HashMap; -use std::future::Future; -use std::str::FromStr; -use futures_util::stream::StreamExt; +mod types; +mod widgets; +mod xmpp_interface; + +use crate::dioxus_elements::div; +use dioxus::html::button; use dioxus::prelude::*; use dioxus_desktop::Config; -use log::info; -use tokio::select; -use xmpp::{BareJid, ClientBuilder, ClientType}; -use xmpp::parsers::message::MessageType; +use futures_util::stream::StreamExt; +use jid::BareJid; +use std::future::Future; +use std::str::FromStr; +use widgets::App; const STYLESHEET: &str = r#" body { @@ -28,351 +31,42 @@ const STYLESHEET: &str = r#" height: 100%; } + .login-form { + margin: auto; + text-align: center; + } + + .login-form input[type=text], .login-form input[type=password] { + display: block; + padding: 2mm; + margin: 2mm 0; + } + + .login-form input[type=checkbox] { + vertical-align: middle; + padding: 2mm; + margin: 2mm; + } + + .login-form button { + display: block; + padding: 2mm; + margin: 2mm 0; + width: 100%; + box-sizing: border-box; + } "#; fn main() { env_logger::init(); - let entry = keyring::Entry::new("dergchat", "dergchat").expect("Failed to create keyring entry"); - - entry.set_password("topS3cr3tP4$$w0rd").expect("Failed to set password"); - let password = entry.get_password().expect("Failed to get password"); - println!("My password is '{}'", password); - entry.delete_password().expect("Failed to delete password"); - hot_reload_init!(); // launch the dioxus app in a webview - dioxus_desktop::launch_cfg(App, - Config::default() - .with_custom_head(format!(r#""#, STYLESHEET)) - .with_background_color((32, 64, 32, 255))); -} - -#[derive(Props)] -struct RoomListProps<'a> { - rooms: Vec, - on_room_picked: EventHandler<'a, BareJid>, - on_room_left: EventHandler<'a, BareJid>, -} - -/// A Dioxus component that renders a list of rooms -fn RoomList<'a>(cx: Scope<'a, RoomListProps>) -> Element<'a> { - render! { - ul { - list_style: "none", - flex_grow: 1, - margin: 0, - padding: 0, - for room in cx.props.rooms.iter() { - rsx! { li { - display: "flex", - flex_direction: "row", - - onclick: |evt| cx.props.on_room_picked.call(room.to_owned()), - - div { - flex_grow: 1, - "{room}" - } - - button { - - onclick: |evt| { - evt.stop_propagation(); - cx.props.on_room_left.call(room.to_owned()); - }, - "X" - } - } } - } - } - } -} - -#[derive(Debug, Clone)] -struct Message { - sender: String, - body: String, -} - -#[derive(Props)] -struct RoomViewProps<'a> { - room: BareJid, - messages: Vec, - on_message_sent: EventHandler<'a, String>, -} - -#[derive(Props)] -struct SendMessageProps<'a> { - on_message_sent: EventHandler<'a, String>, -} - -fn RoomView<'a>(cx: Scope<'a, RoomViewProps>) -> Element<'a> { - - let message = use_state(cx, || "".to_owned()); - - render! { - div { - padding: "5mm", - flex_grow: 1, - display: "flex", - background_color: "#166322", - flex_direction: "column", - h2 { - margin: 0, - padding: 0, - border_bottom: "1px solid lightgray", - "{cx.props.room}" - } - ul { - flex_grow: 1, - overflow_y: "scroll", - for message in cx.props.messages.iter() { - rsx! { li { - "{message.sender}: {message.body}" - } } - } - } - SendMessage { - on_message_sent: |x:String| cx.props.on_message_sent.call(x), - } - } - } -} - -fn SendMessage<'a>(cx: Scope<'a, SendMessageProps>) -> Element<'a> { - - let message = use_state(cx, || "".to_owned()); - - render! { - div { - display: "flex", - flex_direction: "row", - textarea { - resize: "none", - flex_grow: 1, - oninput: |evt| { - message.set(evt.value.clone()); - } - } - button { - height: "100%", - onclick: move |_| { - cx.props.on_message_sent.call(message.current().to_string()); - }, - "Send" - } - } - } -} - -async fn handle_event(event: xmpp::Event, - agent: &mut xmpp::Agent, - messages: &mut UseRef>>) { - match event { - xmpp::Event::JoinRoom(jid, conference) => { - 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); }); - } - xmpp::Event::ChatMessage(id, sender, body) => { - 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, - }); - }); - } - xmpp::Event::RoomMessage(id, room_jid, sender_nick, body) => { - 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, - })); - } - xmpp::Event::RoomPrivateMessage(id, room_jid, sender_nick, body) => { - 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); - } - } -} - -#[derive(Debug)] -enum NetworkCommand { - JoinRoom { room: BareJid }, - LeaveRoom { room: BareJid }, - SendMessage { recipient: BareJid, message: String }, -} - -#[derive(Props)] -struct RoomJoinProps<'a> { - on_join_room: EventHandler<'a, String>, -} - -fn RoomJoinWidget<'a>(cx: Scope<'a, RoomJoinProps>) -> Element<'a> { - - let room = use_state(cx, || "".to_owned()); - - render! { - div { - display: "flex", - flex_direction: "row", - input { - flex_grow: 1, - display: "block", - oninput: |evt| { - room.set(evt.value.clone()); - } - } - button { - onclick: move |_| cx.props.on_join_room.call(room.current().to_string()), - "Join" - } - } - } -} - -// define a component that renders a div with the text "Hello, world!" -fn App(cx: Scope) -> Element { - - let messages = use_ref(cx, || HashMap::new()); - let current_room = use_state(cx, || None::); - - let cr = use_coroutine(cx, - |rx: UnboundedReceiver| run_xmpp(messages.clone(), rx), + dioxus_desktop::launch_cfg( + App, + Config::default() + .with_custom_head(format!(r#""#, STYLESHEET)) + .with_background_color((32, 64, 32, 255)), ); - - render! { - div { - margin: 0, - padding: 0, - display: "flex", - width: "100%", - height: "100%", - - div { - padding: "5mm", - background_color: "#1d852d", - display: "flex", - flex_direction: "column", - div { - border_bottom: "1px solid lightgray", - "Mizah" - } - RoomList { - rooms: messages.read().keys().cloned().collect(), - on_room_picked: move |x:BareJid| { - println!("Room selected: {:?}", x); - current_room.set(Some(x)); - }, - on_room_left: move |x:BareJid| { - println!("Leaving room: {:?}", x); - cr.send(NetworkCommand::LeaveRoom { room: x }); - }, - } - RoomJoinWidget { - on_join_room: move |x:String| { - println!("Joining room: {:?}", x); - cr.send(NetworkCommand::JoinRoom { room: BareJid::from_str(&x).unwrap() }); - }, - } - } - - if let Some(room) = current_room.get() { - let messages = messages.read().get(&room).expect("Selected non-existant room").to_vec(); - - rsx! { - RoomView { - room: room.clone(), - messages: messages, - on_message_sent: move |x:String| { - println!("Message sent: {:?}", x); - cr.send(NetworkCommand::SendMessage { recipient: room.clone(), message: x }); - }, - } - } - - } else { - - rsx! { - div { - padding: "5mm", - flex_grow: 1, - display: "flex", - background_color: "#166322", - "No room selected. Pick one from the list on the left." - } - } - - } - } - } } - -fn run_xmpp(mut room_data: UseRef>>, - mut commands: UnboundedReceiver) -> impl Future + Sized { - - async move { - - let jid = BareJid::from_str("bot@mizah.xyz").unwrap(); - - let password = "TLOwnPNPDGN9nfRRqPwh"; - - let mut agent = ClientBuilder::new(jid, &password) - .set_client(ClientType::Pc, "dergchat") - .set_default_nick("dergchat") - .build(); - - loop { - select! { - events = agent.wait_for_events() => { - if let Some(events) = events { - for event in events { - handle_event(event, &mut 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; - }, - } - } else { - info!("Command channel closed"); - break; - } - }, - } - } - } -} - diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..640e64b --- /dev/null +++ b/src/types.rs @@ -0,0 +1,29 @@ +use jid::BareJid; +use std::fmt::Debug; + +#[derive(Debug, Clone, PartialEq)] +pub struct Room { + pub messages: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Message { + pub sender: String, + pub body: String, +} + +pub struct LoginCredentials { + pub username: BareJid, + pub default_nick: String, + pub password: String, +} + +impl Debug for LoginCredentials { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("LoginCredentials") + .field("username", &self.username) + .field("default_nick", &self.default_nick) + .field("password", &"********") + .finish() + } +} diff --git a/src/widgets/login_screen.rs b/src/widgets/login_screen.rs new file mode 100644 index 0000000..92d3573 --- /dev/null +++ b/src/widgets/login_screen.rs @@ -0,0 +1,136 @@ +use crate::types::LoginCredentials; +use dioxus::core::{Element, Scope}; +use dioxus::hooks::use_state; +use dioxus::prelude::*; +use dioxus_elements::div; +use jid::BareJid; +use std::fmt::Debug; +use std::str::FromStr; + +#[derive(Debug, Clone, PartialEq)] +pub enum LoginStatus { + LoggedOut, + LoggingIn, + LoggedIn, + Error(String), +} + +// fn login_credentials_from_strings( +// username: String, +// default_nick: String, +// password: String, +// ) -> Result { +// let username = BareJid::from_str(&username).map_err(|_| "Invalid username".to_string())?; +// let password = password; +// +// // Default nick must be non-empty +// let default_nick = if default_nick.is_empty() { +// Err("Default nick must be non-empty".to_string()) +// } else { +// Ok(default_nick) +// }?; +// +// Ok(LoginCredentials { +// username, +// password, +// default_nick, +// }) +// } + +pub struct LoginAttempt { + pub username: String, + pub default_nick: String, + pub password: String, +} + +impl Debug for LoginAttempt { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("LoginAttempt") + .field("username", &self.username) + .field("default_nick", &self.default_nick) + .field("password", &"********") + .finish() + } +} + +#[derive(Props)] +pub struct LoginScreenProps<'a> { + login_state: LoginStatus, + on_login_attempt: EventHandler<'a, LoginAttempt>, +} + +pub fn LoginScreen<'a>(cx: Scope<'a, LoginScreenProps>) -> Element<'a> { + let username = use_state(cx, || "".to_string()); + let default_nick = use_state(cx, || "".to_string()); + let password = use_state(cx, || "".to_string()); + let default_nick_was_changed = use_state(cx, || false); + + render! { + + div { + class: "login-form", + input { + "type": "text", + placeholder: "Username", + oninput: move |x| { + username.set(x.value.clone()); + + if !default_nick_was_changed.get() { + if let Some(before_at) = x.value.split('@').next() { + default_nick.set(before_at.to_string()); + } + } + }, + } + + input { + placeholder: "Default nick", + "type": "text", + oninput: move |x| { + default_nick.set(x.value.clone()); + default_nick_was_changed.set(true); + }, + } + + input { + placeholder: "Password", + "type": "password", + oninput: move |x| { + password.set(x.value.clone()); + }, + } + + if let LoginStatus::Error(e) = &cx.props.login_state { + rsx! { + div { + class: "error", + "{e}" + } + } + } + + rsx! { + button { + + class: match cx.props.login_state { + LoginStatus::LoggedOut => "login-button logged-out", + LoginStatus::LoggingIn => "login-button logging-in", + LoginStatus::LoggedIn => "login-button logged-in", + LoginStatus::Error(_) => "error", + }, + + onclick: move |_| { + cx.props.on_login_attempt.call(LoginAttempt { + username: username.current().to_string(), + default_nick: default_nick.current().to_string(), + password: password.current().to_string(), + }); + }, + + "Login" + } + + } + } + } +} diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs new file mode 100644 index 0000000..a531f6a --- /dev/null +++ b/src/widgets/mod.rs @@ -0,0 +1,181 @@ +use crate::types::{LoginCredentials, Message}; +use crate::widgets::login_screen::LoginAttempt; +use crate::widgets::login_screen::{LoginScreen, LoginStatus}; +use crate::widgets::room_join_widget::{RoomJoinProps, RoomJoinWidget}; +use crate::widgets::room_list::{RoomList, RoomListProps}; +use crate::widgets::room_view::RoomView; +use crate::xmpp_interface::xmpp_mainloop; +use crate::xmpp_interface::NetworkCommand; +use dioxus::core::{Element, Scope}; +use dioxus::core_macro::Props; +use dioxus::prelude::*; +use futures_util::StreamExt; +use jid::BareJid; +use log::{error, info}; +use std::collections::HashMap; +use std::fmt::format; +use std::str::FromStr; +use xmpp::{Agent, ClientBuilder, ClientType}; + +mod login_screen; +mod room_join_widget; +mod room_list; +mod room_view; +mod send_message; + +async fn await_online(agent: &mut Agent) -> Result<(), String> { + let events = agent + .wait_for_events() + .await + .ok_or("Stream closed unexpectedly".to_string())?; + + assert_eq!(events.len(), 1); + + match events.into_iter().next().expect("No events") { + xmpp::Event::Online => { + info!("Online"); + Ok(()) + } + xmpp::Event::Disconnected => { + error!("Disconnected"); + Err("Disconnected".to_string()) + } + e => return Err(format!("Unexpected event: {:?}", e)), + } +} +pub async fn run_xmpp_toplevel( + connection_status: UseState, + mut messages: UseRef>>, + mut commands: UnboundedReceiver, +) { + + // 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"); + } +} + +pub fn App(cx: Scope) -> Element { + let messages = use_ref(cx, || HashMap::new()); + let current_room = use_state(cx, || None::); + let connection_status = use_state(cx, || LoginStatus::LoggedOut); + + let coroutine = use_coroutine(cx, |rx: UnboundedReceiver| + run_xmpp_toplevel(connection_status.to_owned(), messages.to_owned(), rx)); + + render! { + + // If not logged in, show a login screen. + if connection_status.get() != &LoginStatus::LoggedIn { + rsx! { + LoginScreen { + login_state: connection_status.current().as_ref().clone(), + on_login_attempt: move |x: LoginAttempt| { + coroutine.send(NetworkCommand::TryLogin { + credentials: LoginCredentials { + username: BareJid::from_str(&x.username).expect("Invalid JID"), + default_nick: x.default_nick, + password: x.password, + }, + }); + }, + } + } + } else { + + // We're logged in; show a sidebar and a room view. + + rsx!{ + + // Sidebar. + div { + padding: "5mm", + background_color: "#1d852d", + display: "flex", + flex_direction: "column", + + div { + border_bottom: "1px solid lightgray", + "Mizah" + } + + RoomList { + rooms: messages.read().keys().cloned().collect(), + on_room_picked: move |x: BareJid| { + current_room.set(Some(x)); + }, + on_room_left: move |x: BareJid| { + coroutine.send(NetworkCommand::LeaveRoom { room: x }); + }, + } + + RoomJoinWidget { + on_join_room: move |x: String| { + coroutine.send(NetworkCommand::JoinRoom { + room: BareJid::from_str(&x).expect("Invalid JID"), + }); + }, + } + } + + // Room view. + + if let Some(room) = current_room.get() { + let messages = messages.read().get(&room).expect("Selected non-existant room").to_vec(); + + rsx! { + RoomView { + room: room.clone(), + messages: messages, + on_message_sent: move |x:String| { + println!("Message sent: {:?}", x); + coroutine.send(NetworkCommand::SendMessage { recipient: room.clone(), message: x }); + }, + } + } + } else { + // No room selected + + rsx! { + div { + padding: "5mm", + flex_grow: 1, + display: "flex", + background_color: "#166322", + "No room selected. Pick one from the list on the left." + } + } + + } + + } + + + } + + } +} \ No newline at end of file diff --git a/src/widgets/room_join_widget.rs b/src/widgets/room_join_widget.rs new file mode 100644 index 0000000..e5ae3a2 --- /dev/null +++ b/src/widgets/room_join_widget.rs @@ -0,0 +1,31 @@ +use dioxus::core::{Element, Scope}; +use dioxus::core_macro::Props; +use dioxus::hooks::use_state; +use dioxus::prelude::*; + +#[derive(Props)] +pub struct RoomJoinProps<'a> { + on_join_room: EventHandler<'a, String>, +} + +pub fn RoomJoinWidget<'a>(cx: Scope<'a, RoomJoinProps>) -> Element<'a> { + let room = use_state(cx, || "".to_owned()); + + render! { + div { + display: "flex", + flex_direction: "row", + input { + flex_grow: 1, + display: "block", + oninput: |evt| { + room.set(evt.value.clone()); + } + } + button { + onclick: move |_| cx.props.on_join_room.call(room.current().to_string()), + "Join" + } + } + } +} diff --git a/src/widgets/room_list.rs b/src/widgets/room_list.rs new file mode 100644 index 0000000..88f9fdd --- /dev/null +++ b/src/widgets/room_list.rs @@ -0,0 +1,45 @@ +use dioxus::core::{Element, Scope}; +use dioxus::core_macro::Props; +use dioxus::prelude::*; +use jid::BareJid; + +#[derive(Props)] +pub struct RoomListProps<'a> { + rooms: Vec, + on_room_picked: EventHandler<'a, BareJid>, + on_room_left: EventHandler<'a, BareJid>, +} + +/// A Dioxus component that renders a list of rooms +pub fn RoomList<'a>(cx: Scope<'a, RoomListProps>) -> Element<'a> { + render! { + ul { + list_style: "none", + flex_grow: 1, + margin: 0, + padding: 0, + for room in cx.props.rooms.iter() { + rsx! { li { + display: "flex", + flex_direction: "row", + + onclick: |evt| cx.props.on_room_picked.call(room.to_owned()), + + div { + flex_grow: 1, + "{room}" + } + + button { + + onclick: |evt| { + evt.stop_propagation(); + cx.props.on_room_left.call(room.to_owned()); + }, + "X" + } + } } + } + } + } +} diff --git a/src/widgets/room_view.rs b/src/widgets/room_view.rs new file mode 100644 index 0000000..ec12d3c --- /dev/null +++ b/src/widgets/room_view.rs @@ -0,0 +1,46 @@ +use crate::types::Message; +use crate::widgets::send_message::SendMessage; +use dioxus::core::{Element, Scope}; +use dioxus::core_macro::Props; +use dioxus::hooks::use_state; +use dioxus::prelude::*; +use jid::BareJid; + +#[derive(Props)] +pub struct RoomViewProps<'a> { + room: BareJid, + messages: Vec, + on_message_sent: EventHandler<'a, String>, +} + +pub fn RoomView<'a>(cx: Scope<'a, RoomViewProps>) -> Element<'a> { + let message = use_state(cx, || "".to_owned()); + + render! { + div { + padding: "5mm", + flex_grow: 1, + display: "flex", + background_color: "#166322", + flex_direction: "column", + h2 { + margin: 0, + padding: 0, + border_bottom: "1px solid lightgray", + "{cx.props.room}" + } + ul { + flex_grow: 1, + overflow_y: "scroll", + for message in cx.props.messages.iter() { + rsx! { li { + "{message.sender}: {message.body}" + } } + } + } + SendMessage { + on_message_sent: |x:String| cx.props.on_message_sent.call(x), + } + } + } +} diff --git a/src/widgets/send_message.rs b/src/widgets/send_message.rs new file mode 100644 index 0000000..25d5524 --- /dev/null +++ b/src/widgets/send_message.rs @@ -0,0 +1,34 @@ +use dioxus::core::{Element, Scope}; +use dioxus::core_macro::Props; +use dioxus::hooks::use_state; +use dioxus::prelude::*; + +#[derive(Props)] +pub struct SendMessageProps<'a> { + on_message_sent: EventHandler<'a, String>, +} + +pub fn SendMessage<'a>(cx: Scope<'a, SendMessageProps>) -> Element<'a> { + let message = use_state(cx, || "".to_owned()); + + render! { + div { + display: "flex", + flex_direction: "row", + textarea { + resize: "none", + flex_grow: 1, + oninput: |evt| { + message.set(evt.value.clone()); + } + } + button { + height: "100%", + onclick: move |_| { + cx.props.on_message_sent.call(message.current().to_string()); + }, + "Send" + } + } + } +} diff --git a/src/xmpp_interface.rs b/src/xmpp_interface.rs new file mode 100644 index 0000000..427cc93 --- /dev/null +++ b/src/xmpp_interface.rs @@ -0,0 +1,127 @@ +use crate::types::{LoginCredentials, Message}; +use dioxus::hooks::{UnboundedReceiver, UseRef}; +use futures_util::stream::StreamExt; +use jid::BareJid; +use log::{error, info}; +use std::collections::HashMap; +use std::future::Future; +use std::str::FromStr; +use tokio::select; +use xmpp::parsers::message::MessageType; +use xmpp::{Agent, ClientBuilder, ClientType}; + +async fn handle_event( + event: xmpp::Event, + agent: &mut xmpp::Agent, + messages: &mut UseRef>>, +) { + match event { + xmpp::Event::JoinRoom(jid, conference) => { + 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); + }); + } + xmpp::Event::ChatMessage(id, sender, body) => { + 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, + }); + }); + } + xmpp::Event::RoomMessage(id, room_jid, sender_nick, body) => { + 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, + }) + }); + } + xmpp::Event::RoomPrivateMessage(id, room_jid, sender_nick, body) => { + 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); + } + } +} + +#[derive(Debug)] +pub enum NetworkCommand { + TryLogin { credentials: LoginCredentials }, + JoinRoom { room: BareJid }, + LeaveRoom { room: BareJid }, + SendMessage { recipient: BareJid, message: String }, +} + +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."); + }, + } + } else { + info!("Command channel closed"); + break; + } + }, + } + } + } +}