refactor device

This commit is contained in:
2026-03-16 14:00:11 +07:00
parent 1f9c0444d5
commit f075a83320
3 changed files with 256 additions and 245 deletions

View File

@@ -37,6 +37,7 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<Workspace> {
cx.new(|cx| Workspace::new(window, cx))
}
struct DeviceNotifcation;
struct SignerNotifcation;
struct RelayNotifcation;
@@ -171,9 +172,32 @@ impl Workspace {
cx,
);
}
DeviceEvent::NotSet { reason } => {
let note = Notification::new()
.id::<DeviceNotifcation>()
.title("Cannot setup the encryption key")
.message(reason)
.autohide(false)
.with_kind(NotificationKind::Error);
window.push_notification(note, cx);
}
DeviceEvent::NotSubscribe { reason } => {
let note = Notification::new()
.id::<DeviceNotifcation>()
.title("Cannot getting messages")
.message(reason)
.autohide(false)
.with_kind(NotificationKind::Error);
window.push_notification(note, cx);
}
DeviceEvent::Error(error) => {
window.push_notification(Notification::error(error).autohide(false), cx);
}
_ => {
// TODO
}
};
}),
);
@@ -424,35 +448,13 @@ impl Workspace {
.child(SharedString::from(ENC_WARN)),
),
)
.on_ok(move |_ev, window, cx| {
.on_ok(move |_ev, _window, cx| {
let device = DeviceRegistry::global(cx);
let task = device.read(cx).create_encryption(cx);
window
.spawn(cx, async move |cx| {
let result = task.await;
cx.update(|window, cx| match result {
Ok(keys) => {
device.update(cx, |this, cx| {
this.set_signer(keys, cx);
this.listen_request(cx);
});
window.close_modal(cx);
}
Err(e) => {
window.push_notification(
Notification::error(e.to_string()).autohide(false),
cx,
);
}
})
.ok();
})
.detach();
// false to keep modal open
false
device.update(cx, |this, cx| {
this.set_announcement(cx);
});
// true to close modal
true
})
});
}
@@ -702,25 +704,46 @@ impl Workspace {
.ghost()
.dropdown_menu(move |this, _window, cx| {
let device = DeviceRegistry::global(cx);
let state = device.read(cx).state();
let subscribing = device.read(cx).subscribing;
let requesting = device.read(cx).requesting;
this.min_w(px(260.))
.when(requesting, |this| {
this.item(PopupMenuItem::element(move |_window, cx| {
h_flex()
.px_1()
.w_full()
.gap_2()
.text_sm()
.child(
div()
.size_1p5()
.rounded_full()
.bg(cx.theme().icon_accent),
)
.child(SharedString::from("Waiting for approval..."))
}))
})
.item(PopupMenuItem::element(move |_window, cx| {
h_flex()
.px_1()
.w_full()
.gap_2()
.text_sm()
.child(
div()
.size_1p5()
.rounded_full()
.when(state.set(), |this| this.bg(gpui::green()))
.when(state.requesting(), |this| {
this.bg(cx.theme().icon_accent)
}),
)
.child(SharedString::from(state.to_string()))
.child(div().size_1p5().rounded_full().map(|this| {
if subscribing {
this.bg(cx.theme().icon_accent)
} else {
this.bg(cx.theme().icon_muted)
}
}))
.map(|this| {
if subscribing {
this.child(SharedString::from("Getting messages..."))
} else {
this.child(SharedString::from("Not getting messages"))
}
})
}))
.separator()
.menu_with_icon(

View File

@@ -10,9 +10,7 @@ use gpui::{
};
use nostr_sdk::prelude::*;
use person::PersonRegistry;
use state::{
Announcement, DEVICE_GIFTWRAP, DeviceState, NostrRegistry, StateEvent, TIMEOUT, app_name,
};
use state::{Announcement, DEVICE_GIFTWRAP, NostrRegistry, StateEvent, TIMEOUT, app_name};
use theme::ActiveTheme;
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
@@ -36,17 +34,53 @@ impl Global for GlobalDeviceRegistry {}
pub enum DeviceEvent {
/// A new encryption signer has been set
Set,
/// The encryption key has been reset
Reset,
/// Encryption key is not set
NotSet { reason: SharedString },
/// An event to notify that Coop isn't subscribed to gift wrap events
NotSubscribe { reason: SharedString },
/// An error occurred
Error(SharedString),
}
impl DeviceEvent {
pub fn error<T>(error: T) -> Self
where
T: Into<SharedString>,
{
Self::Error(error.into())
}
pub fn not_subscribe<T>(reason: T) -> Self
where
T: Into<SharedString>,
{
Self::NotSubscribe {
reason: reason.into(),
}
}
pub fn not_set<T>(reason: T) -> Self
where
T: Into<SharedString>,
{
Self::NotSet {
reason: reason.into(),
}
}
}
/// Device Registry
///
/// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
#[derive(Debug)]
pub struct DeviceRegistry {
/// Device state
state: DeviceState,
/// Whether the registry is currently subscribing to gift wrap events
pub subscribing: bool,
/// Whether the registry is waiting for encryption key approval from other devices
pub requesting: bool,
/// Async tasks
tasks: Vec<Task<Result<(), Error>>>,
@@ -71,30 +105,30 @@ impl DeviceRegistry {
/// Create a new device registry instance
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let nostr = NostrRegistry::global(cx);
let state = DeviceState::default();
let subscription = Some(cx.subscribe_in(
&nostr,
window,
|this, _state, event, _window, cx| match event {
// Get announcement when signer is set
let subscription = cx.subscribe_in(&nostr, window, |this, _e, event, _window, cx| {
match event {
StateEvent::SignerSet => {
this.reset(cx);
this.set_subscribing(false, cx);
this.set_requesting(false, cx);
}
StateEvent::RelayConnected => {
this.get_announcement(cx);
}
_ => {}
},
));
};
});
cx.defer_in(window, |this, window, cx| {
this.handle_notifications(window, cx);
});
Self {
state,
subscribing: false,
requesting: false,
tasks: vec![],
_subscription: subscription,
_subscription: Some(subscription),
}
}
@@ -140,13 +174,13 @@ impl DeviceRegistry {
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
while let Ok(event) = rx.recv_async().await {
match event.kind {
// New request event
// New request event from other device
Kind::Custom(4454) => {
this.update_in(cx, |this, window, cx| {
this.ask_for_approval(event, window, cx);
})?;
}
// New response event
// New response event from the master device
Kind::Custom(4455) => {
this.update(cx, |this, cx| {
this.extract_encryption(event, cx);
@@ -155,24 +189,24 @@ impl DeviceRegistry {
_ => {}
}
}
Ok(())
}));
}
/// Get the device state
pub fn state(&self) -> DeviceState {
self.state.clone()
/// Set whether the registry is currently subscribing to gift wrap events
fn set_subscribing(&mut self, subscribing: bool, cx: &mut Context<Self>) {
self.subscribing = subscribing;
cx.notify();
}
/// Set the device state
fn set_state(&mut self, state: DeviceState, cx: &mut Context<Self>) {
self.state = state;
/// Set whether the registry is waiting for encryption key approval from other devices
fn set_requesting(&mut self, requesting: bool, cx: &mut Context<Self>) {
self.requesting = requesting;
cx.notify();
}
/// Set the decoupled encryption key for the current user
pub fn set_signer<S>(&mut self, new: S, cx: &mut Context<Self>)
fn set_signer<S>(&mut self, new: S, cx: &mut Context<Self>)
where
S: NostrSigner + 'static,
{
@@ -184,7 +218,7 @@ impl DeviceRegistry {
// Update state
this.update(cx, |this, cx| {
this.set_state(DeviceState::Set, cx);
cx.emit(DeviceEvent::Set);
this.get_messages(cx);
})?;
@@ -192,12 +226,6 @@ impl DeviceRegistry {
}));
}
/// Reset the device state
fn reset(&mut self, cx: &mut Context<Self>) {
self.state = DeviceState::Idle;
cx.notify();
}
/// Get all messages for encryption keys
fn get_messages(&mut self, cx: &mut Context<Self>) {
let task = self.subscribe_to_giftwrap_events(cx);
@@ -205,59 +233,50 @@ impl DeviceRegistry {
self.tasks.push(cx.spawn(async move |this, cx| {
if let Err(e) = task.await {
this.update(cx, |_this, cx| {
cx.emit(DeviceEvent::Error(SharedString::from(e.to_string())));
cx.emit(DeviceEvent::not_subscribe(e.to_string()));
})?;
} else {
this.update(cx, |this, cx| {
this.set_subscribing(true, cx);
})?;
}
Ok(())
}));
}
/// Get the messaging relays for the current user
fn get_user_messaging_relays(&self, cx: &App) -> Task<Result<Vec<RelayUrl>, Error>> {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer();
cx.background_spawn(async move {
let public_key = signer.get_public_key().await?;
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(public_key)
.limit(1);
if let Some(event) = client.database().query(filter).await?.first_owned() {
// Extract relay URLs from the event
let urls: Vec<RelayUrl> = nip17::extract_owned_relay_list(event).collect();
// Ensure all relays are connected
for url in urls.iter() {
client.add_relay(url).and_connect().await?;
}
Ok(urls)
} else {
Err(anyhow!("Relays not found"))
}
})
}
/// Continuously get gift wrap events for the current user in their messaging relays
fn subscribe_to_giftwrap_events(&self, cx: &App) -> Task<Result<(), Error>> {
let persons = PersonRegistry::global(cx);
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer();
let urls = self.get_user_messaging_relays(cx);
let Some(user) = signer.public_key() else {
return Task::ready(Err(anyhow!("User not found")));
};
let profile = persons.read(cx).get(&user, cx);
let relays = profile.messaging_relays().clone();
cx.background_spawn(async move {
let urls = urls.await?;
let encryption = signer.get_encryption_signer().await.context("not found")?;
let public_key = encryption.get_public_key().await?;
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
let id = SubscriptionId::new(DEVICE_GIFTWRAP);
// Ensure user has relays configured
if relays.is_empty() {
return Err(anyhow!("No messaging relays found"));
}
// Ensure relays are connected
for url in relays.iter() {
client.add_relay(url).and_connect().await?;
}
// Construct target for subscription
let target: HashMap<RelayUrl, Filter> = urls
let target: HashMap<RelayUrl, Filter> = relays
.into_iter()
.map(|relay| (relay, filter.clone()))
.collect();
@@ -302,13 +321,15 @@ impl DeviceRegistry {
self.tasks.push(cx.spawn(async move |this, cx| {
match task.await {
Ok(event) => {
// Set encryption key from the announcement event
this.update(cx, |this, cx| {
this.new_signer(&event, cx);
this.set_encryption(&event, cx);
})?;
}
Err(_) => {
// User has no announcement, create a new one
this.update(cx, |this, cx| {
this.announce(cx);
this.set_announcement(cx);
})?;
}
}
@@ -317,8 +338,30 @@ impl DeviceRegistry {
}));
}
/// Create a new device signer and announce it to user's relay list
pub fn set_announcement(&mut self, cx: &mut Context<Self>) {
let task = self.new_encryption(cx);
self.tasks.push(cx.spawn(async move |this, cx| {
match task.await {
Ok(keys) => {
this.update(cx, |this, cx| {
this.set_signer(keys, cx);
this.wait_for_request(cx);
})?;
}
Err(e) => {
this.update(cx, |_this, cx| {
cx.emit(DeviceEvent::error(e.to_string()));
})?;
}
}
Ok(())
}));
}
/// Create new encryption keys
pub fn create_encryption(&self, cx: &App) -> Task<Result<Keys, Error>> {
fn new_encryption(&self, cx: &App) -> Task<Result<Keys, Error>> {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
@@ -328,12 +371,13 @@ impl DeviceRegistry {
cx.background_spawn(async move {
// Construct an announcement event
let event = client
.sign_event_builder(EventBuilder::new(Kind::Custom(10044), "").tags(vec![
Tag::custom(TagKind::custom("n"), vec![n]),
Tag::client(app_name()),
]))
.await?;
let builder = EventBuilder::new(Kind::Custom(10044), "").tags(vec![
Tag::custom(TagKind::custom("n"), vec![n]),
Tag::client(app_name()),
]);
// Sign the event with user's signer
let event = client.sign_event_builder(builder).await?;
// Publish announcement
client.send_event(&event).to_nip65().await?;
@@ -345,39 +389,23 @@ impl DeviceRegistry {
})
}
/// Create a new device signer and announce it
fn announce(&mut self, cx: &mut Context<Self>) {
let task = self.create_encryption(cx);
self.tasks.push(cx.spawn(async move |this, cx| {
let keys = task.await?;
// Update signer
this.update(cx, |this, cx| {
this.set_signer(keys, cx);
this.listen_request(cx);
})?;
Ok(())
}));
}
/// Initialize device signer (decoupled encryption key) for the current user
pub fn new_signer(&mut self, event: &Event, cx: &mut Context<Self>) {
/// Set encryption key from the announcement event
fn set_encryption(&mut self, event: &Event, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let announcement = Announcement::from(event);
let device_pubkey = announcement.public_key();
// Get encryption key from the database and compare with the announcement
let task: Task<Result<Keys, Error>> = cx.background_spawn(async move {
if let Ok(keys) = get_keys(&client).await {
if keys.public_key() != device_pubkey {
return Err(anyhow!("Key mismatch"));
return Err(anyhow!("Encryption Key doesn't match the announcement"));
};
Ok(keys)
} else {
Err(anyhow!("Key not found"))
Err(anyhow!("Encryption Key not found. Please create a new key"))
}
});
@@ -386,74 +414,49 @@ impl DeviceRegistry {
Ok(keys) => {
this.update(cx, |this, cx| {
this.set_signer(keys, cx);
this.listen_request(cx);
this.wait_for_request(cx);
})?;
}
Err(e) => {
log::warn!("Failed to initialize device signer: {e}");
this.update(cx, |this, cx| {
this.request(cx);
this.listen_approval(cx);
this.update(cx, |_this, cx| {
cx.emit(DeviceEvent::not_set(e.to_string()));
})?;
}
};
Ok(())
}));
}
/// Listen for device key requests on user's write relays
pub fn listen_request(&mut self, cx: &mut Context<Self>) {
/// Wait for encryption key requests from now on
fn wait_for_request(&mut self, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer();
let Some(public_key) = signer.public_key() else {
return;
};
self.tasks.push(cx.background_spawn(async move {
let public_key = signer.get_public_key().await?;
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
// Construct a filter for device key requests
let filter = Filter::new()
// Construct a filter for encryption key requests
let now = Filter::new()
.kind(Kind::Custom(4454))
.author(public_key)
.since(Timestamp::now());
// Subscribe to the device key requests on user's write relays
client.subscribe(filter).await?;
Ok(())
});
task.detach();
}
/// Listen for device key approvals on user's write relays
fn listen_approval(&mut self, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer();
let Some(public_key) = signer.public_key() else {
return;
};
self.tasks.push(cx.background_spawn(async move {
// Construct a filter for device key requests
let filter = Filter::new()
.kind(Kind::Custom(4455))
// Construct a filter for the last encryption key request
let last = Filter::new()
.kind(Kind::Custom(4454))
.author(public_key)
.since(Timestamp::now());
.limit(1);
// Subscribe to the device key requests on user's write relays
client.subscribe(filter).await?;
client.subscribe(vec![now, last]).await?;
Ok(())
}));
}
/// Request encryption keys from other device
fn request(&mut self, cx: &mut Context<Self>) {
pub fn request(&mut self, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer();
@@ -461,9 +464,10 @@ impl DeviceRegistry {
let app_keys = nostr.read(cx).keys();
let app_pubkey = app_keys.public_key();
let task: Task<Result<Option<Keys>, Error>> = cx.background_spawn(async move {
let task: Task<Result<Option<Event>, Error>> = cx.background_spawn(async move {
let public_key = signer.get_public_key().await?;
// Construct a filter to get the latest approval event
let filter = Filter::new()
.kind(Kind::Custom(4455))
.author(public_key)
@@ -471,30 +475,18 @@ impl DeviceRegistry {
.limit(1);
match client.database().query(filter).await?.first_owned() {
Some(event) => {
let root_device = event
.tags
.find(TagKind::custom("P"))
.and_then(|tag| tag.content())
.and_then(|content| PublicKey::parse(content).ok())
.context("Invalid event's tags")?;
let payload = event.content.as_str();
let decrypted = app_keys.nip44_decrypt(&root_device, payload).await?;
let secret = SecretKey::from_hex(&decrypted)?;
let keys = Keys::new(secret);
Ok(Some(keys))
}
// Found an approval event
Some(event) => Ok(Some(event)),
// No approval event found, construct a request event
None => {
// Construct an event for device key request
let event = client
.sign_event_builder(EventBuilder::new(Kind::Custom(4454), "").tags(vec![
Tag::client(app_name()),
Tag::custom(TagKind::custom("P"), vec![app_pubkey]),
]))
.await?;
let builder = EventBuilder::new(Kind::Custom(4454), "").tags(vec![
Tag::custom(TagKind::custom("P"), vec![app_pubkey]),
Tag::client(app_name()),
]);
// Sign the event with user's signer
let event = client.sign_event_builder(builder).await?;
// Send the event to write relays
client.send_event(&event).to_nip65().await?;
@@ -506,32 +498,56 @@ impl DeviceRegistry {
self.tasks.push(cx.spawn(async move |this, cx| {
match task.await {
Ok(Some(keys)) => {
Ok(Some(event)) => {
this.update(cx, |this, cx| {
this.set_signer(keys, cx);
this.extract_encryption(event, cx);
})?;
}
Ok(None) => {
this.update(cx, |this, cx| {
this.set_state(DeviceState::Requesting, cx);
this.set_requesting(true, cx);
this.wait_for_approval(cx);
})?;
}
Err(e) => {
log::error!("Failed to request the encryption key: {e}");
this.update(cx, |_this, cx| {
cx.emit(DeviceEvent::error(e.to_string()));
})?;
}
};
Ok(())
}));
}
/// Wait for encryption key approvals
fn wait_for_approval(&mut self, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let signer = nostr.read(cx).signer();
self.tasks.push(cx.background_spawn(async move {
let public_key = signer.get_public_key().await?;
// Construct a filter for device key requests
let filter = Filter::new()
.kind(Kind::Custom(4455))
.author(public_key)
.since(Timestamp::now());
// Subscribe to the device key requests on user's write relays
client.subscribe(filter).await?;
Ok(())
}));
}
/// Parse the response event for device keys from other devices
/// Parse the approval event to get encryption key then set it
fn extract_encryption(&mut self, event: Event, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let app_keys = nostr.read(cx).keys();
let task: Task<Result<Keys, Error>> = cx.background_spawn(async move {
let root_device = event
let master = event
.tags
.find(TagKind::custom("P"))
.and_then(|tag| tag.content())
@@ -539,7 +555,7 @@ impl DeviceRegistry {
.context("Invalid event's tags")?;
let payload = event.content.as_str();
let decrypted = app_keys.nip44_decrypt(&root_device, payload).await?;
let decrypted = app_keys.nip44_decrypt(&master, payload).await?;
let secret = SecretKey::from_hex(&decrypted)?;
let keys = Keys::new(secret);
@@ -548,13 +564,19 @@ impl DeviceRegistry {
});
self.tasks.push(cx.spawn(async move |this, cx| {
let keys = task.await?;
// Update signer
this.update(cx, |this, cx| {
this.set_signer(keys, cx);
})?;
match task.await {
Ok(keys) => {
this.update(cx, |this, cx| {
this.set_signer(keys, cx);
this.set_requesting(false, cx);
})?;
}
Err(e) => {
this.update(cx, |_this, cx| {
cx.emit(DeviceEvent::not_set(e.to_string()));
})?;
}
}
Ok(())
}));
}

View File

@@ -1,40 +1,6 @@
use std::fmt::Display;
use gpui::SharedString;
use nostr_sdk::prelude::*;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub enum DeviceState {
#[default]
Idle,
Requesting,
Set,
}
impl Display for DeviceState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DeviceState::Idle => write!(f, "Idle"),
DeviceState::Requesting => write!(f, "Wait for approval"),
DeviceState::Set => write!(f, "Encryption Key is ready"),
}
}
}
impl DeviceState {
pub fn idle(&self) -> bool {
matches!(self, DeviceState::Idle)
}
pub fn requesting(&self) -> bool {
matches!(self, DeviceState::Requesting)
}
pub fn set(&self) -> bool {
matches!(self, DeviceState::Set)
}
}
/// Announcement
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Announcement {