diff --git a/Cargo.lock b/Cargo.lock index 745daae..128f43a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2340,8 +2340,10 @@ dependencies = [ "keyring", "luz", "serde", + "thiserror 2.0.11", "tokio", "tokio-stream", + "toml", "tracing", "tracing-subscriber", "uuid", diff --git a/Cargo.toml b/Cargo.toml index f2c6a34..3d1dcbe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,3 +16,5 @@ keyring = { version = "3", features = ["apple-native", "windows-native", "sync-s uuid = { version = "1.13.1", features = ["v4"] } indexmap = "2.7.1" serde = { version = "1.0.218", features = ["derive"] } +thiserror = "2.0.11" +toml = "0.8" diff --git a/src/login_modal.rs b/src/login_modal.rs index f4e556e..02d878e 100644 --- a/src/login_modal.rs +++ b/src/login_modal.rs @@ -4,10 +4,12 @@ use iced::{ Element, Task, }; use jid::JID; +use keyring::Entry; use luz::{ presence::{Offline, Presence}, LuzHandle, }; +use serde::{Deserialize, Serialize}; use tokio_stream::wrappers::ReceiverStream; use tracing::info; @@ -41,6 +43,12 @@ pub enum Action { ClientCreated(Task), } +#[derive(Serialize, Deserialize)] +pub struct Creds { + pub jid: String, + pub password: String, +} + impl LoginModal { pub fn update(&mut self, message: Message) -> Action { match message { @@ -60,6 +68,7 @@ impl LoginModal { info!("submitting login"); let jid_str = self.jid.clone(); let password = self.password.clone(); + let remember_me = self.remember_me.clone(); Action::ClientCreated( Task::future(async move { let jid: Result = jid_str.parse(); @@ -70,19 +79,56 @@ impl LoginModal { .await; match result { Ok((luz_handle, receiver)) => { - let stream = ReceiverStream::new(receiver); - let stream = - stream.map(|message| crate::Message::Luz(message)); - vec![ - Task::done(crate::Message::ClientCreated(Client { + let mut tasks = Vec::new(); + tasks.push(Task::done(crate::Message::ClientCreated( + Client { client: luz_handle, jid: j, connection_status: Presence::Offline( Offline::default(), ), - })), - Task::stream(stream), - ] + }, + ))); + let stream = ReceiverStream::new(receiver); + let stream = + stream.map(|message| crate::Message::Luz(message)); + tasks.push(Task::stream(stream)); + + if remember_me { + let entry = Entry::new("macaw", "macaw"); + match entry { + Ok(e) => { + let creds = Creds { + jid: jid_str, + password, + }; + let creds = toml::to_string(&creds); + match creds { + Ok(c) => { + let result = e.set_password(&c); + if let Err(e) = result { + tasks.push(Task::done(crate::Message::Error( + crate::Error::CredentialsSave(e.into()), + ))); + } + } + Err(e) => tasks.push(Task::done( + crate::Message::Error( + crate::Error::CredentialsSave( + e.into(), + ), + ), + )), + } + } + Err(e) => { + tasks.push(Task::done(crate::Message::Error( + crate::Error::CredentialsSave(e.into()), + ))) + } + } + } + tasks } Err(_e) => { tracing::error!("error (database probably)"); diff --git a/src/main.rs b/src/main.rs index d583293..8e38e7b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ use std::borrow::Cow; use std::collections::{HashMap, HashSet}; use std::fmt::Debug; use std::ops::{Deref, DerefMut}; +use std::sync::Arc; use iced::futures::{SinkExt, Stream, StreamExt}; use iced::widget::button::Status; @@ -15,12 +16,13 @@ use iced::{stream, Color, Element, Subscription, Task, Theme}; use indexmap::{indexmap, IndexMap}; use jid::JID; use keyring::Entry; -use login_modal::LoginModal; +use login_modal::{Creds, LoginModal}; use luz::chat::{Chat, Message as ChatMessage}; use luz::presence::{Offline, Presence}; use luz::CommandMessage; use luz::{roster::Contact, user::User, LuzHandle, UpdateMessage}; use serde::{Deserialize, Serialize}; +use thiserror::Error; use tokio::sync::{mpsc, oneshot}; use tokio_stream::wrappers::ReceiverStream; use tracing::{error, info}; @@ -58,11 +60,6 @@ pub struct OpenChat { pub struct NewChat; -pub struct Creds { - jid: String, - password: String, -} - impl Macaw { pub fn new(client: Option, config: Config) -> Self { let account; @@ -112,11 +109,60 @@ impl Deref for Client { } } -fn main() -> iced::Result { +#[tokio::main] +async fn main() -> iced::Result { tracing_subscriber::fmt::init(); let cfg = confy::load("macaw", None).unwrap(); - let client: Option<(JID, LuzHandle, mpsc::Receiver)> = None; + let entry = Entry::new("macaw", "macaw"); + let mut client_creation_error: Option = None; + let mut creds: Option = None; + + match entry { + Ok(e) => { + let result = e.get_password(); + match result { + Ok(c) => { + let result = toml::from_str(&c); + match result { + Ok(c) => creds = Some(c), + Err(e) => { + client_creation_error = + Some(Error::CredentialsLoad(CredentialsLoadError::Toml(e.into()))) + } + } + } + Err(e) => match e { + keyring::Error::NoEntry => {} + _ => { + client_creation_error = Some(Error::CredentialsLoad( + CredentialsLoadError::Keyring(e.into()), + )) + } + }, + } + } + Err(e) => { + client_creation_error = Some(Error::CredentialsLoad(CredentialsLoadError::Keyring( + e.into(), + ))) + } + } + + let mut client: Option<(JID, LuzHandle, mpsc::Receiver)> = None; + if let Some(creds) = creds { + let jid = creds.jid.parse::(); + match jid { + Ok(jid) => { + let luz = LuzHandle::new(jid.clone(), creds.password.to_string(), "macaw.db").await; + match luz { + Ok((handle, recv)) => client = Some((jid.as_bare(), handle, recv)), + Err(e) => client_creation_error = Some(Error::ClientCreation(e)), + } + } + Err(e) => client_creation_error = Some(Error::CredentialsLoad(e.into())), + } + } if let Some((jid, luz_handle, update_recv)) = client { let stream = ReceiverStream::new(update_recv); @@ -137,8 +183,13 @@ fn main() -> iced::Result { ) }) } else { - iced::application("Macaw", Macaw::update, Macaw::view) - .run_with(|| (Macaw::new(None, cfg), Task::none())) + if let Some(e) = client_creation_error { + iced::application("Macaw", Macaw::update, Macaw::view) + .run_with(|| (Macaw::new(None, cfg), Task::done(Message::Error(e)))) + } else { + iced::application("Macaw", Macaw::update, Macaw::view) + .run_with(|| (Macaw::new(None, cfg), Task::none())) + } } } @@ -156,6 +207,47 @@ pub enum Message { CloseChat(JID), MessageCompose(String), SendMessage(JID, String), + Error(Error), +} + +#[derive(Debug, Error, Clone)] +pub enum Error { + #[error("failed to create Luz client: {0}")] + ClientCreation(#[from] luz::error::DatabaseError), + #[error("failed to save credentials: {0}")] + CredentialsSave(CredentialsSaveError), + #[error("failed to load credentials: {0}")] + CredentialsLoad(CredentialsLoadError), +} + +#[derive(Debug, Error, Clone)] +pub enum CredentialsSaveError { + #[error("keyring: {0}")] + Keyring(Arc), + #[error("toml serialisation: {0}")] + Toml(#[from] toml::ser::Error), +} + +impl From for CredentialsSaveError { + fn from(e: keyring::Error) -> Self { + Self::Keyring(Arc::new(e)) + } +} + +#[derive(Debug, Error, Clone)] +pub enum CredentialsLoadError { + #[error("keyring: {0}")] + Keyring(Arc), + #[error("toml serialisation: {0}")] + Toml(#[from] toml::de::Error), + #[error("invalid jid: {0}")] + JID(#[from] jid::ParseError), +} + +impl From for CredentialsLoadError { + fn from(e: keyring::Error) -> Self { + Self::Keyring(Arc::new(e)) + } } impl Macaw { @@ -358,6 +450,7 @@ impl Macaw { ) .discard() } + Message::Error(error) => todo!("error notification toasts, logging?"), } }