From 5a2fae397cb0269cdb2b9ce5ded742a78d58cdef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?cel=20=F0=9F=8C=B8?= Date: Mon, 24 Feb 2025 15:06:58 +0000 Subject: [PATCH] implement log-in modal --- .gitignore | 1 + Cargo.lock | 115 +++++++++++++ Cargo.toml | 2 +- src/main.rs | 478 ++++++++++++++++++++++++++++++++++++++++++---------- 4 files changed, 502 insertions(+), 94 deletions(-) diff --git a/.gitignore b/.gitignore index d2c28ba..8c308d0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target +.vscode macaw.db diff --git a/Cargo.lock b/Cargo.lock index 994388f..a58f241 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,6 +33,17 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "ahash" version = "0.7.8" @@ -371,6 +382,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + [[package]] name = "block2" version = "0.5.1" @@ -463,6 +483,15 @@ dependencies = [ "wayland-client", ] +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + [[package]] name = "cc" version = "1.2.13" @@ -498,6 +527,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "circular" version = "0.3.0" @@ -1980,6 +2019,16 @@ dependencies = [ "hashbrown 0.15.2", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + [[package]] name = "instant" version = "0.1.13" @@ -2254,6 +2303,7 @@ dependencies = [ "iced", "jid", "luz", + "secret-service", "tokio", "tokio-stream", "tracing", @@ -2475,6 +2525,30 @@ dependencies = [ "winapi", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -2492,6 +2566,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -2512,6 +2595,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -3491,6 +3585,25 @@ dependencies = [ "tiny-skia", ] +[[package]] +name = "secret-service" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4d35ad99a181be0a60ffcbe85d680d98f87bdc4d7644ade319b87076b9dbfd4" +dependencies = [ + "aes", + "cbc", + "futures-util", + "generic-array", + "hkdf", + "num", + "once_cell", + "rand", + "serde", + "sha2", + "zbus", +] + [[package]] name = "security-framework" version = "2.11.1" @@ -4246,6 +4359,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", + "tracing", "windows-sys 0.52.0", ] @@ -5454,6 +5568,7 @@ dependencies = [ "serde_repr", "sha1", "static_assertions", + "tokio", "tracing", "uds_windows", "windows-sys 0.52.0", diff --git a/Cargo.toml b/Cargo.toml index 13f2e6f..5c1322b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,4 +11,4 @@ tokio = "1.43.0" tokio-stream = "0.1.17" tracing-subscriber = "0.3.19" tracing = "0.1.41" - +secret-service = { version = "4.0.0", features = ["rt-tokio-crypto-rust"] } diff --git a/src/main.rs b/src/main.rs index b2308fb..1a940fd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,38 +1,127 @@ +use std::borrow::Cow; use std::collections::{HashMap, HashSet}; +use std::fmt::Debug; +use std::ops::{Deref, DerefMut}; use iced::futures::{SinkExt, Stream, StreamExt}; -use iced::widget::{button, column, row, text, text_input}; -use iced::{stream, Element, Subscription, Task, Theme}; +use iced::widget::text::{Fragment, IntoFragment}; +use iced::widget::{ + button, center, column, container, mouse_area, opaque, row, stack, text, text_input, Text, +}; +use iced::{stream, Color, Element, Subscription, Task, Theme}; use jid::JID; use luz::chat::{Chat, Message as ChatMessage}; use luz::presence::{Offline, Presence}; use luz::CommandMessage; use luz::{roster::Contact, user::User, LuzHandle, UpdateMessage}; -use tokio::sync::oneshot; +use tokio::sync::{mpsc, oneshot}; use tokio_stream::wrappers::ReceiverStream; +use tracing::info; -#[derive(Default)] pub struct Macaw { - client: Option, + client: Account, roster: HashMap, users: HashMap, presences: HashMap, chats: HashMap)>, subscription_requests: HashSet, - connection_status: Option, +} + +pub struct Creds { + jid: String, + password: String, +} + +impl Macaw { + pub fn new(client: Option) -> Self { + let account; + if let Some(client) = client { + account = Account::LoggedIn(client); + } else { + account = Account::LoggedOut { + jid: "".to_string(), + password: "".to_string(), + error: None, + }; + } + + Self { + client: account, + roster: HashMap::new(), + users: HashMap::new(), + presences: HashMap::new(), + chats: HashMap::new(), + subscription_requests: HashSet::new(), + } + } +} + +pub enum Account { + LoggedIn(Client), + LoggedOut { + jid: String, + password: String, + error: Option, + }, +} + +#[derive(Debug, Clone)] +pub enum Error { + InvalidJID(String), + DatabaseConnection, +} + +#[derive(Clone, Debug)] +pub struct Client { + client: LuzHandle, + jid: JID, + connection_status: Presence, +} + +impl DerefMut for Client { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.client + } +} + +impl Deref for Client { + type Target = LuzHandle; + + fn deref(&self) -> &Self::Target { + &self.client + } } fn main() -> iced::Result { tracing_subscriber::fmt::init(); - iced::application("Macaw", Macaw::update, Macaw::view) - .subscription(Macaw::subscription) - .run() + let client: Option<(JID, LuzHandle, mpsc::Receiver)> = None; + + if let Some((jid, luz_handle, update_recv)) = client { + let stream = ReceiverStream::new(update_recv); + let stream = stream.map(|message| Message::Luz(message)); + iced::application("Macaw", Macaw::update, Macaw::view).run_with(|| { + ( + Macaw::new(Some(Client { + client: luz_handle, + // TODO: + jid, + connection_status: Presence::Offline(Offline::default()), + })), + // TODO: autoconnect config + Task::stream(stream), + ) + }) + } else { + iced::application("Macaw", Macaw::update, Macaw::view) + .run_with(|| (Macaw::new(None), Task::none())) + } } #[derive(Debug, Clone)] enum Message { - ClientCreated(LuzHandle), + LoginModal(LoginModalMessage), + ClientCreated(Client), Luz(UpdateMessage), Roster(HashMap), Connect, @@ -40,27 +129,15 @@ enum Message { OpenChat(JID), } +#[derive(Debug, Clone)] +enum LoginModalMessage { + JID(String), + Password(String), + Submit, + Error(Error), +} + impl Macaw { - fn stream() -> impl Stream { - stream::channel(100, |mut output| async { - let (luz, recv) = LuzHandle::new( - "test@blos.sm".try_into().unwrap(), - "slayed".to_string(), - "./macaw.db", - ) - .await - .unwrap(); - output.send(Message::ClientCreated(luz)).await; - let stream = ReceiverStream::new(recv); - let stream = stream.map(|message| Message::Luz(message)).map(Ok); - stream.forward(output).await; - }) - } - - fn subscription(&self) -> Subscription { - Subscription::run(Macaw::stream) - } - fn update(&mut self, message: Message) -> Task { match message { Message::Luz(update_message) => match update_message { @@ -68,18 +145,35 @@ impl Macaw { tracing::error!("Luz error: {:?}", error); Task::none() } - UpdateMessage::Online(online, vec) => { - self.connection_status = Some(Presence::Online(online)); - let mut roster = HashMap::new(); - for contact in vec { - roster.insert(contact.user_jid.clone(), contact); + UpdateMessage::Online(online, vec) => match &mut self.client { + Account::LoggedIn(client) => { + client.connection_status = Presence::Online(online); + let mut roster = HashMap::new(); + for contact in vec { + roster.insert(contact.user_jid.clone(), contact); + } + self.roster = roster; + Task::none() } - self.roster = roster; - Task::none() - } + Account::LoggedOut { + jid, + password, + error, + } => Task::none(), + }, UpdateMessage::Offline(offline) => { - self.connection_status = Some(Presence::Offline(offline)); - Task::none() + // TODO: update all contacts' presences to unknown (offline) + match &mut self.client { + Account::LoggedIn(client) => { + client.connection_status = Presence::Offline(offline); + Task::none() + } + Account::LoggedOut { + jid, + password, + error, + } => Task::none(), + } } UpdateMessage::FullRoster(vec) => { let mut macaw_roster = HashMap::new(); @@ -118,13 +212,13 @@ impl Macaw { Task::none() } }, - Message::ClientCreated(luz_handle) => { - let cloned: LuzHandle = luz_handle.clone(); - self.client = Some(cloned); + // TODO: NEXT + Message::ClientCreated(client) => { + self.client = Account::LoggedIn(client.clone()); let (send, recv) = oneshot::channel(); Task::perform( async move { - luz_handle.send(CommandMessage::GetRoster(send)).await; + client.client.send(CommandMessage::GetRoster(send)).await; recv.await }, |result| { @@ -141,64 +235,262 @@ impl Macaw { self.roster = hash_map; Task::none() } - Message::Connect => { - let client = self.client.clone(); - Task::future(async move { - client.clone().unwrap().send(CommandMessage::Connect).await; - }) - .discard() - } - Message::Disconnect => { - let client = self.client.clone(); - Task::future(async move { - client - .clone() - .unwrap() - .send(CommandMessage::Disconnect(Offline::default())) - .await; - }) - .discard() - } + Message::Connect => match &self.client { + Account::LoggedIn(client) => { + let client = client.client.clone(); + Task::future(async move { + client.send(CommandMessage::Connect).await; + }) + .discard() + } + Account::LoggedOut { + jid, + password, + error, + } => Task::none(), + }, + Message::Disconnect => match &self.client { + Account::LoggedIn(client) => { + let client = client.client.clone(); + Task::future(async move { + client + .send(CommandMessage::Disconnect(Offline::default())) + .await; + }) + .discard() + } + Account::LoggedOut { + jid, + password, + error, + } => Task::none(), + }, Message::OpenChat(jid) => todo!(), + Message::LoginModal(login_modal_message) => match login_modal_message { + LoginModalMessage::JID(j) => match &mut self.client { + Account::LoggedIn(_client) => Task::none(), + Account::LoggedOut { + jid, + password, + error, + } => { + *jid = j; + Task::none() + } + }, + LoginModalMessage::Password(p) => match &mut self.client { + Account::LoggedIn(_client) => Task::none(), + Account::LoggedOut { + jid, + password, + error, + } => { + *password = p; + Task::none() + } + }, + LoginModalMessage::Submit => match &self.client { + Account::LoggedIn(_client) => Task::none(), + Account::LoggedOut { + jid: jid_str, + password, + error, + } => { + info!("submitting login"); + let jid_str = jid_str.clone(); + let password = password.clone(); + Task::future(async move { + let jid: Result = jid_str.parse(); + match jid { + Ok(j) => { + let result = + LuzHandle::new(j.clone(), password.to_string(), "macaw.db") + .await; + match result { + Ok((luz_handle, receiver)) => { + let stream = ReceiverStream::new(receiver); + let stream = + stream.map(|message| Message::Luz(message)); + vec![ + Task::done(Message::ClientCreated(Client { + client: luz_handle, + jid: j, + connection_status: Presence::Offline( + Offline::default(), + ), + })), + Task::stream(stream), + ] + } + Err(e) => { + tracing::error!("error (database probably)"); + return vec![Task::done(Message::LoginModal( + LoginModalMessage::Error(Error::DatabaseConnection), + ))]; + } + } + } + Err(_) => { + tracing::error!("parsing jid"); + return vec![Task::done(Message::LoginModal( + LoginModalMessage::Error(Error::InvalidJID( + jid_str.to_string(), + )), + ))]; + } + } + }) + .then(|tasks| Task::batch(tasks)) + } + }, + LoginModalMessage::Error(e) => match &mut self.client { + Account::LoggedIn(_client) => Task::none(), + Account::LoggedOut { + jid, + password, + error, + } => { + tracing::error!("luz::new: {:?}", e); + *error = Some(e); + Task::none() + } + }, + }, } } fn view(&self) -> Element { - let mut contacts: Vec> = Vec::new(); - for (_, contact) in &self.roster { - contacts.push( - button(match &contact.user_jid.localpart { - Some(u) => u, - None => "no username", - }) - .on_press(Message::OpenChat(contact.user_jid.clone())) - .into(), - ); - } - let column = column(contacts); - let connection_status = match &self.connection_status { - Some(s) => match s { - Presence::Online(online) => "connected", - Presence::Offline(offline) => "disconnected", - }, - None => "no account", + let ui = { + let mut contacts: Vec> = Vec::new(); + for (_, contact) in &self.roster { + let jid: Cow<'_, str> = (&contact.user_jid).into(); + contacts.push( + button(text(jid)) + .on_press(Message::OpenChat(contact.user_jid.clone())) + .into(), + ); + } + let column = column(contacts); + let connection_status = match &self.client { + Account::LoggedIn(client) => match &client.connection_status { + Presence::Online(_online) => "online", + Presence::Offline(_offline) => "disconnected", + }, + Account::LoggedOut { + jid: _, + password: _, + error, + } => "disconnected", + }; + // match &self.client.as_ref().map(|client| &client.connection_status) { + // Some(s) => match s { + // Presence::Online(online) => "connected", + // Presence::Offline(offline) => "disconnected", + // }, + // None => "no account", + // }; + let client_jid: Cow<'_, str> = match &self.client { + Account::LoggedIn(client) => (&client.jid).into(), + Account::LoggedOut { + jid: _, + password: _, + error, + } => Cow::from("no account"), + // map(|client| (&client.jid).into()); + }; + column![ + row![ + text(client_jid), + text(connection_status), + button("connect").on_press(Message::Connect), + button("disconnect").on_press(Message::Disconnect) + ], + text("Buddy List:"), + // + // + column, + ] }; - column![ - row![ - text("test@blos.sm"), - text(connection_status), - button("connect").on_press(Message::Connect), - button("disconnect").on_press(Message::Disconnect) - ], - text("Buddy List:"), - // - // - column, - ] - .into() + + // temporarily center to fill space + let ui = center(ui).into(); + + match &self.client { + Account::LoggedIn(_client) => ui, + Account::LoggedOut { + jid, + password, + error, + } => { + let signup = container( + column![ + text("Log In").size(24), + column![ + column![ + text("JID").size(12), + text_input("berry@macaw.chat", &jid) + .on_input(|j| Message::LoginModal(LoginModalMessage::JID(j))) + .on_submit(Message::LoginModal(LoginModalMessage::Submit)) + .padding(5), + ] + .spacing(5), + column![ + text("Password").size(12), + text_input("", &password) + .on_input(|p| Message::LoginModal(LoginModalMessage::Password( + p + ))) + .on_submit(Message::LoginModal(LoginModalMessage::Submit)) + .secure(true) + .padding(5), + ] + .spacing(5), + button(text("Submit")) + .on_press(Message::LoginModal(LoginModalMessage::Submit)), + ] + .spacing(10) + ] + .spacing(20), + ) + .width(300) + .padding(10) + .style(container::rounded_box); + + // signup.into() + modal(ui, signup) + } + } } fn theme(&self) -> Theme { Theme::Dark } } + +fn modal<'a, Message>( + base: impl Into>, + content: impl Into>, + // on_blur: Message, +) -> Element<'a, Message> +where + Message: Clone + 'a, +{ + stack![ + base.into(), + opaque( + mouse_area(center(opaque(content)).style(|_theme| { + container::Style { + background: Some( + Color { + a: 0.8, + ..Color::BLACK + } + .into(), + ), + ..container::Style::default() + } + })) // .on_press(on_blur) + ) + ] + .into() +}