diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index 50df8d9bd..e16661859 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -2165,7 +2165,7 @@ impl MutinyWallet { async fn sync_nostr_profile(&self) -> Result<(), MutinyError> { let npub = self.nostr.get_npub().await; if let Some(metadata) = self.nostr.primal_client.get_user_profile(npub).await? { - self.storage.set_nostr_profile(metadata)?; + self.storage.set_nostr_profile(&metadata)?; } Ok(()) diff --git a/mutiny-core/src/nostr/mod.rs b/mutiny-core/src/nostr/mod.rs index 6a4d7e73c..4b308cb50 100644 --- a/mutiny-core/src/nostr/mod.rs +++ b/mutiny-core/src/nostr/mod.rs @@ -27,10 +27,11 @@ use nostr::nips::nip47::*; use nostr::{ nips::nip04::{decrypt, encrypt}, Alphabet, Event, EventBuilder, EventId, Filter, JsonUtil, Keys, Kind, Metadata, SecretKey, - SingleLetterTag, Tag, TagKind, Timestamp, + SingleLetterTag, Tag, TagKind, Timestamp, UncheckedUrl, }; use nostr_sdk::{Client, NostrSigner, RelayPoolNotification}; use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; use std::collections::{HashMap, HashSet}; use std::sync::{atomic::Ordering, Arc, RwLock}; use std::time::Duration; @@ -394,7 +395,7 @@ impl NostrManager { }; let with_name = if let Some(name) = name { - current.name(name) + current.name(name).display_name(name) } else { current }; @@ -420,11 +421,98 @@ impl NostrManager { let event_id = self.client.set_metadata(&with_nip05).await?; log_info!(self.logger, "New kind 0: {event_id}"); - self.storage.set_nostr_profile(with_nip05.clone())?; + self.storage.set_nostr_profile(&with_nip05)?; Ok(with_nip05) } + /// This should only be called when the user is setting up a new profile + /// never for an existing profile + pub async fn setup_new_profile( + &self, + name: Option, + img_url: Option, + lnurl: Option, + nip05: Option, + ) -> Result { + // pull latest profile from primal + let npub = self.get_npub().await; + match self.primal_client.get_user_profile(npub).await { + Ok(Some(_)) => { + log_error!( + self.logger, + "User already has a profile, can't setup new profile" + ); + return Err(MutinyError::InvalidArgumentsError); + } + Ok(None) => (), + Err(e) => { + // if we can't get the profile from primal, fall back to local + // otherwise we can't create profile if the primal server is down + log_error!( + self.logger, + "Failed to get user profile from primal, falling back to local: {e}" + ); + if self.storage.get_nostr_profile()?.is_some() { + return Err(MutinyError::InvalidArgumentsError); + } + } + }; + + // create follow/relays list + { + // make sure we follow ourselves as well, makes other nostr feeds better + let tags = [Tag::public_key(npub)]; + // set relay list in the content properly + let content: HashMap = RELAYS + .iter() + .map(|relay| { + let value = json!({"read":true,"write":true}); + (relay.to_string(), value) + }) + .collect(); + let builder = EventBuilder::new(Kind::ContactList, json!(content).to_string(), tags); + self.client.send_event_builder(builder).await?; + } + + // create real relay list + { + let builder = EventBuilder::relay_list( + RELAYS + .iter() + .map(|x| (UncheckedUrl::from(x.to_string()), None)), + ); + self.client.send_event_builder(builder).await?; + } + + // create metadata + let metadata = { + let name = name.unwrap_or_else(|| "Anon".to_string()); + let mut m = Metadata::default().name(name.clone()).display_name(name); + + if let Some(img_url) = img_url { + m = m.picture(img_url) + }; + if let Some(lnurl) = lnurl { + if let Some(ln_addr) = lnurl.lightning_address() { + m = m.lud16(ln_addr.to_string()) + } else { + m = m.lud06(lnurl.to_string()) + } + }; + if let Some(nip05) = nip05 { + m = m.nip05(nip05) + }; + m + }; + + let event_id = self.client.set_metadata(&metadata).await?; + log_info!(self.logger, "New kind 0: {event_id}"); + self.storage.set_nostr_profile(&metadata)?; + + Ok(metadata) + } + /// Sets the user's nostr profile metadata as deleted pub async fn delete_profile(&self) -> Result { let metadata = Metadata::default() @@ -435,7 +523,7 @@ impl NostrManager { let event_id = self.client.set_metadata(&metadata).await?; log_info!(self.logger, "New kind 0: {event_id}"); - self.storage.set_nostr_profile(metadata.clone())?; + self.storage.set_nostr_profile(&metadata)?; Ok(metadata) } @@ -481,7 +569,22 @@ impl NostrManager { tags.push(Tag::public_key(npub)); EventBuilder::new(Kind::ContactList, content, tags) } - None => EventBuilder::new(Kind::ContactList, "", [Tag::public_key(npub)]), + None => { + // make sure we follow ourselves as well, makes other nostr feeds better + let tags = [ + Tag::public_key(npub), + Tag::public_key(self.get_npub().await), + ]; + // set relay list in the content properly + let content: HashMap = RELAYS + .iter() + .map(|relay| { + let value = json!({"read":true,"write":true}); + (relay.to_string(), value) + }) + .collect(); + EventBuilder::new(Kind::ContactList, json!(content).to_string(), tags) + } }; let event = self diff --git a/mutiny-core/src/storage.rs b/mutiny-core/src/storage.rs index 80531ba2c..68f4d17dd 100644 --- a/mutiny-core/src/storage.rs +++ b/mutiny-core/src/storage.rs @@ -593,7 +593,7 @@ pub trait MutinyStorage: Clone + Sized + Send + Sync + 'static { self.get_data(NOSTR_PROFILE_METADATA) } - fn set_nostr_profile(&self, metadata: Metadata) -> Result<(), MutinyError> { + fn set_nostr_profile(&self, metadata: &Metadata) -> Result<(), MutinyError> { self.set_data(NOSTR_PROFILE_METADATA.to_string(), metadata, None) } diff --git a/mutiny-wasm/src/lib.rs b/mutiny-wasm/src/lib.rs index bcd46658f..e56762dd5 100644 --- a/mutiny-wasm/src/lib.rs +++ b/mutiny-wasm/src/lib.rs @@ -1873,6 +1873,37 @@ impl MutinyWallet { Ok(JsValue::from_serde(&profile)?) } + /// This should only be called when the user is setting up a new profile + /// never for an existing profile + pub async fn setup_new_profile( + &self, + name: Option, + img_url: Option, + lnurl: Option, + nip05: Option, + ) -> Result { + let img_url = img_url + .map(|i| nostr::Url::from_str(&i)) + .transpose() + .map_err(|_| MutinyJsError::InvalidArgumentsError)?; + + let lnurl = lnurl + .map(|l| { + LightningAddress::from_str(&l) + .map(|a| a.lnurl()) + .or(LnUrl::from_str(&l)) + }) + .transpose() + .map_err(|_| MutinyJsError::InvalidArgumentsError)?; + + let profile = self + .inner + .nostr + .setup_new_profile(name, img_url, lnurl, nip05) + .await?; + Ok(JsValue::from_serde(&profile)?) + } + /// Sets the user's nostr profile data #[wasm_bindgen] pub async fn edit_nostr_profile(