Did a bunch of refactoring.

This commit is contained in:
Werner Kroneman 2023-12-10 17:41:27 +01:00
parent cbb307e8a2
commit 5a33bf2280
8 changed files with 117 additions and 83 deletions

1
Cargo.lock generated
View File

@ -2212,6 +2212,7 @@ checksum = "b4a52cacd869b804660986b10aa2076c3a4b6da644c7198f9fd0b613f4a7b249"
dependencies = [
"memchr",
"minidom",
"serde",
"stringprep",
]

View File

@ -31,7 +31,7 @@ dioxus-hot-reload = { version="0.4.3", features=["file_watcher"] }
futures-util = "0.3.29"
xmpp = { git = "https://gitlab.com/werner.kroneman/xmpp-rs.git", rev="ecd0be4aad985e9812626d6c4499c2586c158aba"}
keyring = "2.1.0"
jid = "0.10.0"
jid = { version = "0.10.0", features = ["serde"] }
confy = "0.5.1"
serde = "1.0"
serde_derive = "1.0"

View File

@ -16,15 +16,17 @@
use crate::widgets::login_screen::LoginAttempt;
use dioxus::prelude::*;
use jid::BareJid;
use keyring::Entry;
use log::error;
use serde_derive::{Deserialize, Serialize};
use crate::passwords::Password;
/// The configuration struct containing all the configuration options.
#[derive(Serialize, Deserialize, Default)]
#[derive(Default, Debug, Serialize, Deserialize)]
pub struct Configuration {
pub stored_username: String,
pub stored_default_nick: String,
pub stored_username: Option<BareJid>,
pub stored_default_nick: Option<String>,
}
/// Cache the login attempt into the config and keyring.
@ -36,30 +38,25 @@ pub struct Configuration {
/// # Arguments
/// * `attempt` - The login attempt to store.
/// * `config` - The current app configuration; UseState should prevent race conditions in writing.
pub fn store_login_details(attempt: LoginAttempt, config: &mut UseRef<Configuration>) {
// Extract the fields from the login attempt.
let LoginAttempt {
username,
default_nick,
password,
} = attempt;
pub fn store_login_details(jid: BareJid,
default_nick: String,
password: &Password,
c: &mut Configuration) {
// Open the config state and store the username and default nick.
config.with_mut(|c| {
// Open the keyring and store the password.
let entry = Entry::new("dergchat", &username).expect("Failed to create keyring entry.");
let entry = Entry::new("dergchat", &jid.to_string()).expect("Failed to create keyring entry.");
entry
.set_password(&password)
.set_password(&password.0)
.expect("Failed to set password in keyring.");
// Store the username and default nick in the config.
c.stored_username = username;
c.stored_default_nick = default_nick;
c.stored_username = Some(jid);
c.stored_default_nick = Some(default_nick);
if let Err(e) = confy::store("dergchat", None, c) {
error!("Failed to store the config file: {}", e);
}
});
}
/// Load the configuration from the config file.

View File

@ -20,6 +20,7 @@ mod configuration;
mod types;
mod widgets;
mod xmpp_interface;
mod passwords;
use dioxus::prelude::*;
use dioxus_desktop::Config;

40
src/passwords.rs Normal file
View File

@ -0,0 +1,40 @@
// 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/>.
use std::fmt::Debug;
use jid::BareJid;
use keyring::Entry;
pub struct Password(pub String);
impl Debug for Password {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Password")
.field("password", &"********")
.finish()
}
}
/// Retrieve the password for the given username from the keyring, if it exists.
pub fn try_retrieve_password_from_keyring(username: &BareJid) -> Option<Password> {
Entry::new("dergchat", &username.to_string()).expect("Failed to create keyring entry.").get_password().ok().map(|p| Password(p))
}
/// Store the password for the given username in the keyring.
pub fn store_keyring_password(username: &BareJid, password: &Password) {
let entry = Entry::new("dergchat", &username.to_string()).expect("Failed to create keyring entry.");
entry.set_password(&*password.0).expect("Failed to set password in keyring.");
}

View File

@ -16,6 +16,7 @@
use jid::BareJid;
use std::fmt::Debug;
use crate::passwords::Password;
#[derive(Debug, Clone, PartialEq)]
pub struct Room {
@ -28,18 +29,9 @@ pub struct Message {
pub body: String,
}
#[derive(Debug)]
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()
}
pub password: Password,
}

View File

