use std::{collections::HashSet, str::FromStr, sync::Arc, time::Duration}; use anyhow::{anyhow, Error}; use common::profile::NostrProfile; use global::{ constants::{ ALL_MESSAGES_SUB_ID, CLIENT_KEYRING, DEVICE_ANNOUNCEMENT_KIND, DEVICE_REQUEST_KIND, DEVICE_RESPONSE_KIND, DEVICE_SUB_ID, MASTER_KEYRING, NEW_MESSAGE_SUB_ID, }, get_app_name, get_client, get_device_keys, get_device_name, set_device_keys, }; use gpui::{ div, px, relative, App, AppContext, Context, Entity, Global, ParentElement, Styled, Task, Window, }; use nostr_sdk::prelude::*; use smallvec::SmallVec; use ui::{ button::{Button, ButtonRounded, ButtonVariants}, indicator::Indicator, notification::Notification, theme::{scale::ColorScaleStep, ActiveTheme}, ContextModal, Root, Sizable, StyledExt, }; use crate::views::{app, onboarding, relays}; struct GlobalDevice(Entity); impl Global for GlobalDevice {} #[derive(Debug, Default)] pub enum DeviceState { Master, Minion, #[default] None, } impl DeviceState { pub fn subscribe(&self, window: &mut Window, cx: &mut Context) { match self { Self::Master => { let client = get_client(); let task: Task> = cx.background_spawn(async move { let signer = client.signer().await?; let public_key = signer.get_public_key().await?; let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); let filter = Filter::new() .kind(Kind::Custom(DEVICE_REQUEST_KIND)) .author(public_key) .limit(1); // Subscribe for the latest request client.subscribe(filter, Some(opts)).await?; let filter = Filter::new() .kind(Kind::Custom(DEVICE_REQUEST_KIND)) .author(public_key) .since(Timestamp::now()); // Subscribe for new device requests client.subscribe(filter, None).await?; Ok(()) }); cx.spawn_in(window, |_, _cx| async move { if let Err(err) = task.await { log::error!("Failed to subscribe for device requests: {}", err); } }) .detach(); } Self::Minion => { let client = get_client(); let task: Task> = cx.background_spawn(async move { let signer = client.signer().await?; let public_key = signer.get_public_key().await?; let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); let filter = Filter::new() .kind(Kind::Custom(DEVICE_RESPONSE_KIND)) .author(public_key); // Getting all previous approvals client.subscribe(filter.clone(), Some(opts)).await?; // Continously receive the request approval client .subscribe(filter.since(Timestamp::now()), None) .await?; Ok(()) }); cx.spawn_in(window, |_, _cx| async move { if let Err(err) = task.await { log::error!("Failed to subscribe for device approval: {}", err); } }) .detach(); } _ => {} } } } /// Current Device (Client) /// /// NIP-4e: #[derive(Debug)] pub struct Device { /// Profile (Metadata) of current user profile: Option, /// Client Keys client_keys: Arc, /// Device State state: Entity, requesters: Entity>, is_processing: bool, } pub fn init(window: &mut Window, cx: &App) { // Initialize client keys let read_keys = cx.read_credentials(CLIENT_KEYRING); let window_handle = window.window_handle(); cx.spawn(|cx| async move { let client_keys = if let Ok(Some((_, secret))) = read_keys.await { let secret_key = SecretKey::from_slice(&secret).unwrap(); Arc::new(Keys::new(secret_key)) } else { // Generate new keys and save them to keyring let keys = Keys::generate(); if let Ok(write_keys) = cx.update(|cx| { cx.write_credentials( CLIENT_KEYRING, keys.public_key.to_hex().as_str(), keys.secret_key().as_secret_bytes(), ) }) { _ = write_keys.await; }; Arc::new(keys) }; cx.update(|cx| { let state = cx.new(|_| DeviceState::None); let weak_state = state.downgrade(); let requesters = cx.new(|_| HashSet::new()); let entity = cx.new(|_| Device { profile: None, is_processing: false, state, client_keys, requesters, }); window_handle .update(cx, |_, window, cx| { // Open the onboarding view Root::update(window, cx, |this, window, cx| { this.replace_view(onboarding::init(window, cx).into()); cx.notify(); }); // Observe the DeviceState changes if let Some(state) = weak_state.upgrade() { window .observe(&state, cx, |this, window, cx| { this.update(cx, |this, cx| { this.subscribe(window, cx); }); }) .detach(); }; // Observe the Device changes window .observe(&entity, cx, |this, window, cx| { this.update(cx, |this, cx| { this.on_device_change(window, cx); }); }) .detach(); }) .ok(); Device::set_global(entity, cx) }) .ok(); }) .detach(); } impl Device { pub fn global(cx: &App) -> Option> { cx.try_global::().map(|model| model.0.clone()) } pub fn set_global(device: Entity, cx: &mut App) { cx.set_global(GlobalDevice(device)); } pub fn client_keys(&self) -> Arc { self.client_keys.clone() } pub fn profile(&self) -> Option<&NostrProfile> { self.profile.as_ref() } pub fn set_profile(&mut self, profile: NostrProfile, cx: &mut Context) { self.profile = Some(profile); cx.notify(); } pub fn set_state(&mut self, state: DeviceState, cx: &mut Context) { self.state.update(cx, |this, cx| { *this = state; cx.notify(); }); } pub fn set_processing(&mut self, is_processing: bool, cx: &mut Context) { self.is_processing = is_processing; cx.notify(); } pub fn add_requester(&mut self, public_key: PublicKey, cx: &mut Context) { self.requesters.update(cx, |this, cx| { this.insert(public_key); cx.notify(); }); } /// Login and set user signer pub fn login(&self, signer: T, cx: &mut Context) -> Task> where T: NostrSigner + 'static, { let client = get_client(); // Set the user's signer as the main signer let login: Task> = cx.background_spawn(async move { // Use user's signer for main signer _ = client.set_signer(signer).await; // Verify nostr signer and get public key let signer = client.signer().await?; let public_key = signer.get_public_key().await?; // Fetch user's metadata let metadata = client .fetch_metadata(public_key, Duration::from_secs(2)) .await? .unwrap_or_default(); // Get user's inbox relays let filter = Filter::new() .kind(Kind::InboxRelays) .author(public_key) .limit(1); let relays = if let Some(event) = client .fetch_events(filter, Duration::from_secs(2)) .await? .first_owned() { let relays = event .tags .filter_standardized(TagKind::Relay) .filter_map(|t| { if let TagStandard::Relay(url) = t { Some(url.to_owned()) } else { None } }) .collect::>(); Some(relays) } else { None }; let profile = NostrProfile::new(public_key, metadata).relays(relays); Ok(profile) }); cx.spawn(|this, cx| async move { match login.await { Ok(user) => { cx.update(|cx| { this.update(cx, |this, cx| { this.profile = Some(user); cx.notify(); }) .ok(); }) .ok(); Ok(()) } Err(e) => Err(e), } }) } /// This function is called whenever the device is changed fn on_device_change(&mut self, window: &mut Window, cx: &mut Context) { let Some(profile) = self.profile.as_ref() else { // User not logged in, render the Onboarding View Root::update(window, cx, |this, window, cx| { this.replace_view(onboarding::init(window, cx).into()); cx.notify(); }); return; }; // Replace the Onboarding View with the Dock View Root::update(window, cx, |this, window, cx| { this.replace_view(app::init(window, cx).into()); cx.notify(); }); // Get the user's messaging relays // If it is empty, user must setup relays let ready = profile.messaging_relays.is_some(); cx.spawn_in(window, |this, mut cx| async move { cx.update(|window, cx| { if !ready { this.update(cx, |this, cx| { this.render_setup_relays(window, cx); }) .ok(); } else { this.update(cx, |this, cx| { this.start_subscription(cx); }) .ok(); } }) .ok(); }) .detach(); } /// Initialize subscription for current user pub fn start_subscription(&self, cx: &Context) { if self.is_processing { return; } let Some(profile) = self.profile() else { return; }; let user = profile.public_key; let client = get_client(); let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); let device_kind = Kind::Custom(DEVICE_ANNOUNCEMENT_KIND); // Create a device announcement filter let device = Filter::new().kind(device_kind).author(user).limit(1); // Create a contact list filter let contacts = Filter::new().kind(Kind::ContactList).author(user).limit(1); // Create a user's data filter let data = Filter::new() .author(user) .since(Timestamp::now()) .kinds(vec![ Kind::Metadata, Kind::InboxRelays, Kind::RelayList, device_kind, ]); // Create a filter for getting all gift wrapped events send to current user let msg = Filter::new().kind(Kind::GiftWrap).pubkey(user); // Create a filter to continuously receive new messages. let new_msg = Filter::new().kind(Kind::GiftWrap).pubkey(user).limit(0); let task: Task> = cx.background_spawn(async move { // Only subscribe to the latest device announcement let sub_id = SubscriptionId::new(DEVICE_SUB_ID); client.subscribe_with_id(sub_id, device, Some(opts)).await?; // Only subscribe to the latest contact list client.subscribe(contacts, Some(opts)).await?; // Continuously receive new user's data since now client.subscribe(data, None).await?; let sub_id = SubscriptionId::new(ALL_MESSAGES_SUB_ID); client.subscribe_with_id(sub_id, msg, Some(opts)).await?; let sub_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID); client.subscribe_with_id(sub_id, new_msg, None).await?; Ok(()) }); cx.spawn(|_, _| async move { if let Err(e) = task.await { log::error!("Subscription error: {}", e); } }) .detach(); } /// Setup Device /// /// NIP-4e: pub fn setup_device(&mut self, window: &mut Window, cx: &mut Context) { let Some(profile) = self.profile().cloned() else { return; }; // If processing, return early if self.is_processing { return; } // Only process if device keys are not set self.set_processing(true, cx); let client = get_client(); let public_key = profile.public_key; let kind = Kind::Custom(DEVICE_ANNOUNCEMENT_KIND); let filter = Filter::new().kind(kind).author(public_key).limit(1); // Fetch device announcement events let fetch_announcement = cx.background_spawn(async move { if let Some(event) = client.database().query(filter).await?.first_owned() { Ok(event) } else { Err(anyhow!("Device Announcement not found.")) } }); cx.spawn_in(window, |this, mut cx| async move { // Device Keys has been set, no need to retrieve device announcement again if get_device_keys().await.is_some() { return; } match fetch_announcement.await { Ok(event) => { log::info!("Found a device announcement: {:?}", event); let n_tag = event .tags .find(TagKind::custom("n")) .and_then(|t| t.content()) .map(|hex| hex.to_owned()); let credentials_task = match cx.update(|_, cx| cx.read_credentials(MASTER_KEYRING)) { Ok(task) => task, Err(err) => { log::error!("Failed to read credentials: {:?}", err); log::info!("Trying to request keys from Master Device..."); cx.update(|window, cx| { this.update(cx, |this, cx| { this.request_master_keys(window, cx); }) }) .ok(); return; } }; match credentials_task.await { Ok(Some((pubkey, secret))) if n_tag.as_deref() == Some(&pubkey) => { cx.update(|window, cx| { this.update(cx, |this, cx| { this.set_master_keys(secret, window, cx); }) }) .ok(); } _ => { log::info!("This device is not the Master Device."); log::info!("Trying to request keys from Master Device..."); cx.update(|window, cx| { this.update(cx, |this, cx| { this.request_master_keys(window, cx); }) }) .ok(); } } } Err(_) => { log::info!("Device Announcement not found."); log::info!("Appoint this device as Master Device."); cx.update(|window, cx| { this.update(cx, |this, cx| { this.set_new_master_keys(window, cx); }) .ok(); }) .ok(); } } }) .detach(); } /// Create a new Master Keys, appointing this device as Master Device. /// /// NIP-4e: pub fn set_new_master_keys(&self, window: &mut Window, cx: &Context) { let client = get_client(); let app_name = get_app_name(); let task: Task, Error>> = cx.background_spawn(async move { let keys = Keys::generate(); let kind = Kind::Custom(DEVICE_ANNOUNCEMENT_KIND); let client_tag = Tag::client(app_name); let pubkey_tag = Tag::custom(TagKind::custom("n"), vec![keys.public_key().to_hex()]); let event = EventBuilder::new(kind, "").tags(vec![client_tag, pubkey_tag]); if let Err(e) = client.send_event_builder(event).await { log::error!("Failed to send Device Announcement: {}", e); } else { log::info!("Device Announcement has been sent"); } Ok(Arc::new(keys)) }); cx.spawn_in(window, |this, mut cx| async move { if get_device_keys().await.is_some() { return; } if let Ok(keys) = task.await { // Update global state set_device_keys(keys.clone()).await; // Save keys if let Ok(task) = cx.update(|_, cx| { cx.write_credentials( MASTER_KEYRING, keys.public_key().to_hex().as_str(), keys.secret_key().as_secret_bytes(), ) }) { if let Err(e) = task.await { log::error!("Failed to write device keys to keyring: {}", e); } }; cx.update(|_, cx| { this.update(cx, |this, cx| { this.set_state(DeviceState::Master, cx); }) .ok(); }) .ok(); } }) .detach(); } /// Device already has Master Keys, re-appointing this device as Master Device. /// /// NIP-4e: pub fn set_master_keys(&self, secret: Vec, window: &mut Window, cx: &Context) { let Ok(secret_key) = SecretKey::from_slice(&secret) else { log::error!("Failed to parse secret key"); return; }; let keys = Arc::new(Keys::new(secret_key)); cx.spawn_in(window, |this, mut cx| async move { log::info!("Re-appointing this device as Master Device."); set_device_keys(keys).await; cx.update(|_, cx| { this.update(cx, |this, cx| { this.set_state(DeviceState::Master, cx); }) .ok(); }) .ok(); }) .detach(); } /// Send a request to ask for device keys from the other Nostr client /// /// NIP-4e: pub fn request_master_keys(&self, window: &mut Window, cx: &Context) { let client = get_client(); let app_name = get_app_name(); let client_keys = self.client_keys.clone(); let kind = Kind::Custom(DEVICE_REQUEST_KIND); let client_tag = Tag::client(app_name); let pubkey_tag = Tag::custom( TagKind::custom("pubkey"), vec![client_keys.public_key().to_hex()], ); // Create a request event builder let builder = EventBuilder::new(kind, "").tags(vec![client_tag, pubkey_tag]); let task: Task> = cx.background_spawn(async move { log::info!("Sent a request to ask for device keys from the other Nostr client"); if let Err(e) = client.send_event_builder(builder).await { log::error!("Failed to send device keys request: {}", e); } else { log::info!("Waiting for response..."); } Ok(()) }); cx.spawn_in(window, |this, mut cx| async move { if task.await.is_ok() { cx.update(|window, cx| { this.update(cx, |this, cx| { this.set_state(DeviceState::Minion, cx); this.render_waiting_modal(window, cx); }) .ok(); }) .ok(); } }) .detach(); } /// Received Device Keys approval from Master Device, /// /// NIP-4e: pub fn recv_approval(&self, event: Event, window: &mut Window, cx: &Context) { let local_signer = self.client_keys.clone(); let task = cx.background_spawn(async move { if let Some(tag) = event .tags .find(TagKind::custom("P")) .and_then(|tag| tag.content()) { if let Ok(public_key) = PublicKey::from_str(tag) { let secret = local_signer .nip44_decrypt(&public_key, &event.content) .await?; let keys = Arc::new(Keys::parse(&secret)?); // Update global state with new device keys set_device_keys(keys).await; log::info!("Received master keys"); Ok(()) } else { Err(anyhow!("Public Key is invalid")) } } else { Err(anyhow!("Failed to decrypt the Master Keys")) } }); cx.spawn_in(window, |_, mut cx| async move { // No need to update if device keys are already available if get_device_keys().await.is_some() { return; } if let Err(e) = task.await { cx.update(|window, cx| { window.push_notification( Notification::error(format!("Failed to decrypt: {}", e)), cx, ); }) .ok(); } else { cx.update(|window, cx| { window.close_all_modals(cx); window.push_notification( Notification::success("Device Keys request has been approved"), cx, ); }) .ok(); } }) .detach(); } /// Received Master Keys request from other Nostr client /// /// NIP-4e: pub fn recv_request(&mut self, event: Event, window: &mut Window, cx: &mut Context) { let Some(target_pubkey) = event .tags .find(TagKind::custom("pubkey")) .and_then(|tag| tag.content()) .and_then(|content| PublicKey::parse(content).ok()) else { log::error!("Invalid public key."); return; }; // Prevent processing duplicate requests if self.requesters.read(cx).contains(&target_pubkey) { return; } self.add_requester(target_pubkey, cx); let client = get_client(); let read_keys = cx.read_credentials(MASTER_KEYRING); let local_signer = self.client_keys.clone(); let device_name = event .tags .find(TagKind::Client) .and_then(|tag| tag.content()) .unwrap_or("Other Device") .to_owned(); let response = window.prompt( gpui::PromptLevel::Info, "Requesting Device Keys", Some( format!( "{} is requesting shared device keys stored in this device", device_name ) .as_str(), ), &["Approve", "Deny"], cx, ); cx.spawn_in(window, |_, cx| async move { match response.await { Ok(0) => { if let Ok(Some((_, secret))) = read_keys.await { let local_pubkey = local_signer.get_public_key().await?; // Get device's secret key let device_secret = SecretKey::from_slice(&secret)?; let device_secret_hex = device_secret.to_secret_hex(); // Encrypt device's secret key by using NIP-44 let content = local_signer .nip44_encrypt(&target_pubkey, &device_secret_hex) .await?; // Create pubkey tag for other device (lowercase p) let other_tag = Tag::public_key(target_pubkey); // Create pubkey tag for this device (uppercase P) let local_tag = Tag::custom( TagKind::SingleLetter(SingleLetterTag::uppercase(Alphabet::P)), vec![local_pubkey.to_hex()], ); // Create event builder let kind = Kind::Custom(DEVICE_RESPONSE_KIND); let tags = vec![other_tag, local_tag]; let builder = EventBuilder::new(kind, content).tags(tags); cx.background_spawn(async move { if let Err(err) = client.send_event_builder(builder).await { log::error!("Failed to send device keys to other client: {}", err); } else { log::info!("Sent device keys to other client"); } }) .await; Ok(()) } else { Err(anyhow!("Device Keys not found")) } } _ => Ok(()), } }) .detach(); } /// Show setup relays modal /// /// NIP-17: pub fn render_setup_relays(&mut self, window: &mut Window, cx: &mut Context) { let relays = relays::init(window, cx); window.open_modal(cx, move |this, window, cx| { let is_loading = relays.read(cx).loading(); this.keyboard(false) .closable(false) .width(px(430.)) .title("Your Messaging Relays are not configured") .child(relays.clone()) .footer( div() .p_2() .border_t_1() .border_color(cx.theme().base.step(cx, ColorScaleStep::FIVE)) .child( Button::new("update_inbox_relays_btn") .label("Update") .primary() .bold() .rounded(ButtonRounded::Large) .w_full() .loading(is_loading) .on_click(window.listener_for(&relays, |this, _, window, cx| { this.update(window, cx); })), ), ) }); } /// Show waiting modal /// /// NIP-4e: pub fn render_waiting_modal(&mut self, window: &mut Window, cx: &mut Context) { window.open_modal(cx, move |this, _window, cx| { let msg = format!( "Please open {} and approve sharing device keys request.", get_device_name() ); this.keyboard(false) .closable(false) .width(px(430.)) .child( div() .flex() .items_center() .justify_center() .size_full() .p_4() .child( div() .flex() .flex_col() .items_center() .justify_center() .size_full() .child( div() .flex() .flex_col() .text_sm() .child( div() .font_semibold() .child("You're using a new device."), ) .child( div() .text_color( cx.theme() .base .step(cx, ColorScaleStep::ELEVEN), ) .line_height(relative(1.3)) .child(msg), ), ), ), ) .footer( div() .p_4() .border_t_1() .border_color(cx.theme().base.step(cx, ColorScaleStep::FIVE)) .w_full() .flex() .gap_2() .items_center() .justify_center() .text_xs() .text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN)) .child(Indicator::new().small()) .child("Waiting for approval ..."), ) }); } }