notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit baaa7cc05d97dc0b5e18a79c2e2c2aba2a89c940
parent 64904da5e8666d91976a1770f2d222330afca916
Author: kernelkind <kernelkind@gmail.com>
Date:   Sat, 27 Apr 2024 12:12:38 -0400

move login logic from promise to async fns

Signed-off-by: kernelkind <kernelkind@gmail.com>
Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Msrc/key_parsing.rs | 139++++++++++++++++++++++++++++++++++---------------------------------------------
Msrc/login_manager.rs | 71++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
2 files changed, 109 insertions(+), 101 deletions(-)

diff --git a/src/key_parsing.rs b/src/key_parsing.rs @@ -2,9 +2,9 @@ use std::collections::HashMap; use std::str::FromStr; use crate::Error; -use ehttp::{Request, Response}; use nostr_sdk::{prelude::Keys, PublicKey, SecretKey}; use poll_promise::Promise; +use reqwest::{Request, Response}; use serde::{Deserialize, Serialize}; #[derive(Debug, PartialEq)] @@ -30,9 +30,13 @@ pub struct Nip05Result { pub relays: Option<HashMap<String, Vec<String>>>, } -fn parse_nip05_response(response: Response) -> Result<Nip05Result, Error> { - serde_json::from_slice::<Nip05Result>(&response.bytes) - .map_err(|e| Error::Generic(e.to_string())) +async fn parse_nip05_response(response: Response) -> Result<Nip05Result, Error> { + match response.bytes().await { + Ok(bytes) => { + serde_json::from_slice::<Nip05Result>(&bytes).map_err(|e| Error::Generic(e.to_string())) + } + Err(e) => Err(Error::Generic(e.to_string())), + } } fn get_pubkey_from_result(result: Nip05Result, user: String) -> Result<PublicKey, Error> { @@ -44,70 +48,56 @@ fn get_pubkey_from_result(result: Nip05Result, user: String) -> Result<PublicKey } } -fn get_nip05_pubkey(id: &str) -> Promise<Result<PublicKey, Error>> { - let (sender, promise) = Promise::new(); +async fn get_nip05_pubkey(id: &str) -> Result<PublicKey, Error> { let mut parts = id.split('@'); let user = match parts.next() { Some(user) => user, None => { - sender.send(Err(Error::Generic( + return Err(Error::Generic( "Address does not contain username.".to_string(), - ))); - return promise; + )); } }; let host = match parts.next() { Some(host) => host, None => { - sender.send(Err(Error::Generic( + return Err(Error::Generic( "Nip05 address does not contain host.".to_string(), - ))); - return promise; + )); } }; if parts.next().is_some() { - sender.send(Err(Error::Generic( + return Err(Error::Generic( "Nip05 address contains extraneous parts.".to_string(), - ))); - return promise; + )); } let url = format!("https://{host}/.well-known/nostr.json?name={user}"); - let request = Request::get(url); - + let request = Request::new(reqwest::Method::GET, url.parse().unwrap()); let cloned_user = user.to_string(); - ehttp::fetch(request, move |response: Result<Response, String>| { - let result = match response { - Ok(resp) => parse_nip05_response(resp) - .and_then(move |result| get_pubkey_from_result(result, cloned_user)), - Err(e) => Err(Error::Generic(e.to_string())), - }; - sender.send(result); - }); - promise + let client = reqwest::Client::new(); + match client.execute(request).await { + Ok(resp) => match parse_nip05_response(resp).await { + Ok(result) => match get_pubkey_from_result(result, cloned_user) { + Ok(pubkey) => Ok(pubkey), + Err(e) => Err(Error::Generic(e.to_string())), + }, + Err(e) => Err(Error::Generic(e.to_string())), + }, + Err(e) => Err(Error::Generic(e.to_string())), + } } fn retrieving_nip05_pubkey(key: &str) -> bool { key.contains('@') } -fn nip05_promise_wrapper(id: &str) -> Promise<Result<Keys, LoginError>> { - let (sender, promise) = Promise::new(); - let original_promise = get_nip05_pubkey(id); - - std::thread::spawn(move || { - let result = original_promise.block_and_take(); - let transformed_result = match result { - Ok(public_key) => Ok(Keys::from_public_key(public_key)), - Err(e) => Err(LoginError::Nip05Failed(e.to_string())), - }; - sender.send(transformed_result); - }); - - promise +pub fn perform_key_retrieval(key: &str) -> Promise<Result<Keys, LoginError>> { + let key_string = String::from(key); + Promise::spawn_async(async move { get_login_key(&key_string).await }) } /// Attempts to turn a string slice key from the user into a Nostr-Sdk Keys object. @@ -118,15 +108,7 @@ fn nip05_promise_wrapper(id: &str) -> Promise<Result<Keys, LoginError>> { /// - Private hex key: "5dab..." /// - NIP-05 address: "example@nostr.com" /// -/// For NIP-05 addresses, retrieval of the public key is an asynchronous operation that returns a `Promise`, so it -/// will not be immediately ready. -/// All other key formats are processed synchronously even though they are still behind a Promise, they will be -/// available immediately. -/// -/// Returns a `Promise` that resolves to `Result<Keys, LoginError>`. `LoginError` is returned in case of invalid format, -/// unsupported key types, or network errors during NIP-05 address resolution. -/// -pub fn perform_key_retrieval(key: &str) -> Promise<Result<Keys, LoginError>> { +pub async fn get_login_key(key: &str) -> Result<Keys, LoginError> { let tmp_key: &str = if let Some(stripped) = key.strip_prefix('@') { stripped } else { @@ -134,16 +116,16 @@ pub fn perform_key_retrieval(key: &str) -> Promise<Result<Keys, LoginError>> { }; if retrieving_nip05_pubkey(tmp_key) { - nip05_promise_wrapper(tmp_key) + match get_nip05_pubkey(tmp_key).await { + Ok(pubkey) => Ok(Keys::from_public_key(pubkey)), + Err(e) => Err(LoginError::Nip05Failed(e.to_string())), + } + } else if let Ok(pubkey) = PublicKey::from_str(tmp_key) { + Ok(Keys::from_public_key(pubkey)) + } else if let Ok(secret_key) = SecretKey::from_str(tmp_key) { + Ok(Keys::new(secret_key)) } else { - let result: Result<Keys, LoginError> = if let Ok(pubkey) = PublicKey::from_str(tmp_key) { - Ok(Keys::from_public_key(pubkey)) - } else if let Ok(secret_key) = SecretKey::from_str(tmp_key) { - Ok(Keys::new(secret_key)) - } else { - Err(LoginError::InvalidKey) - }; - Promise::from_ready(result) + Err(LoginError::InvalidKey) } } @@ -152,8 +134,17 @@ mod tests { use super::*; use crate::promise_assert; - #[test] - fn test_pubkey() { + #[tokio::test] + async fn test_pubkey_async() { + let pubkey_str = "npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s"; + let expected_pubkey = PublicKey::from_str(pubkey_str).expect("Should not have errored."); + let login_key_result = get_login_key(pubkey_str).await; + + assert_eq!(Ok(Keys::from_public_key(expected_pubkey)), login_key_result); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_pubkey() { let pubkey_str = "npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s"; let expected_pubkey = PublicKey::from_str(pubkey_str).expect("Should not have errored."); let login_key_result = perform_key_retrieval(pubkey_str); @@ -165,8 +156,8 @@ mod tests { ); } - #[test] - fn test_hex_pubkey() { + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_hex_pubkey() { let pubkey_str = "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"; let expected_pubkey = PublicKey::from_str(pubkey_str).expect("Should not have errored."); let login_key_result = perform_key_retrieval(pubkey_str); @@ -178,8 +169,8 @@ mod tests { ); } - #[test] - fn test_privkey() { + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_privkey() { let privkey_str = "nsec1g8wt3hlwjpa4827xylr3r0lccufxltyekhraexes8lqmpp2hensq5aujhs"; let expected_privkey = SecretKey::from_str(privkey_str).expect("Should not have errored."); let login_key_result = perform_key_retrieval(privkey_str); @@ -191,8 +182,8 @@ mod tests { ); } - #[test] - fn test_hex_privkey() { + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_hex_privkey() { let privkey_str = "41dcb8dfee907b53abc627c711bff8c7126fac99b5c7dc9b303fc1b08557cce0"; let expected_privkey = SecretKey::from_str(privkey_str).expect("Should not have errored."); let login_key_result = perform_key_retrieval(privkey_str); @@ -204,8 +195,8 @@ mod tests { ); } - #[test] - fn test_nip05() { + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_nip05() { let nip05_str = "damus@damus.io"; let expected_pubkey = PublicKey::from_str("npub18m76awca3y37hkvuneavuw6pjj4525fw90necxmadrvjg0sdy6qsngq955") @@ -218,16 +209,4 @@ mod tests { &login_key_result ); } - - #[test] - fn test_nip05_pubkey() { - let nip05_str = "damus@damus.io"; - let expected_pubkey = - PublicKey::from_str("npub18m76awca3y37hkvuneavuw6pjj4525fw90necxmadrvjg0sdy6qsngq955") - .expect("Should not have errored."); - let login_key_result = get_nip05_pubkey(nip05_str); - - let res = login_key_result.block_and_take().expect("Should not error"); - assert_eq!(expected_pubkey, res); - } } diff --git a/src/login_manager.rs b/src/login_manager.rs @@ -4,12 +4,11 @@ use egui::{TextBuffer, TextEdit}; use nostr_sdk::Keys; use poll_promise::Promise; -/// Helper storage object for retrieving the plaintext key from the user and converting it into a -/// nostr-sdk Keys object if possible. +/// The UI view interface to log in to a nostr account. #[derive(Default)] pub struct LoginManager { login_key: String, - promise: Option<Promise<Result<Keys, LoginError>>>, + promise_query: Option<(String, Promise<Result<Keys, LoginError>>)>, error: Option<LoginError>, key_on_error: Option<String>, } @@ -18,12 +17,13 @@ impl<'a> LoginManager { pub fn new() -> Self { LoginManager { login_key: String::new(), - promise: None, + promise_query: None, error: None, key_on_error: None, } } + /// Get the textedit for the login UI without exposing the key variable pub fn get_login_textedit( &'a mut self, textedit_closure: fn(&'a mut dyn TextBuffer) -> TextEdit<'a>, @@ -31,14 +31,30 @@ impl<'a> LoginManager { textedit_closure(&mut self.login_key) } + /// User pressed the 'login' button pub fn apply_login(&'a mut self) { - self.promise = Some(perform_key_retrieval(&self.login_key)); + let new_promise = match &self.promise_query { + Some((query, _)) => { + if query != &self.login_key { + Some(perform_key_retrieval(&self.login_key)) + } else { + None + } + } + None => Some(perform_key_retrieval(&self.login_key)), + }; + + if let Some(new_promise) = new_promise { + self.promise_query = Some((self.login_key.clone(), new_promise)); + } } + /// Whether to indicate to the user that there is a network operation occuring pub fn is_awaiting_network(&self) -> bool { - self.promise.is_some() + self.promise_query.is_some() } + /// Whether to indicate to the user that a login error occured pub fn check_for_error(&'a mut self) -> Option<&'a LoginError> { if let Some(error_key) = &self.key_on_error { if self.login_key != *error_key { @@ -50,10 +66,11 @@ impl<'a> LoginManager { self.error.as_ref() } + /// Whether to indicate to the user that a successful login occured pub fn check_for_successful_login(&mut self) -> Option<Keys> { - if let Some(promise) = &mut self.promise { + if let Some((_, promise)) = &mut self.promise_query { if promise.ready().is_some() { - if let Some(promise) = self.promise.take() { + if let Some((_, promise)) = self.promise_query.take() { match promise.block_and_take() { Ok(key) => { return Some(key); @@ -76,10 +93,9 @@ mod tests { use nostr_sdk::PublicKey; use std::time::{Duration, Instant}; - #[test] - fn test_retrieve_key() { + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_retrieve_key() { let mut manager = LoginManager::new(); - let manager_ref = &mut manager; let expected_str = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"; let expected_key = Keys::from_public_key(PublicKey::from_hex(expected_str).unwrap()); @@ -89,24 +105,37 @@ mod tests { let cur_time = start_time.elapsed(); if cur_time < Duration::from_millis(10u64) { - let key = "test"; - manager_ref.login_key = String::from(key); - manager_ref.promise = Some(perform_key_retrieval(key)); + let _ = manager.get_login_textedit(|text| { + text.clear(); + text.insert_text("test", 0); + egui::TextEdit::singleline(text) + }); + manager.apply_login(); } else if cur_time < Duration::from_millis(30u64) { - let key = "test2"; - manager_ref.login_key = String::from(key); - manager_ref.promise = Some(perform_key_retrieval(key)); + let _ = manager.get_login_textedit(|text| { + text.clear(); + text.insert_text("test2", 0); + egui::TextEdit::singleline(text) + }); + manager.apply_login(); } else { - manager_ref.login_key = String::from(expected_str); - manager_ref.promise = Some(perform_key_retrieval(expected_str)); + let _ = manager.get_login_textedit(|text| { + text.clear(); + text.insert_text( + "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681", + 0, + ); + egui::TextEdit::singleline(text) + }); + manager.apply_login(); } - if let Some(key) = manager_ref.check_for_successful_login() { + if let Some(key) = manager.check_for_successful_login() { assert_eq!(expected_key, key); return; } } - panic!(); + panic!("Test failed to get expected key."); } }