@ -22,6 +22,7 @@ use crate::widgets::room_view::RoomView;
use crate::widgets::sidebar::SideBar;
use crate::xmpp_interface::NetworkCommand;
use dioxus::core::{Element, Scope};
use crate::passwords::Password;
use dioxus::prelude::*;
use futures_util::StreamExt;
@ -29,21 +30,42 @@ use jid::BareJid;
use log::{error, info};
use std::collections::HashMap;
use crate::configuration::load_config;
use crate::configuration::{Configuration, load_config};
use crate::xmpp_interface;
use keyring::Entry;
use std::str::FromStr;
use std::string::String;
use crate::passwords::try_retrieve_password_from_keyring;
use crate::configuration::store_login_details;
pub mod login_screen;
mod no_room_open;
pub mod no_room_open;
pub mod room_join_widget;
pub mod room_list;
pub mod room_view;
pub mod send_message;
pub mod sidebar;
fn try_retrieve_credentials(config: &Configuration) -> Option<LoginCredentials> {
if let (Some(user), Some(nick)) = (&config.stored_username, &config.stored_default_nick) {
if let Some(password) = try_retrieve_password_from_keyring(&user) {
Some(LoginCredentials {
username: user.clone(),
default_nick: nick.clone(),
password,
})
} else {
info!("No stored password found; will not try to log in.");
None
}
} else {
info!("No stored username or default nick found; will not try to log in.");
None
}
}
pub fn App(cx: Scope) -> Element {
let messages = use_ref(cx, || HashMap::new());
let current_room = use_state(cx, || None::<BareJid>);
let connection_status = use_state(cx, || LoginStatus::LoggedOut);
@ -54,41 +76,19 @@ pub fn App(cx: Scope) -> Element {
let config = use_ref(cx, load_config);
{
let config = config.to_owned();
let _connection_status = connection_status.to_owned();
let coroutine = coroutine.to_owned();
use_on_create(cx, move || {
use_on_create(cx, || {
let config = config.to_owned();
let _connection_status = connection_status.to_owned();
let coroutine = coroutine.to_owned();
async move {
let entry = Entry::new("dergchat", &config.read().stored_username)
.expect("Failed to create keyring entry.");
let _password = match entry.get_password() {
Ok(_x) => {
info!("Password retrieved from keyring; will try to log in.");
let (username, default_nick) = config
.with(|c| (c.stored_username.clone(), c.stored_default_nick.clone()));
// If we have a username, nickname and password, immediately try to log in.
if username.len() > 0 && default_nick.len() > 0 {
let credentials = LoginCredentials {
username: BareJid::from_str(&username).expect("Invalid JID"),
default_nick: default_nick,
password: entry
.get_password()
.expect("Failed to get password from keyring"),
};
coroutine.send(NetworkCommand::TryLogin { credentials });
}
}
Err(e) => println!("Failed to get password from keyring: {}", e),
};
if let Some(credentials) = try_retrieve_credentials(&*config.read()) {
info!("Will try to log in as {} with default nick {}", credentials.username, credentials.default_nick);
coroutine.send(NetworkCommand::TryLogin { credentials });
} else {
info!("No stored credentials found; will not try to automatically log in.");
}
}
});
}
render! {
@ -97,33 +97,36 @@ pub fn App(cx: Scope) -> Element {
rsx! {
LoginScreen {
cached_username: config.read().stored_username.clone(),
cached_nick: config.read().stored_default_nick.clone(),
cached_username: config.read().stored_username.clone().map(|x| x.to_string()).unwrap_or("".to_string()),
cached_nick: config.read().stored_default_nick.clone().unwrap_or("".to_string()),
login_state: connection_status.current().as_ref().clone(),
on_login_attempt: move |x: LoginAttempt| {
{
let x = x.clone();
// Store the JID in the PKV.
config.with_mut(move |c| {
c.stored_username = x.username.clone();
c.stored_default_nick = x.default_nick.clone();
let LoginAttempt { username, default_nick, password } = x;
// Store the password in the keyring.
let entry = Entry::new("dergchat", &x.username).expect("Failed to create keyring entry.");
entry.set_password(&x.password).expect("Failed to set password in keyring.");
// First, validate the username as a jid:
let jid = match BareJid::from_str(&username) {
Ok(x) => x,
Err(e) => {
error!("Invalid JID: {}", e);
return;
}
};
if let Err(e) = confy::store("dergchat", None, &*config.read()) {
error!("Failed to store JID in config file: {}", e);
}
});
}
// Wrap the password in a Password struct.
let password = Password(password);
// Store the login details in the config and keyring for auto-login next time.
config.with_mut(|c| {
store_login_details(jid.clone(), default_nick.clone(), &password, c);
});
// Finally, send the login command to the xmpp interface.
coroutine.send(NetworkCommand::TryLogin {
credentials: LoginCredentials {
username: BareJid::from_str(&x.username).expect("Invalid JID"),
default_nick: x.default_nick,
password: x.password,
username: jid,
default_nick,
password,
},
});
},

View File

@ -203,7 +203,7 @@ pub async fn run_xmpp_toplevel(
connection_status.set(LoginStatus::LoggingIn);
let mut agent = ClientBuilder::new(credentials.username, &*credentials.password)
let mut agent = ClientBuilder::new(credentials.username, &credentials.password.0)
.set_client(ClientType::Pc, "dergchat")
.build();