aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar cel 🌸 <cel@bunny.garden>2025-03-01 06:56:17 +0000
committerLibravatar cel 🌸 <cel@bunny.garden>2025-03-01 06:56:39 +0000
commita938d39dc5b21298f0d1c7829a87dbc8331917b6 (patch)
tree9b9545237f56cca744636251e744ea643c8453bc
parent66e12f72644ed06f8bd219bfb3098f9fa53d7be8 (diff)
downloadmacaw-a938d39dc5b21298f0d1c7829a87dbc8331917b6.tar.gz
macaw-a938d39dc5b21298f0d1c7829a87dbc8331917b6.tar.bz2
macaw-a938d39dc5b21298f0d1c7829a87dbc8331917b6.zip
implement credential saving with cross-platform keyring
-rw-r--r--Cargo.lock2
-rw-r--r--Cargo.toml2
-rw-r--r--src/login_modal.rs62
-rw-r--r--src/main.rs113
4 files changed, 161 insertions, 18 deletions
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<crate::Message>),
}
+#[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, _> = 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<Client>, 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<UpdateMessage>)> = None;
+ let entry = Entry::new("macaw", "macaw");
+ let mut client_creation_error: Option<Error> = None;
+ let mut creds: Option<Creds> = 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<UpdateMessage>)> = None;
+ if let Some(creds) = creds {
+ let jid = creds.jid.parse::<JID>();
+ 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<keyring::Error>),
+ #[error("toml serialisation: {0}")]
+ Toml(#[from] toml::ser::Error),
+}
+
+impl From<keyring::Error> 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<keyring::Error>),
+ #[error("toml serialisation: {0}")]
+ Toml(#[from] toml::de::Error),
+ #[error("invalid jid: {0}")]
+ JID(#[from] jid::ParseError),
+}
+
+impl From<keyring::Error> 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?"),
}
}