chore: refactor NIP-4E implementation
This commit is contained in:
@@ -1,17 +1,17 @@
|
|||||||
use std::time::Duration;
|
use std::{sync::Arc, time::Duration};
|
||||||
|
|
||||||
use anyhow::{anyhow, Context as AnyContext, Error};
|
use anyhow::{anyhow, Error};
|
||||||
use common::profile::NostrProfile;
|
use common::profile::NostrProfile;
|
||||||
use global::{
|
use global::{
|
||||||
constants::{
|
constants::{
|
||||||
ALL_MESSAGES_SUB_ID, CLIENT_KEYRING, DEVICE_ANNOUNCEMENT_KIND, DEVICE_REQUEST_KIND,
|
ALL_MESSAGES_SUB_ID, CLIENT_KEYRING, DEVICE_ANNOUNCEMENT_KIND, DEVICE_REQUEST_KIND,
|
||||||
DEVICE_RESPONSE_KIND, MASTER_KEYRING, NEW_MESSAGE_SUB_ID,
|
DEVICE_RESPONSE_KIND, DEVICE_SUB_ID, MASTER_KEYRING, NEW_MESSAGE_SUB_ID,
|
||||||
},
|
},
|
||||||
get_app_name, get_client, get_device_name, set_device_keys,
|
get_app_name, get_client, get_device_keys, get_device_name, set_device_keys,
|
||||||
};
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, relative, App, AppContext, AsyncApp, Context, Entity, Global, ParentElement, Styled,
|
div, px, relative, App, AppContext, Context, Entity, Global, ParentElement, Styled, Task,
|
||||||
Task, Window,
|
Window,
|
||||||
};
|
};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
@@ -81,11 +81,10 @@ pub fn init(window: &mut Window, cx: &App) {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Observe login behavior
|
|
||||||
window
|
window
|
||||||
.observe(&entity, cx, |this, window, cx| {
|
.observe(&entity, cx, |this, window, cx| {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.on_login(window, cx);
|
this.on_device_change(window, cx);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
@@ -137,6 +136,7 @@ impl Device {
|
|||||||
let metadata = client
|
let metadata = client
|
||||||
.fetch_metadata(public_key, Duration::from_secs(2))
|
.fetch_metadata(public_key, Duration::from_secs(2))
|
||||||
.await
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
// Get user's inbox relays
|
// Get user's inbox relays
|
||||||
@@ -191,7 +191,8 @@ impl Device {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_login(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
/// This function is called whenever the device is changed
|
||||||
|
fn on_device_change(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let Some(profile) = self.profile.as_ref() else {
|
let Some(profile) = self.profile.as_ref() else {
|
||||||
// User not logged in, render the Onboarding View
|
// User not logged in, render the Onboarding View
|
||||||
Root::update(window, cx, |this, window, cx| {
|
Root::update(window, cx, |this, window, cx| {
|
||||||
@@ -208,33 +209,205 @@ impl Device {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
|
|
||||||
let pubkey = profile.public_key;
|
// Get the user's messaging relays
|
||||||
let client_keys = self.client_keys.clone();
|
// If it is empty, user must setup relays
|
||||||
|
let ready = profile.messaging_relays.is_some();
|
||||||
|
|
||||||
// User's messaging relays not found
|
|
||||||
if profile.messaging_relays.is_none() {
|
|
||||||
cx.spawn_in(window, |this, mut cx| async move {
|
cx.spawn_in(window, |this, mut cx| async move {
|
||||||
cx.update(|window, cx| {
|
cx.update(|window, cx| {
|
||||||
|
if !ready {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.render_setup_relays(window, cx);
|
this.render_setup_relays(window, cx);
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
|
} else {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.start_subscription(cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize subscription for current user
|
||||||
|
pub fn start_subscription(&self, cx: &Context<Self>) {
|
||||||
|
let Some(profile) = self.profile() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
cx.spawn_in(window, |this, mut cx| async move {
|
let user = profile.public_key;
|
||||||
// Initialize subscription for current user
|
let client = get_client();
|
||||||
_ = Device::subscribe(pubkey, &cx).await;
|
|
||||||
|
|
||||||
// Initialize master keys for current user
|
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||||
if let Ok(Some(keys)) = Device::fetch_master_keys(pubkey, &cx).await {
|
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<Result<(), Error>> = 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: <https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md>
|
||||||
|
pub fn setup_device(&mut self, window: &mut Window, cx: &Context<Self>) {
|
||||||
|
let Some(profile) = self.profile() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 {
|
||||||
|
if get_device_keys().await.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(event) = fetch_announcement.await {
|
||||||
|
log::info!("Device Announcement: {:?}", event);
|
||||||
|
if let Ok(task) = cx.update(|_, cx| cx.read_credentials(MASTER_KEYRING)) {
|
||||||
|
if let Ok(Some((pubkey, secret))) = task.await {
|
||||||
|
if let Some(n) = event
|
||||||
|
.tags
|
||||||
|
.find(TagKind::custom("n"))
|
||||||
|
.and_then(|t| t.content())
|
||||||
|
.map(|hex| hex.to_owned())
|
||||||
|
{
|
||||||
|
if n == pubkey {
|
||||||
|
cx.update(|window, cx| {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.reinit_master_keys(secret, window, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
} else {
|
||||||
|
cx.update(|window, cx| {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.request_keys(window, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::error!("Failed to read credentials");
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("User cancelled keyring.")
|
||||||
|
} else {
|
||||||
|
cx.update(|window, cx| {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.set_master_keys(window, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new master keys
|
||||||
|
///
|
||||||
|
/// NIP-4e: <https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md>
|
||||||
|
pub fn set_master_keys(&self, window: &mut Window, cx: &Context<Self>) {
|
||||||
|
log::info!("Device Announcement isn't found.");
|
||||||
|
log::info!("Appoint this device as master");
|
||||||
|
|
||||||
|
let client = get_client();
|
||||||
|
let app_name = get_app_name();
|
||||||
|
|
||||||
|
let task: Task<Result<Arc<Keys>, 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, |_, 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;
|
set_device_keys(keys.clone()).await;
|
||||||
|
|
||||||
|
// Save keys
|
||||||
if let Ok(task) = cx.update(|_, cx| {
|
if let Ok(task) = cx.update(|_, cx| {
|
||||||
cx.write_credentials(
|
cx.write_credentials(
|
||||||
MASTER_KEYRING,
|
MASTER_KEYRING,
|
||||||
@@ -242,32 +415,119 @@ impl Device {
|
|||||||
keys.secret_key().as_secret_bytes(),
|
keys.secret_key().as_secret_bytes(),
|
||||||
)
|
)
|
||||||
}) {
|
}) {
|
||||||
_ = task.await;
|
if let Err(e) = task.await {
|
||||||
|
log::error!("Failed to write device keys to keyring: {}", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(event) = Device::fetch_request(pubkey, &cx).await {
|
/// Reinitialize master keys
|
||||||
cx.update(|window, cx| {
|
///
|
||||||
this.update(cx, |this, cx| {
|
/// NIP-4e: <https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md>
|
||||||
this.handle_request(event, window, cx);
|
pub fn reinit_master_keys(&self, secret: Vec<u8>, window: &mut Window, cx: &Context<Self>) {
|
||||||
})
|
let Some(profile) = self.profile() else {
|
||||||
.ok();
|
return;
|
||||||
})
|
};
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
|
||||||
let client = get_client();
|
let client = get_client();
|
||||||
|
let public_key = profile.public_key;
|
||||||
|
|
||||||
|
let task: Task<Result<Arc<Keys>, Error>> = cx.background_spawn(async move {
|
||||||
|
let secret_key = SecretKey::from_slice(&secret)?;
|
||||||
|
let keys = Arc::new(Keys::new(secret_key));
|
||||||
|
|
||||||
|
log::info!("Reappointing this device as master.");
|
||||||
|
|
||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
.kind(Kind::Custom(DEVICE_REQUEST_KIND))
|
.kind(Kind::Custom(DEVICE_REQUEST_KIND))
|
||||||
.author(pubkey)
|
.author(public_key)
|
||||||
.since(Timestamp::now());
|
.since(Timestamp::now());
|
||||||
|
|
||||||
|
// Subscribe for new device requests
|
||||||
_ = client.subscribe(filter, None).await;
|
_ = client.subscribe(filter, None).await;
|
||||||
|
|
||||||
|
Ok(keys)
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.spawn_in(window, |this, mut cx| async move {
|
||||||
|
if let Ok(keys) = task.await {
|
||||||
|
set_device_keys(keys).await;
|
||||||
|
|
||||||
|
cx.update(|window, cx| {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.fetch_request(window, cx);
|
||||||
})
|
})
|
||||||
.await;
|
.ok();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
} else {
|
} else {
|
||||||
// Send request for master keys
|
cx.update(|window, cx| {
|
||||||
if Device::request_keys(pubkey, client_keys, &cx).await.is_ok() {
|
this.update(cx, |this, cx| {
|
||||||
|
this.request_keys(window, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a request to ask for device keys from the other Nostr client
|
||||||
|
///
|
||||||
|
/// NIP-4e: <https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md>
|
||||||
|
pub fn request_keys(&self, window: &mut Window, cx: &Context<Self>) {
|
||||||
|
let Some(profile) = self.profile() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let client = get_client();
|
||||||
|
let app_name = get_app_name();
|
||||||
|
|
||||||
|
let public_key = profile.public_key;
|
||||||
|
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<Result<(), Error>> = 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...");
|
||||||
|
}
|
||||||
|
|
||||||
|
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, |this, mut cx| async move {
|
||||||
|
if task.await.is_ok() {
|
||||||
cx.update(|window, cx| {
|
cx.update(|window, cx| {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.render_waiting_modal(window, cx);
|
this.render_waiting_modal(window, cx);
|
||||||
@@ -276,6 +536,45 @@ impl Device {
|
|||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch the latest request from the other Nostr client
|
||||||
|
///
|
||||||
|
/// NIP-4e: <https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md>
|
||||||
|
fn fetch_request(&self, window: &mut Window, cx: &Context<Self>) {
|
||||||
|
let Some(profile) = self.profile() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let client = get_client();
|
||||||
|
let public_key = profile.public_key;
|
||||||
|
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kind(Kind::Custom(DEVICE_REQUEST_KIND))
|
||||||
|
.author(public_key)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
let task: Task<Result<Event, Error>> = cx.background_spawn(async move {
|
||||||
|
let events = client.fetch_events(filter, Duration::from_secs(2)).await?;
|
||||||
|
|
||||||
|
if let Some(event) = events.first_owned() {
|
||||||
|
Ok(event)
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("No request found"))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.spawn_in(window, |this, mut cx| async move {
|
||||||
|
if let Ok(event) = task.await {
|
||||||
|
cx.update(|window, cx| {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.handle_request(event, window, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
@@ -292,10 +591,11 @@ impl Device {
|
|||||||
.nip44_decrypt(&public_key, &event.content)
|
.nip44_decrypt(&public_key, &event.content)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let keys = Keys::parse(&secret)?;
|
let keys = Arc::new(Keys::parse(&secret)?);
|
||||||
|
|
||||||
// Update global state with new device keys
|
// Update global state with new device keys
|
||||||
set_device_keys(keys).await;
|
set_device_keys(keys).await;
|
||||||
|
|
||||||
log::info!("Received device keys from other client");
|
log::info!("Received device keys from other client");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -510,198 +810,4 @@ impl Device {
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch the latest request from the other Nostr client
|
|
||||||
///
|
|
||||||
/// NIP-4e: <https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md>
|
|
||||||
fn fetch_request(user: PublicKey, cx: &AsyncApp) -> Task<Result<Event, Error>> {
|
|
||||||
let client = get_client();
|
|
||||||
let filter = Filter::new()
|
|
||||||
.kind(Kind::Custom(DEVICE_REQUEST_KIND))
|
|
||||||
.author(user)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
|
||||||
let events = client.fetch_events(filter, Duration::from_secs(2)).await?;
|
|
||||||
|
|
||||||
if let Some(event) = events.first_owned() {
|
|
||||||
Ok(event)
|
|
||||||
} else {
|
|
||||||
Err(anyhow!("No request found"))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send a request to ask for device keys from the other Nostr client
|
|
||||||
///
|
|
||||||
/// NIP-4e: <https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md>
|
|
||||||
fn request_keys(user: PublicKey, client_keys: Keys, cx: &AsyncApp) -> Task<Result<(), Error>> {
|
|
||||||
let client = get_client();
|
|
||||||
let app_name = get_app_name();
|
|
||||||
|
|
||||||
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]);
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
log::info!("Waiting for response...");
|
|
||||||
|
|
||||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
|
||||||
let filter = Filter::new()
|
|
||||||
.kind(Kind::Custom(DEVICE_RESPONSE_KIND))
|
|
||||||
.author(user);
|
|
||||||
|
|
||||||
// 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(())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the master keys for current user
|
|
||||||
///
|
|
||||||
/// NIP-4e: <https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md>
|
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
fn fetch_master_keys(user: PublicKey, cx: &AsyncApp) -> Task<Result<Option<Keys>, Error>> {
|
|
||||||
let client = get_client();
|
|
||||||
|
|
||||||
let kind = Kind::Custom(DEVICE_ANNOUNCEMENT_KIND);
|
|
||||||
let filter = Filter::new().kind(kind).author(user).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() {
|
|
||||||
println!("event: {:?}", event);
|
|
||||||
Ok(event)
|
|
||||||
} else {
|
|
||||||
Err(anyhow!("Device Announcement not found."))
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.spawn(|cx| async move {
|
|
||||||
let Ok(task) = cx.update(|cx| cx.read_credentials(MASTER_KEYRING)) else {
|
|
||||||
return Err(anyhow!("Failed to read credentials"));
|
|
||||||
};
|
|
||||||
|
|
||||||
let secret = task.await;
|
|
||||||
|
|
||||||
if let Ok(event) = fetch_announcement.await {
|
|
||||||
if let Ok(Some((_, secret))) = secret {
|
|
||||||
let secret_key = SecretKey::from_slice(&secret)?;
|
|
||||||
let keys = Keys::new(secret_key);
|
|
||||||
let device_pubkey = keys.public_key();
|
|
||||||
|
|
||||||
log::info!("Device's Public Key: {:?}", device_pubkey);
|
|
||||||
|
|
||||||
let n_tag = event.tags.find(TagKind::custom("n")).context("Not found")?;
|
|
||||||
let content = n_tag.content().context("Not found")?;
|
|
||||||
let target_pubkey = PublicKey::parse(content)?;
|
|
||||||
|
|
||||||
// If device public key matches announcement public key, re-appoint as master
|
|
||||||
if device_pubkey == target_pubkey {
|
|
||||||
log::info!("Re-appointing this device as master");
|
|
||||||
return Ok(Some(keys));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(None)
|
|
||||||
} else {
|
|
||||||
log::info!("Device announcement is not found, appoint this device as master");
|
|
||||||
|
|
||||||
let app_name = get_app_name();
|
|
||||||
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 _task: Result<(), Error> = cx
|
|
||||||
.background_spawn(async move {
|
|
||||||
let signer = client.signer().await?;
|
|
||||||
let event = EventBuilder::new(kind, "")
|
|
||||||
.tags(vec![client_tag, pubkey_tag])
|
|
||||||
.sign(&signer)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if let Err(e) = client.send_event(&event).await {
|
|
||||||
log::error!("Failed to send device announcement: {}", e);
|
|
||||||
} else {
|
|
||||||
log::info!("Device announcement sent");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(Some(keys))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initialize subscription for current user
|
|
||||||
fn subscribe(user: PublicKey, cx: &AsyncApp) -> Task<Result<(), Error>> {
|
|
||||||
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);
|
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
|
||||||
// Only subscribe to the latest device announcement
|
|
||||||
client.subscribe(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(())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use futures::{select, FutureExt};
|
|||||||
use global::{
|
use global::{
|
||||||
constants::{
|
constants::{
|
||||||
ALL_MESSAGES_SUB_ID, APP_ID, APP_NAME, BOOTSTRAP_RELAYS, DEVICE_ANNOUNCEMENT_KIND,
|
ALL_MESSAGES_SUB_ID, APP_ID, APP_NAME, BOOTSTRAP_RELAYS, DEVICE_ANNOUNCEMENT_KIND,
|
||||||
DEVICE_REQUEST_KIND, DEVICE_RESPONSE_KIND, NEW_MESSAGE_SUB_ID,
|
DEVICE_REQUEST_KIND, DEVICE_RESPONSE_KIND, DEVICE_SUB_ID, NEW_MESSAGE_SUB_ID,
|
||||||
},
|
},
|
||||||
get_client, get_device_keys, set_device_name,
|
get_client, get_device_keys, set_device_name,
|
||||||
};
|
};
|
||||||
@@ -41,6 +41,8 @@ enum Signal {
|
|||||||
RequestMasterKey(Event),
|
RequestMasterKey(Event),
|
||||||
/// Receive approve master key event
|
/// Receive approve master key event
|
||||||
ReceiveMasterKey(Event),
|
ReceiveMasterKey(Event),
|
||||||
|
/// Receive announcement event
|
||||||
|
ReceiveAnnouncement,
|
||||||
/// Receive EOSE
|
/// Receive EOSE
|
||||||
Eose,
|
Eose,
|
||||||
}
|
}
|
||||||
@@ -117,6 +119,7 @@ fn main() {
|
|||||||
let rng_keys = Keys::generate();
|
let rng_keys = Keys::generate();
|
||||||
let all_id = SubscriptionId::new(ALL_MESSAGES_SUB_ID);
|
let all_id = SubscriptionId::new(ALL_MESSAGES_SUB_ID);
|
||||||
let new_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID);
|
let new_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID);
|
||||||
|
let device_id = SubscriptionId::new(DEVICE_SUB_ID);
|
||||||
let mut notifications = client.notifications();
|
let mut notifications = client.notifications();
|
||||||
|
|
||||||
while let Ok(notification) = notifications.recv().await {
|
while let Ok(notification) = notifications.recv().await {
|
||||||
@@ -190,9 +193,9 @@ fn main() {
|
|||||||
}
|
}
|
||||||
RelayMessage::EndOfStoredEvents(subscription_id) => {
|
RelayMessage::EndOfStoredEvents(subscription_id) => {
|
||||||
if all_id == *subscription_id {
|
if all_id == *subscription_id {
|
||||||
if let Err(e) = event_tx.send(Signal::Eose).await {
|
_ = event_tx.send(Signal::Eose).await;
|
||||||
log::error!("Failed to send eose: {}", e)
|
} else if device_id == *subscription_id {
|
||||||
};
|
_ = event_tx.send(Signal::ReceiveAnnouncement).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -269,6 +272,13 @@ fn main() {
|
|||||||
chats.update(cx, |this, cx| this.push_message(event, cx))
|
chats.update(cx, |this, cx| this.push_message(event, cx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Signal::ReceiveAnnouncement => {
|
||||||
|
if let Some(device) = Device::global(cx) {
|
||||||
|
device.update(cx, |this, cx| {
|
||||||
|
this.setup_device(window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Signal::ReceiveMasterKey(event) => {
|
Signal::ReceiveMasterKey(event) => {
|
||||||
if let Some(device) = Device::global(cx) {
|
if let Some(device) = Device::global(cx) {
|
||||||
device.update(cx, |this, cx| {
|
device.update(cx, |this, cx| {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use global::{constants::IMAGE_SERVICE, get_client};
|
|||||||
use gpui::{
|
use gpui::{
|
||||||
div, img, prelude::FluentBuilder, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
div, img, prelude::FluentBuilder, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
||||||
Flatten, FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions, Render,
|
Flatten, FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions, Render,
|
||||||
SharedString, Styled, Window,
|
SharedString, Styled, Task, Window,
|
||||||
};
|
};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use smol::fs;
|
use smol::fs;
|
||||||
@@ -79,30 +79,18 @@ impl Profile {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let client = get_client();
|
let client = get_client();
|
||||||
let (tx, rx) = oneshot::channel::<Option<Metadata>>();
|
let task: Task<Result<Option<Metadata>, Error>> = cx.background_spawn(async move {
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
|
||||||
let result = async {
|
|
||||||
let signer = client.signer().await?;
|
let signer = client.signer().await?;
|
||||||
let public_key = signer.get_public_key().await?;
|
let public_key = signer.get_public_key().await?;
|
||||||
let metadata = client
|
let metadata = client
|
||||||
.fetch_metadata(public_key, Duration::from_secs(2))
|
.fetch_metadata(public_key, Duration::from_secs(2))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok::<_, anyhow::Error>(metadata)
|
Ok(metadata)
|
||||||
}
|
});
|
||||||
.await;
|
|
||||||
|
|
||||||
if let Ok(metadata) = result {
|
|
||||||
_ = tx.send(Some(metadata));
|
|
||||||
} else {
|
|
||||||
_ = tx.send(None);
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
if let Ok(Some(metadata)) = rx.await {
|
if let Ok(Some(metadata)) = task.await {
|
||||||
_ = cx.update_window(window_handle, |_, window, cx| {
|
_ = cx.update_window(window_handle, |_, window, cx| {
|
||||||
_ = this.update(cx, |this: &mut Profile, cx| {
|
_ = this.update(cx, |this: &mut Profile, cx| {
|
||||||
this.avatar_input.update(cx, |this, cx| {
|
this.avatar_input.update(cx, |this, cx| {
|
||||||
|
|||||||
@@ -222,6 +222,7 @@ impl Compose {
|
|||||||
let metadata = client
|
let metadata = client
|
||||||
.fetch_metadata(public_key, Duration::from_secs(2))
|
.fetch_metadata(public_key, Duration::from_secs(2))
|
||||||
.await
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
Ok(NostrProfile::new(public_key, metadata))
|
Ok(NostrProfile::new(public_key, metadata))
|
||||||
@@ -237,6 +238,7 @@ impl Compose {
|
|||||||
let metadata = client
|
let metadata = client
|
||||||
.fetch_metadata(public_key, Duration::from_secs(2))
|
.fetch_metadata(public_key, Duration::from_secs(2))
|
||||||
.await
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
Ok(NostrProfile::new(public_key, metadata))
|
Ok(NostrProfile::new(public_key, metadata))
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ pub const BOOTSTRAP_RELAYS: [&str; 3] = [
|
|||||||
/// Subscriptions
|
/// Subscriptions
|
||||||
pub const NEW_MESSAGE_SUB_ID: &str = "listen_new_giftwraps";
|
pub const NEW_MESSAGE_SUB_ID: &str = "listen_new_giftwraps";
|
||||||
pub const ALL_MESSAGES_SUB_ID: &str = "listen_all_giftwraps";
|
pub const ALL_MESSAGES_SUB_ID: &str = "listen_all_giftwraps";
|
||||||
|
pub const DEVICE_SUB_ID: &str = "listen_device_announcement";
|
||||||
|
|
||||||
/// Image Resizer Service
|
/// Image Resizer Service
|
||||||
pub const IMAGE_SERVICE: &str = "https://wsrv.nl";
|
pub const IMAGE_SERVICE: &str = "https://wsrv.nl";
|
||||||
|
|||||||
@@ -63,11 +63,11 @@ pub async fn get_device_keys() -> Option<Arc<dyn NostrSigner>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set device keys
|
/// Set device keys
|
||||||
pub async fn set_device_keys<T>(signer: T)
|
pub async fn set_device_keys<T>(signer: Arc<T>)
|
||||||
where
|
where
|
||||||
T: NostrSigner + 'static,
|
T: NostrSigner + 'static,
|
||||||
{
|
{
|
||||||
DEVICE_KEYS.lock().await.replace(Arc::new(signer));
|
DEVICE_KEYS.lock().await.replace(signer);
|
||||||
|
|
||||||
// Re-subscribe to all messages
|
// Re-subscribe to all messages
|
||||||
smol::spawn(async move {
|
smol::spawn(async move {
|
||||||
|
|||||||
Reference in New Issue
Block a user