feat: nip4e (#188)
* encryption keys * . * . * move nip4e to device crate * . * . * use i18n for device crate * refactor * refactor * . * add reset button * send message with encryption keys * clean up * . * choose signer * fix * update i18n * fix sending
This commit is contained in:
390
Cargo.lock
generated
390
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -64,7 +64,7 @@ pub trait RenderedTimestamp {
|
|||||||
|
|
||||||
impl RenderedTimestamp for Timestamp {
|
impl RenderedTimestamp for Timestamp {
|
||||||
fn to_human_time(&self) -> SharedString {
|
fn to_human_time(&self) -> SharedString {
|
||||||
let input_time = match Local.timestamp_opt(self.as_u64() as i64, 0) {
|
let input_time = match Local.timestamp_opt(self.as_secs() as i64, 0) {
|
||||||
chrono::LocalResult::Single(time) => time,
|
chrono::LocalResult::Single(time) => time,
|
||||||
_ => return SharedString::from("9999"),
|
_ => return SharedString::from("9999"),
|
||||||
};
|
};
|
||||||
@@ -85,7 +85,7 @@ impl RenderedTimestamp for Timestamp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn to_ago(&self) -> SharedString {
|
fn to_ago(&self) -> SharedString {
|
||||||
let input_time = match Local.timestamp_opt(self.as_u64() as i64, 0) {
|
let input_time = match Local.timestamp_opt(self.as_secs() as i64, 0) {
|
||||||
chrono::LocalResult::Single(time) => time,
|
chrono::LocalResult::Single(time) => time,
|
||||||
_ => return SharedString::from("1m"),
|
_ => return SharedString::from("1m"),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ title_bar = { path = "../title_bar" }
|
|||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
states = { path = "../states" }
|
states = { path = "../states" }
|
||||||
|
key_store = { path = "../key_store" }
|
||||||
registry = { path = "../registry" }
|
registry = { path = "../registry" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
auto_update = { path = "../auto_update" }
|
auto_update = { path = "../auto_update" }
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use gpui::{actions, App, AppContext};
|
use gpui::{actions, App, AppContext};
|
||||||
|
use key_store::backend::KeyItem;
|
||||||
|
use key_store::KeyStore;
|
||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
use registry::keystore::KeyItem;
|
|
||||||
use registry::Registry;
|
use registry::Registry;
|
||||||
use states::app_state;
|
use states::app_state;
|
||||||
|
|
||||||
@@ -48,7 +49,7 @@ pub fn load_embedded_fonts(cx: &App) {
|
|||||||
|
|
||||||
pub fn reset(cx: &mut App) {
|
pub fn reset(cx: &mut App) {
|
||||||
let registry = Registry::global(cx);
|
let registry = Registry::global(cx);
|
||||||
let keystore = registry.read(cx).keystore();
|
let backend = KeyStore::global(cx).read(cx).backend();
|
||||||
|
|
||||||
cx.spawn(async move |cx| {
|
cx.spawn(async move |cx| {
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
@@ -57,12 +58,12 @@ pub fn reset(cx: &mut App) {
|
|||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
keystore
|
backend
|
||||||
.delete_credentials(&KeyItem::User.to_string(), cx)
|
.delete_credentials(&KeyItem::User.to_string(), cx)
|
||||||
.await
|
.await
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
keystore
|
backend
|
||||||
.delete_credentials(&KeyItem::Bunker.to_string(), cx)
|
.delete_credentials(&KeyItem::Bunker.to_string(), cx)
|
||||||
.await
|
.await
|
||||||
.ok();
|
.ok();
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use anyhow::{anyhow, Error};
|
use anyhow::{anyhow, Error};
|
||||||
use auto_update::AutoUpdater;
|
use auto_update::AutoUpdater;
|
||||||
use common::display::RenderedProfile;
|
use common::display::{shorten_pubkey, RenderedProfile};
|
||||||
use common::event::EventUtils;
|
use common::event::EventUtils;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
@@ -14,14 +14,15 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
use i18n::{shared_t, t};
|
use i18n::{shared_t, t};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
use key_store::backend::KeyItem;
|
||||||
|
use key_store::KeyStore;
|
||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use registry::keystore::KeyItem;
|
|
||||||
use registry::{Registry, RegistryEvent};
|
use registry::{Registry, RegistryEvent};
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use states::constants::{BOOTSTRAP_RELAYS, DEFAULT_SIDEBAR_WIDTH};
|
use states::constants::{BOOTSTRAP_RELAYS, DEFAULT_SIDEBAR_WIDTH};
|
||||||
use states::state::{AuthRequest, SignalKind, UnwrappingStatus};
|
use states::state::{Announcement, AuthRequest, Response, SignalKind, UnwrappingStatus};
|
||||||
use states::{app_state, default_nip17_relays, default_nip65_relays};
|
use states::{app_state, default_nip17_relays, default_nip65_relays};
|
||||||
use theme::{ActiveTheme, Theme, ThemeMode};
|
use theme::{ActiveTheme, Theme, ThemeMode};
|
||||||
use title_bar::TitleBar;
|
use title_bar::TitleBar;
|
||||||
@@ -74,7 +75,7 @@ pub struct ChatSpace {
|
|||||||
nip65_ready: bool,
|
nip65_ready: bool,
|
||||||
|
|
||||||
/// All subscriptions for observing the app state
|
/// All subscriptions for observing the app state
|
||||||
_subscriptions: SmallVec<[Subscription; 4]>,
|
_subscriptions: SmallVec<[Subscription; 3]>,
|
||||||
|
|
||||||
/// All long running tasks
|
/// All long running tasks
|
||||||
_tasks: SmallVec<[Task<()>; 5]>,
|
_tasks: SmallVec<[Task<()>; 5]>,
|
||||||
@@ -83,7 +84,7 @@ pub struct ChatSpace {
|
|||||||
impl ChatSpace {
|
impl ChatSpace {
|
||||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let registry = Registry::global(cx);
|
let registry = Registry::global(cx);
|
||||||
let status = registry.read(cx).unwrapping_status.clone();
|
let keystore = KeyStore::global(cx);
|
||||||
|
|
||||||
let title_bar = cx.new(|_| TitleBar::new());
|
let title_bar = cx.new(|_| TitleBar::new());
|
||||||
let dock = cx.new(|cx| DockArea::new(window, cx));
|
let dock = cx.new(|cx| DockArea::new(window, cx));
|
||||||
@@ -100,21 +101,18 @@ impl ChatSpace {
|
|||||||
);
|
);
|
||||||
|
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
// Observe the keystore
|
// Observe device changes
|
||||||
cx.observe_in(®istry, window, |this, registry, window, cx| {
|
cx.observe_in(&keystore, window, move |this, state, window, cx| {
|
||||||
let has_keyring = registry.read(cx).initialized_keystore;
|
if state.read(cx).initialized {
|
||||||
let use_filestore = registry.read(cx).is_using_file_keystore();
|
let backend = state.read(cx).backend();
|
||||||
let not_logged_in = registry.read(cx).signer_pubkey().is_none();
|
|
||||||
|
|
||||||
if use_filestore && not_logged_in {
|
if state.read(cx).initialized {
|
||||||
|
if state.read(cx).is_using_file_keystore() {
|
||||||
this.render_keyring_installation(window, cx);
|
this.render_keyring_installation(window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
if has_keyring && not_logged_in {
|
|
||||||
let keystore = registry.read(cx).keystore();
|
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
let result = keystore
|
let result = backend
|
||||||
.read_credentials(&KeyItem::User.to_string(), cx)
|
.read_credentials(&KeyItem::User.to_string(), cx)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -123,6 +121,7 @@ impl ChatSpace {
|
|||||||
Ok(Some((user, secret))) => {
|
Ok(Some((user, secret))) => {
|
||||||
let public_key = PublicKey::parse(&user).unwrap();
|
let public_key = PublicKey::parse(&user).unwrap();
|
||||||
let secret = String::from_utf8(secret).unwrap();
|
let secret = String::from_utf8(secret).unwrap();
|
||||||
|
|
||||||
this.set_account_layout(public_key, secret, window, cx);
|
this.set_account_layout(public_key, secret, window, cx);
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
@@ -134,23 +133,6 @@ impl ChatSpace {
|
|||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
subscriptions.push(
|
|
||||||
// Observe the global registry's events
|
|
||||||
cx.observe_in(&status, window, move |this, status, window, cx| {
|
|
||||||
let status = status.read(cx);
|
|
||||||
let all_panels = this.get_all_panel_ids(cx);
|
|
||||||
|
|
||||||
if matches!(
|
|
||||||
status,
|
|
||||||
UnwrappingStatus::Processing | UnwrappingStatus::Complete
|
|
||||||
) {
|
|
||||||
Registry::global(cx).update(cx, |this, cx| {
|
|
||||||
this.load_rooms(window, cx);
|
|
||||||
this.refresh_rooms(all_panels, cx);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -238,9 +220,21 @@ impl ChatSpace {
|
|||||||
let settings = AppSettings::global(cx);
|
let settings = AppSettings::global(cx);
|
||||||
|
|
||||||
match signal {
|
match signal {
|
||||||
|
SignalKind::EncryptionNotSet => {
|
||||||
|
this.init_encryption(window, cx);
|
||||||
|
}
|
||||||
|
SignalKind::EncryptionSet(announcement) => {
|
||||||
|
this.load_encryption(announcement, window, cx);
|
||||||
|
}
|
||||||
|
SignalKind::EncryptionRequest(announcement) => {
|
||||||
|
this.render_request(announcement, window, cx);
|
||||||
|
}
|
||||||
|
SignalKind::EncryptionResponse(response) => {
|
||||||
|
this.receive_encryption(response, window, cx);
|
||||||
|
}
|
||||||
SignalKind::SignerSet(public_key) => {
|
SignalKind::SignerSet(public_key) => {
|
||||||
// Close the latest modal if it exists
|
// Close all opened modals
|
||||||
window.close_modal(cx);
|
window.close_all_modals(cx);
|
||||||
|
|
||||||
// Load user's settings
|
// Load user's settings
|
||||||
settings.update(cx, |this, cx| {
|
settings.update(cx, |this, cx| {
|
||||||
@@ -256,15 +250,6 @@ impl ChatSpace {
|
|||||||
// Setup the default layout for current workspace
|
// Setup the default layout for current workspace
|
||||||
this.set_default_layout(window, cx);
|
this.set_default_layout(window, cx);
|
||||||
}
|
}
|
||||||
SignalKind::SignerUnset => {
|
|
||||||
// Clear all current chat rooms
|
|
||||||
registry.update(cx, |this, cx| {
|
|
||||||
this.reset(cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Setup the onboarding layout for current workspace
|
|
||||||
this.set_onboarding_layout(window, cx);
|
|
||||||
}
|
|
||||||
SignalKind::Auth(req) => {
|
SignalKind::Auth(req) => {
|
||||||
let url = &req.url;
|
let url = &req.url;
|
||||||
let auto_auth = AppSettings::get_auto_auth(cx);
|
let auto_auth = AppSettings::get_auto_auth(cx);
|
||||||
@@ -281,11 +266,20 @@ impl ChatSpace {
|
|||||||
this.open_auth_request(req, window, cx);
|
this.open_auth_request(req, window, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SignalKind::GiftWrapStatus(status) => {
|
SignalKind::GiftWrapStatus(s) => {
|
||||||
|
if matches!(s, UnwrappingStatus::Processing | UnwrappingStatus::Complete) {
|
||||||
|
let all_panels = this.get_all_panel_ids(cx);
|
||||||
|
|
||||||
registry.update(cx, |this, cx| {
|
registry.update(cx, |this, cx| {
|
||||||
this.set_unwrapping_status(status, cx);
|
this.load_rooms(window, cx);
|
||||||
|
this.refresh_rooms(all_panels, cx);
|
||||||
|
|
||||||
|
if s == UnwrappingStatus::Complete {
|
||||||
|
this.set_loading(false, cx);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
SignalKind::NewProfile(profile) => {
|
SignalKind::NewProfile(profile) => {
|
||||||
registry.update(cx, |this, cx| {
|
registry.update(cx, |this, cx| {
|
||||||
this.insert_or_update_person(profile, cx);
|
this.insert_or_update_person(profile, cx);
|
||||||
@@ -309,6 +303,92 @@ impl ChatSpace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn init_encryption(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
let result = app_state().init_encryption_keys().await;
|
||||||
|
|
||||||
|
this.update_in(cx, |_, window, cx| {
|
||||||
|
match result {
|
||||||
|
Ok(_) => {
|
||||||
|
window.push_notification(t!("encryption.notice"), cx);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// TODO: ask user to confirm re-running if failed
|
||||||
|
window.push_notification(e.to_string(), cx);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_encryption(&self, ann: Announcement, window: &Window, cx: &Context<Self>) {
|
||||||
|
log::info!("Loading encryption keys: {ann:?}");
|
||||||
|
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
let state = app_state();
|
||||||
|
let result = state.load_encryption_keys(&ann).await;
|
||||||
|
|
||||||
|
this.update_in(cx, |this, window, cx| {
|
||||||
|
match result {
|
||||||
|
Ok(_) => {
|
||||||
|
window.push_notification(t!("encryption.reinit"), cx);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
this.request_encryption(ann, window, cx);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_encryption(&self, ann: Announcement, window: &Window, cx: &Context<Self>) {
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
let result = app_state().request_encryption_keys().await;
|
||||||
|
|
||||||
|
this.update_in(cx, |this, window, cx| {
|
||||||
|
match result {
|
||||||
|
Ok(wait_for_approval) => {
|
||||||
|
if wait_for_approval {
|
||||||
|
this.render_pending(ann, window, cx);
|
||||||
|
} else {
|
||||||
|
window.push_notification(t!("encryption.success"), cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// TODO: ask user to confirm re-running if failed
|
||||||
|
window.push_notification(e.to_string(), cx);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn receive_encryption(&self, res: Response, window: &Window, cx: &Context<Self>) {
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
let result = app_state().receive_encryption_keys(res).await;
|
||||||
|
|
||||||
|
this.update_in(cx, |_, window, cx| {
|
||||||
|
match result {
|
||||||
|
Ok(_) => {
|
||||||
|
window.push_notification(t!("encryption.success"), cx);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// TODO: ask user to confirm re-running if failed
|
||||||
|
window.push_notification(e.to_string(), cx);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
fn auth(&mut self, req: AuthRequest, window: &mut Window, cx: &mut Context<Self>) {
|
fn auth(&mut self, req: AuthRequest, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let settings = AppSettings::global(cx);
|
let settings = AppSettings::global(cx);
|
||||||
|
|
||||||
@@ -730,6 +810,132 @@ impl ChatSpace {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_request(&mut self, ann: Announcement, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let client_name = SharedString::from(ann.client().to_string());
|
||||||
|
let target = ann.public_key();
|
||||||
|
|
||||||
|
let note = Notification::new()
|
||||||
|
.custom_id(SharedString::from(ann.id().to_hex()))
|
||||||
|
.autohide(false)
|
||||||
|
.icon(IconName::Info)
|
||||||
|
.title(shared_t!("request_encryption.label"))
|
||||||
|
.content(move |_window, cx| {
|
||||||
|
v_flex()
|
||||||
|
.gap_2()
|
||||||
|
.text_sm()
|
||||||
|
.child(shared_t!("request_encryption.body"))
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.py_1()
|
||||||
|
.px_1p5()
|
||||||
|
.rounded_sm()
|
||||||
|
.text_xs()
|
||||||
|
.bg(cx.theme().warning_background)
|
||||||
|
.text_color(cx.theme().warning_foreground)
|
||||||
|
.child(client_name.clone()),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
})
|
||||||
|
.action(move |_window, _cx| {
|
||||||
|
Button::new("approve")
|
||||||
|
.label(t!("common.approve"))
|
||||||
|
.small()
|
||||||
|
.primary()
|
||||||
|
.loading(false)
|
||||||
|
.disabled(false)
|
||||||
|
.on_click(move |_ev, _window, cx| {
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let state = app_state();
|
||||||
|
state.response_encryption_keys(target).await.ok();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
window.push_notification(note, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_pending(&mut self, ann: Announcement, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let client_name = SharedString::from(ann.client().to_string());
|
||||||
|
let public_key = shorten_pubkey(ann.public_key(), 8);
|
||||||
|
let view = cx.entity().downgrade();
|
||||||
|
|
||||||
|
window.open_modal(cx, move |this, _window, cx| {
|
||||||
|
let view = view.clone();
|
||||||
|
|
||||||
|
this.overlay_closable(false)
|
||||||
|
.show_close(false)
|
||||||
|
.keyboard(false)
|
||||||
|
.confirm()
|
||||||
|
.width(px(460.))
|
||||||
|
.button_props(
|
||||||
|
ModalButtonProps::default()
|
||||||
|
.cancel_text(t!("common.reset"))
|
||||||
|
.ok_text(t!("common.hide")),
|
||||||
|
)
|
||||||
|
.title(shared_t!("pending_encryption.label"))
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_2()
|
||||||
|
.text_sm()
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.justify_center()
|
||||||
|
.items_center()
|
||||||
|
.text_center()
|
||||||
|
.h_16()
|
||||||
|
.w_full()
|
||||||
|
.rounded(cx.theme().radius)
|
||||||
|
.bg(cx.theme().elevated_surface_background)
|
||||||
|
.font_semibold()
|
||||||
|
.child(client_name.clone())
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().text_muted)
|
||||||
|
.child(SharedString::from(&public_key)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(shared_t!("pending_encryption.body_1", c = client_name))
|
||||||
|
.child(shared_t!("pending_encryption.body_2"))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().warning_foreground)
|
||||||
|
.child(shared_t!("pending_encryption.body_3")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.on_cancel(move |_ev, window, cx| {
|
||||||
|
_ = view.update(cx, |this, cx| {
|
||||||
|
this.render_reset(window, cx);
|
||||||
|
});
|
||||||
|
// false to keep modal open
|
||||||
|
false
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_reset(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
let state = app_state();
|
||||||
|
let result = state.init_encryption_keys().await;
|
||||||
|
|
||||||
|
this.update_in(cx, |_, window, cx| {
|
||||||
|
match result {
|
||||||
|
Ok(_) => {
|
||||||
|
window.push_notification(t!("encryption.success"), cx);
|
||||||
|
window.close_all_modals(cx);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
window.push_notification(e.to_string(), cx);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
fn render_setup_gossip_relays_modal(&mut self, window: &mut Window, cx: &mut App) {
|
fn render_setup_gossip_relays_modal(&mut self, window: &mut Window, cx: &mut App) {
|
||||||
let relays = default_nip65_relays();
|
let relays = default_nip65_relays();
|
||||||
|
|
||||||
@@ -937,15 +1143,15 @@ impl ChatSpace {
|
|||||||
_window: &mut Window,
|
_window: &mut Window,
|
||||||
cx: &Context<Self>,
|
cx: &Context<Self>,
|
||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
let registry = Registry::read_global(cx);
|
let registry = Registry::global(cx);
|
||||||
let status = registry.unwrapping_status.read(cx);
|
let status = registry.read(cx).loading;
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.h_6()
|
.h_6()
|
||||||
.w_full()
|
.w_full()
|
||||||
.child(compose_button())
|
.child(compose_button())
|
||||||
.when(status != &UnwrappingStatus::Complete, |this| {
|
.when(status, |this| {
|
||||||
this.child(deferred(
|
this.child(deferred(
|
||||||
h_flex()
|
h_flex()
|
||||||
.px_2()
|
.px_2()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use gpui::{
|
|||||||
WindowOptions,
|
WindowOptions,
|
||||||
};
|
};
|
||||||
use states::app_state;
|
use states::app_state;
|
||||||
use states::constants::{APP_ID, APP_NAME};
|
use states::constants::{APP_ID, CLIENT_NAME};
|
||||||
use ui::Root;
|
use ui::Root;
|
||||||
|
|
||||||
use crate::actions::{load_embedded_fonts, quit, Quit};
|
use crate::actions::{load_embedded_fonts, quit, Quit};
|
||||||
@@ -63,7 +63,7 @@ fn main() {
|
|||||||
kind: WindowKind::Normal,
|
kind: WindowKind::Normal,
|
||||||
app_id: Some(APP_ID.to_owned()),
|
app_id: Some(APP_ID.to_owned()),
|
||||||
titlebar: Some(TitlebarOptions {
|
titlebar: Some(TitlebarOptions {
|
||||||
title: Some(SharedString::new_static(APP_NAME)),
|
title: Some(SharedString::new_static(CLIENT_NAME)),
|
||||||
traffic_light_position: Some(point(px(9.0), px(9.0))),
|
traffic_light_position: Some(point(px(9.0), px(9.0))),
|
||||||
appears_transparent: true,
|
appears_transparent: true,
|
||||||
}),
|
}),
|
||||||
@@ -86,6 +86,9 @@ fn main() {
|
|||||||
// Initialize app registry
|
// Initialize app registry
|
||||||
registry::init(cx);
|
registry::init(cx);
|
||||||
|
|
||||||
|
// Initialize backend for credentials storage
|
||||||
|
key_store::init(cx);
|
||||||
|
|
||||||
// Initialize settings
|
// Initialize settings
|
||||||
settings::init(cx);
|
settings::init(cx);
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ use gpui::{
|
|||||||
Window,
|
Window,
|
||||||
};
|
};
|
||||||
use i18n::{shared_t, t};
|
use i18n::{shared_t, t};
|
||||||
|
use key_store::backend::KeyItem;
|
||||||
|
use key_store::KeyStore;
|
||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
use registry::keystore::KeyItem;
|
|
||||||
use registry::Registry;
|
use registry::Registry;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use states::app_state;
|
use states::app_state;
|
||||||
@@ -116,7 +117,7 @@ impl Account {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let keystore = Registry::global(cx).read(cx).keystore();
|
let keystore = KeyStore::global(cx).read(cx).backend();
|
||||||
|
|
||||||
// Handle connection in the background
|
// Handle connection in the background
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::HashSet;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use common::display::{RenderedProfile, RenderedTimestamp};
|
use common::display::{RenderedProfile, RenderedTimestamp};
|
||||||
@@ -17,7 +17,7 @@ use indexset::{BTreeMap, BTreeSet};
|
|||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use registry::message::{Message, RenderedMessage};
|
use registry::message::{Message, RenderedMessage};
|
||||||
use registry::room::{Room, RoomKind, RoomSignal, SendReport};
|
use registry::room::{Room, RoomKind, RoomSignal, SendOptions, SendReport, SignerKind};
|
||||||
use registry::Registry;
|
use registry::Registry;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
@@ -47,6 +47,10 @@ mod subject;
|
|||||||
#[action(namespace = chat, no_json)]
|
#[action(namespace = chat, no_json)]
|
||||||
pub struct SeenOn(pub EventId);
|
pub struct SeenOn(pub EventId);
|
||||||
|
|
||||||
|
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
||||||
|
#[action(namespace = chat, no_json)]
|
||||||
|
pub struct SetSigner(pub SignerKind);
|
||||||
|
|
||||||
pub fn init(room: Entity<Room>, window: &mut Window, cx: &mut App) -> Entity<Chat> {
|
pub fn init(room: Entity<Room>, window: &mut Window, cx: &mut App) -> Entity<Chat> {
|
||||||
cx.new(|cx| Chat::new(room, window, cx))
|
cx.new(|cx| Chat::new(room, window, cx))
|
||||||
}
|
}
|
||||||
@@ -54,7 +58,6 @@ pub fn init(room: Entity<Room>, window: &mut Window, cx: &mut App) -> Entity<Cha
|
|||||||
pub struct Chat {
|
pub struct Chat {
|
||||||
// Chat Room
|
// Chat Room
|
||||||
room: Entity<Room>,
|
room: Entity<Room>,
|
||||||
relays: Entity<HashMap<PublicKey, Vec<RelayUrl>>>,
|
|
||||||
|
|
||||||
// Messages
|
// Messages
|
||||||
list_state: ListState,
|
list_state: ListState,
|
||||||
@@ -64,6 +67,7 @@ pub struct Chat {
|
|||||||
|
|
||||||
// New Message
|
// New Message
|
||||||
input: Entity<InputState>,
|
input: Entity<InputState>,
|
||||||
|
options: Entity<SendOptions>,
|
||||||
replies_to: Entity<HashSet<EventId>>,
|
replies_to: Entity<HashSet<EventId>>,
|
||||||
|
|
||||||
// Media Attachment
|
// Media Attachment
|
||||||
@@ -75,20 +79,12 @@ pub struct Chat {
|
|||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
image_cache: Entity<RetainAllImageCache>,
|
image_cache: Entity<RetainAllImageCache>,
|
||||||
|
|
||||||
_subscriptions: SmallVec<[Subscription; 4]>,
|
_subscriptions: SmallVec<[Subscription; 3]>,
|
||||||
_tasks: SmallVec<[Task<()>; 2]>,
|
_tasks: SmallVec<[Task<()>; 2]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Chat {
|
impl Chat {
|
||||||
pub fn new(room: Entity<Room>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
pub fn new(room: Entity<Room>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let attachments = cx.new(|_| vec![]);
|
|
||||||
let replies_to = cx.new(|_| HashSet::new());
|
|
||||||
|
|
||||||
let relays = cx.new(|_| {
|
|
||||||
let this: HashMap<PublicKey, Vec<RelayUrl>> = HashMap::new();
|
|
||||||
this
|
|
||||||
});
|
|
||||||
|
|
||||||
let input = cx.new(|cx| {
|
let input = cx.new(|cx| {
|
||||||
InputState::new(window, cx)
|
InputState::new(window, cx)
|
||||||
.placeholder(t!("chat.placeholder"))
|
.placeholder(t!("chat.placeholder"))
|
||||||
@@ -97,11 +93,16 @@ impl Chat {
|
|||||||
.clean_on_escape()
|
.clean_on_escape()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let attachments = cx.new(|_| vec![]);
|
||||||
|
let replies_to = cx.new(|_| HashSet::new());
|
||||||
|
let options = cx.new(|_| SendOptions::default());
|
||||||
|
|
||||||
|
let id = room.read(cx).id.to_string().into();
|
||||||
let messages = BTreeSet::from([Message::system()]);
|
let messages = BTreeSet::from([Message::system()]);
|
||||||
let list_state = ListState::new(messages.len(), ListAlignment::Bottom, px(1024.));
|
let list_state = ListState::new(messages.len(), ListAlignment::Bottom, px(1024.));
|
||||||
|
|
||||||
let connect = room.read(cx).connect(cx);
|
let connect = room.read(cx).connect(cx);
|
||||||
let load_messages = room.read(cx).load_messages(cx);
|
let get_messages = room.read(cx).get_messages(cx);
|
||||||
|
|
||||||
let mut subscriptions = smallvec![];
|
let mut subscriptions = smallvec![];
|
||||||
let mut tasks = smallvec![];
|
let mut tasks = smallvec![];
|
||||||
@@ -109,7 +110,7 @@ impl Chat {
|
|||||||
tasks.push(
|
tasks.push(
|
||||||
// Load all messages belonging to this room
|
// Load all messages belonging to this room
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
let result = load_messages.await;
|
let result = get_messages.await;
|
||||||
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
this.update_in(cx, |this, window, cx| {
|
||||||
match result {
|
match result {
|
||||||
@@ -126,24 +127,11 @@ impl Chat {
|
|||||||
);
|
);
|
||||||
|
|
||||||
tasks.push(
|
tasks.push(
|
||||||
// Get messaging relays for all members
|
// Get messaging relays and encryption keys announcement for all members
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.background_spawn(async move {
|
||||||
let result = connect.await;
|
if let Err(e) = connect.await {
|
||||||
|
log::error!("Failed to initialize room: {e}");
|
||||||
this.update_in(cx, |this, _window, cx| {
|
|
||||||
match result {
|
|
||||||
Ok(relays) => {
|
|
||||||
this.relays.update(cx, |this, cx| {
|
|
||||||
this.extend(relays);
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
|
||||||
this.insert_warning(e.to_string(), cx);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -189,23 +177,6 @@ impl Chat {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
subscriptions.push(
|
|
||||||
// Observe the messaging relays of the room's members
|
|
||||||
cx.observe_in(&relays, window, |this, entity, _window, cx| {
|
|
||||||
let registry = Registry::global(cx);
|
|
||||||
let relays = entity.read(cx).clone();
|
|
||||||
|
|
||||||
for (public_key, urls) in relays.iter() {
|
|
||||||
if urls.is_empty() {
|
|
||||||
let profile = registry.read(cx).get_person(public_key, cx);
|
|
||||||
let content = t!("chat.nip17_not_found", u = profile.name());
|
|
||||||
|
|
||||||
this.insert_warning(content, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
// Observe when user close chat panel
|
// Observe when user close chat panel
|
||||||
cx.on_release_in(window, move |this, window, cx| {
|
cx.on_release_in(window, move |this, window, cx| {
|
||||||
@@ -219,19 +190,19 @@ impl Chat {
|
|||||||
);
|
);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
id: room.read(cx).id.to_string().into(),
|
id,
|
||||||
image_cache: RetainAllImageCache::new(cx),
|
|
||||||
focus_handle: cx.focus_handle(),
|
|
||||||
rendered_texts_by_id: BTreeMap::new(),
|
|
||||||
reports_by_id: BTreeMap::new(),
|
|
||||||
relays,
|
|
||||||
messages,
|
messages,
|
||||||
room,
|
room,
|
||||||
list_state,
|
list_state,
|
||||||
input,
|
input,
|
||||||
replies_to,
|
replies_to,
|
||||||
attachments,
|
attachments,
|
||||||
|
options,
|
||||||
|
rendered_texts_by_id: BTreeMap::new(),
|
||||||
|
reports_by_id: BTreeMap::new(),
|
||||||
uploading: false,
|
uploading: false,
|
||||||
|
image_cache: RetainAllImageCache::new(cx),
|
||||||
|
focus_handle: cx.focus_handle(),
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
_tasks: tasks,
|
_tasks: tasks,
|
||||||
}
|
}
|
||||||
@@ -239,12 +210,12 @@ impl Chat {
|
|||||||
|
|
||||||
/// Load all messages belonging to this room
|
/// Load all messages belonging to this room
|
||||||
fn load_messages(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn load_messages(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let load_messages = self.room.read(cx).load_messages(cx);
|
let get_messages = self.room.read(cx).get_messages(cx);
|
||||||
|
|
||||||
self._tasks.push(
|
self._tasks.push(
|
||||||
// Run the task in the background
|
// Run the task in the background
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
let result = load_messages.await;
|
let result = get_messages.await;
|
||||||
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
this.update_in(cx, |this, window, cx| {
|
||||||
match result {
|
match result {
|
||||||
@@ -303,9 +274,6 @@ impl Chat {
|
|||||||
this.set_value("", window, cx);
|
this.set_value("", window, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get the backup setting
|
|
||||||
let backup = AppSettings::get_backup_messages(cx);
|
|
||||||
|
|
||||||
// Get replies_to if it's present
|
// Get replies_to if it's present
|
||||||
let replies: Vec<EventId> = self.replies_to.read(cx).iter().copied().collect();
|
let replies: Vec<EventId> = self.replies_to.read(cx).iter().copied().collect();
|
||||||
|
|
||||||
@@ -317,14 +285,17 @@ impl Chat {
|
|||||||
let rumor_id = rumor.id.unwrap();
|
let rumor_id = rumor.id.unwrap();
|
||||||
|
|
||||||
// Create a task for sending the message in the background
|
// Create a task for sending the message in the background
|
||||||
let send_message = room.send_message(rumor.clone(), backup, cx);
|
let opts = self.options.read(cx);
|
||||||
|
let send_message = room.send_message(&rumor, opts, cx);
|
||||||
|
|
||||||
// Optimistically update message list
|
// Optimistically update message list
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
cx.background_executor()
|
let delay = Duration::from_millis(100);
|
||||||
.timer(Duration::from_millis(100))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
|
// Wait for the delay
|
||||||
|
cx.background_executor().timer(delay).await;
|
||||||
|
|
||||||
|
// Update the message list and reset the states
|
||||||
this.update_in(cx, |this, window, cx| {
|
this.update_in(cx, |this, window, cx| {
|
||||||
this.insert_message(Message::user(rumor), true, cx);
|
this.insert_message(Message::user(rumor), true, cx);
|
||||||
this.remove_all_replies(cx);
|
this.remove_all_replies(cx);
|
||||||
@@ -339,6 +310,7 @@ impl Chat {
|
|||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
|
self._tasks.push(
|
||||||
// Continue sending the message in the background
|
// Continue sending the message in the background
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
let result = send_message.await;
|
let result = send_message.await;
|
||||||
@@ -346,10 +318,11 @@ impl Chat {
|
|||||||
this.update_in(cx, |this, window, cx| {
|
this.update_in(cx, |this, window, cx| {
|
||||||
match result {
|
match result {
|
||||||
Ok(reports) => {
|
Ok(reports) => {
|
||||||
|
// Update room's status
|
||||||
this.room.update(cx, |this, cx| {
|
this.room.update(cx, |this, cx| {
|
||||||
if this.kind != RoomKind::Ongoing {
|
if this.kind != RoomKind::Ongoing {
|
||||||
// Update the room kind to ongoing
|
// Update the room kind to ongoing,
|
||||||
// But keep the room kind if send failed
|
// but keep the room kind if send failed
|
||||||
if reports.iter().all(|r| !r.is_sent_success()) {
|
if reports.iter().all(|r| !r.is_sent_success()) {
|
||||||
this.kind = RoomKind::Ongoing;
|
this.kind = RoomKind::Ongoing;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -368,8 +341,8 @@ impl Chat {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
})
|
}),
|
||||||
.detach();
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resend a failed message
|
/// Resend a failed message
|
||||||
@@ -432,6 +405,7 @@ impl Chat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Insert a warning message into the chat panel
|
/// Insert a warning message into the chat panel
|
||||||
|
#[allow(dead_code)]
|
||||||
fn insert_warning(&mut self, content: impl Into<String>, cx: &mut Context<Self>) {
|
fn insert_warning(&mut self, content: impl Into<String>, cx: &mut Context<Self>) {
|
||||||
let m = Message::warning(content.into());
|
let m = Message::warning(content.into());
|
||||||
self.insert_message(m, true, cx);
|
self.insert_message(m, true, cx);
|
||||||
@@ -473,6 +447,10 @@ impl Chat {
|
|||||||
registry.get_person(public_key, cx)
|
registry.get_person(public_key, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn signer_kind(&self, cx: &App) -> SignerKind {
|
||||||
|
self.options.read(cx).signer_kind
|
||||||
|
}
|
||||||
|
|
||||||
fn scroll_to(&self, id: EventId) {
|
fn scroll_to(&self, id: EventId) {
|
||||||
if let Some(ix) = self.messages.iter().position(|m| {
|
if let Some(ix) = self.messages.iter().position(|m| {
|
||||||
if let Message::User(msg) = m {
|
if let Message::User(msg) = m {
|
||||||
@@ -543,30 +521,25 @@ impl Chat {
|
|||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
match Flatten::flatten(task.await.map_err(|e| e.into())) {
|
let result = Flatten::flatten(task.await.map_err(|e| e.into()));
|
||||||
|
|
||||||
|
this.update_in(cx, |this, window, cx| {
|
||||||
|
match result {
|
||||||
Ok(Some(url)) => {
|
Ok(Some(url)) => {
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.add_attachment(url, cx);
|
this.add_attachment(url, cx);
|
||||||
this.set_uploading(false, cx);
|
this.set_uploading(false, cx);
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
}
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
this.update_in(cx, |this, window, cx| {
|
|
||||||
window.push_notification("Failed to upload file", cx);
|
|
||||||
this.set_uploading(false, cx);
|
this.set_uploading(false, cx);
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
this.update_in(cx, |this, window, cx| {
|
|
||||||
window.push_notification(Notification::error(e.to_string()), cx);
|
window.push_notification(Notification::error(e.to_string()), cx);
|
||||||
this.set_uploading(false, cx);
|
this.set_uploading(false, cx);
|
||||||
|
}
|
||||||
|
};
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(())
|
Some(())
|
||||||
})
|
})
|
||||||
@@ -911,6 +884,27 @@ impl Chat {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
.when(report.device_not_found, |this| {
|
||||||
|
this.child(
|
||||||
|
h_flex()
|
||||||
|
.flex_wrap()
|
||||||
|
.justify_center()
|
||||||
|
.p_2()
|
||||||
|
.h_20()
|
||||||
|
.w_full()
|
||||||
|
.text_sm()
|
||||||
|
.rounded(cx.theme().radius)
|
||||||
|
.bg(cx.theme().danger_background)
|
||||||
|
.text_color(cx.theme().danger_foreground)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex_1()
|
||||||
|
.w_full()
|
||||||
|
.text_center()
|
||||||
|
.child(shared_t!("chat.device_not_found", u = name)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
.when_some(report.error.clone(), |this, error| {
|
.when_some(report.error.clone(), |this, error| {
|
||||||
this.child(
|
this.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
@@ -1291,6 +1285,13 @@ impl Chat {
|
|||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn on_set_encryption(&mut self, ev: &SetSigner, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.options.update(cx, move |this, cx| {
|
||||||
|
this.signer_kind = ev.0;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Panel for Chat {
|
impl Panel for Chat {
|
||||||
@@ -1334,8 +1335,11 @@ impl Focusable for Chat {
|
|||||||
|
|
||||||
impl Render for Chat {
|
impl Render for Chat {
|
||||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let kind = self.signer_kind(cx);
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.on_action(cx.listener(Self::on_open_seen_on))
|
.on_action(cx.listener(Self::on_open_seen_on))
|
||||||
|
.on_action(cx.listener(Self::on_set_encryption))
|
||||||
.image_cache(self.image_cache.clone())
|
.image_cache(self.image_cache.clone())
|
||||||
.size_full()
|
.size_full()
|
||||||
.child(
|
.child(
|
||||||
@@ -1384,9 +1388,7 @@ impl Render for Chat {
|
|||||||
.items_end()
|
.items_end()
|
||||||
.gap_2p5()
|
.gap_2p5()
|
||||||
.child(
|
.child(
|
||||||
div()
|
h_flex()
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child(
|
.child(
|
||||||
@@ -1408,7 +1410,31 @@ impl Render for Chat {
|
|||||||
.large(),
|
.large(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(TextInput::new(&self.input)),
|
.child(TextInput::new(&self.input))
|
||||||
|
.child(
|
||||||
|
Button::new("options")
|
||||||
|
.icon(IconName::Settings)
|
||||||
|
.ghost()
|
||||||
|
.large()
|
||||||
|
.popup_menu(move |this, _window, _cx| {
|
||||||
|
this.title("Encrypt by:")
|
||||||
|
.menu_with_check(
|
||||||
|
"Encryption Key",
|
||||||
|
matches!(kind, SignerKind::Encryption),
|
||||||
|
Box::new(SetSigner(SignerKind::Encryption)),
|
||||||
|
)
|
||||||
|
.menu_with_check(
|
||||||
|
"User's Identity",
|
||||||
|
matches!(kind, SignerKind::User),
|
||||||
|
Box::new(SetSigner(SignerKind::User)),
|
||||||
|
)
|
||||||
|
.menu_with_check(
|
||||||
|
"Auto",
|
||||||
|
matches!(kind, SignerKind::Auto),
|
||||||
|
Box::new(SetSigner(SignerKind::Auto)),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ use gpui::{
|
|||||||
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Window,
|
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Window,
|
||||||
};
|
};
|
||||||
use i18n::{shared_t, t};
|
use i18n::{shared_t, t};
|
||||||
|
use key_store::backend::KeyItem;
|
||||||
|
use key_store::KeyStore;
|
||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
use registry::keystore::KeyItem;
|
|
||||||
use registry::Registry;
|
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use states::app_state;
|
use states::app_state;
|
||||||
use states::constants::BUNKER_TIMEOUT;
|
use states::constants::BUNKER_TIMEOUT;
|
||||||
@@ -174,7 +174,7 @@ impl Login {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let keystore = Registry::global(cx).read(cx).keystore();
|
let keystore = KeyStore::global(cx).read(cx).backend();
|
||||||
let username = keys.public_key().to_hex();
|
let username = keys.public_key().to_hex();
|
||||||
let secret = keys.secret_key().to_secret_bytes();
|
let secret = keys.secret_key().to_secret_bytes();
|
||||||
let mut clean_uri = uri.to_string();
|
let mut clean_uri = uri.to_string();
|
||||||
@@ -263,7 +263,7 @@ impl Login {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn login_with_keys(&mut self, keys: Keys, cx: &mut Context<Self>) {
|
pub fn login_with_keys(&mut self, keys: Keys, cx: &mut Context<Self>) {
|
||||||
let keystore = Registry::global(cx).read(cx).keystore();
|
let keystore = KeyStore::global(cx).read(cx).backend();
|
||||||
let username = keys.public_key().to_hex();
|
let username = keys.public_key().to_hex();
|
||||||
let secret = keys.secret_key().to_secret_hex().into_bytes();
|
let secret = keys.secret_key().to_secret_hex().into_bytes();
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
use gpui_tokio::Tokio;
|
use gpui_tokio::Tokio;
|
||||||
use i18n::{shared_t, t};
|
use i18n::{shared_t, t};
|
||||||
|
use key_store::backend::KeyItem;
|
||||||
|
use key_store::KeyStore;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use registry::keystore::KeyItem;
|
|
||||||
use registry::Registry;
|
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use smol::fs;
|
use smol::fs;
|
||||||
use states::constants::BOOTSTRAP_RELAYS;
|
use states::constants::BOOTSTRAP_RELAYS;
|
||||||
@@ -106,7 +106,7 @@ impl NewAccount {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_signer(&mut self, cx: &mut Context<Self>) {
|
pub fn set_signer(&mut self, cx: &mut Context<Self>) {
|
||||||
let keystore = Registry::global(cx).read(cx).keystore();
|
let keystore = KeyStore::global(cx).read(cx).backend();
|
||||||
|
|
||||||
let keys = self.temp_keys.read(cx).clone();
|
let keys = self.temp_keys.read(cx).clone();
|
||||||
let username = keys.public_key().to_hex();
|
let username = keys.public_key().to_hex();
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ use gpui::{
|
|||||||
SharedString, StatefulInteractiveElement, Styled, Task, Window,
|
SharedString, StatefulInteractiveElement, Styled, Task, Window,
|
||||||
};
|
};
|
||||||
use i18n::{shared_t, t};
|
use i18n::{shared_t, t};
|
||||||
|
use key_store::backend::KeyItem;
|
||||||
|
use key_store::KeyStore;
|
||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
use registry::keystore::KeyItem;
|
|
||||||
use registry::Registry;
|
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use states::app_state;
|
use states::app_state;
|
||||||
use states::constants::{APP_NAME, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT};
|
use states::constants::{CLIENT_NAME, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||||
@@ -81,7 +81,7 @@ impl Onboarding {
|
|||||||
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
|
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
|
||||||
|
|
||||||
let relay = RelayUrl::parse(NOSTR_CONNECT_RELAY).unwrap();
|
let relay = RelayUrl::parse(NOSTR_CONNECT_RELAY).unwrap();
|
||||||
let uri = NostrConnectURI::client(app_keys.public_key(), vec![relay], APP_NAME);
|
let uri = NostrConnectURI::client(app_keys.public_key(), vec![relay], CLIENT_NAME);
|
||||||
let qr_code = uri.to_string().to_qr();
|
let qr_code = uri.to_string().to_qr();
|
||||||
|
|
||||||
// NIP46: https://github.com/nostr-protocol/nips/blob/master/46.md
|
// NIP46: https://github.com/nostr-protocol/nips/blob/master/46.md
|
||||||
@@ -126,7 +126,7 @@ impl Onboarding {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let keystore = Registry::global(cx).read(cx).keystore();
|
let keystore = KeyStore::global(cx).read(cx).backend();
|
||||||
let username = self.app_keys.public_key().to_hex();
|
let username = self.app_keys.public_key().to_hex();
|
||||||
let secret = self.app_keys.secret_key().to_secret_bytes();
|
let secret = self.app_keys.secret_key().to_secret_bytes();
|
||||||
let mut clean_uri = uri.to_string();
|
let mut clean_uri = uri.to_string();
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ impl RoomListItem {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn public_key(mut self, public_key: PublicKey) -> Self {
|
pub fn public_key(mut self, public_key: &PublicKey) -> Self {
|
||||||
self.public_key = Some(public_key);
|
self.public_key = Some(public_key.to_owned());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ use settings::AppSettings;
|
|||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use states::app_state;
|
use states::app_state;
|
||||||
use states::constants::{BOOTSTRAP_RELAYS, SEARCH_RELAYS};
|
use states::constants::{BOOTSTRAP_RELAYS, SEARCH_RELAYS};
|
||||||
use states::state::UnwrappingStatus;
|
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||||
@@ -627,7 +626,7 @@ impl Sidebar {
|
|||||||
.name(this.display_name(cx))
|
.name(this.display_name(cx))
|
||||||
.avatar(this.display_image(proxy, cx))
|
.avatar(this.display_image(proxy, cx))
|
||||||
.created_at(this.created_at.to_ago())
|
.created_at(this.created_at.to_ago())
|
||||||
.public_key(this.members[0])
|
.public_key(this.members.iter().nth(0).unwrap().0)
|
||||||
.kind(this.kind)
|
.kind(this.kind)
|
||||||
.on_click(handler),
|
.on_click(handler),
|
||||||
)
|
)
|
||||||
@@ -669,7 +668,7 @@ impl Focusable for Sidebar {
|
|||||||
impl Render for Sidebar {
|
impl Render for Sidebar {
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let registry = Registry::read_global(cx);
|
let registry = Registry::read_global(cx);
|
||||||
let loading = registry.unwrapping_status.read(cx) != &UnwrappingStatus::Complete;
|
let loading = registry.loading;
|
||||||
|
|
||||||
// Get rooms from either search results or the chat registry
|
// Get rooms from either search results or the chat registry
|
||||||
let rooms = if let Some(results) = self.local_result.read(cx).as_ref() {
|
let rooms = if let Some(results) = self.local_result.read(cx).as_ref() {
|
||||||
|
|||||||
28
crates/key_store/Cargo.toml
Normal file
28
crates/key_store/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
[package]
|
||||||
|
name = "key_store"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
common = { path = "../common" }
|
||||||
|
states = { path = "../states" }
|
||||||
|
ui = { path = "../ui" }
|
||||||
|
theme = { path = "../theme" }
|
||||||
|
settings = { path = "../settings" }
|
||||||
|
|
||||||
|
rust-i18n.workspace = true
|
||||||
|
i18n.workspace = true
|
||||||
|
gpui.workspace = true
|
||||||
|
|
||||||
|
nostr.workspace = true
|
||||||
|
nostr-sdk.workspace = true
|
||||||
|
|
||||||
|
anyhow.workspace = true
|
||||||
|
itertools.workspace = true
|
||||||
|
smallvec.workspace = true
|
||||||
|
smol.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
@@ -14,8 +14,6 @@ use states::paths::config_dir;
|
|||||||
pub enum KeyItem {
|
pub enum KeyItem {
|
||||||
User,
|
User,
|
||||||
Bunker,
|
Bunker,
|
||||||
Client,
|
|
||||||
Encryption,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for KeyItem {
|
impl Display for KeyItem {
|
||||||
@@ -23,8 +21,6 @@ impl Display for KeyItem {
|
|||||||
match self {
|
match self {
|
||||||
Self::User => write!(f, "coop-user"),
|
Self::User => write!(f, "coop-user"),
|
||||||
Self::Bunker => write!(f, "coop-bunker"),
|
Self::Bunker => write!(f, "coop-bunker"),
|
||||||
Self::Client => write!(f, "coop-client"),
|
|
||||||
Self::Encryption => write!(f, "coop-encryption"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,7 +31,7 @@ impl From<KeyItem> for String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait KeyStore: Any + Send + Sync {
|
pub trait KeyBackend: Any + Send + Sync {
|
||||||
fn name(&self) -> &str;
|
fn name(&self) -> &str;
|
||||||
|
|
||||||
/// Reads the credentials from the provider.
|
/// Reads the credentials from the provider.
|
||||||
@@ -66,7 +62,7 @@ pub trait KeyStore: Any + Send + Sync {
|
|||||||
/// A credentials provider that stores credentials in the system keychain.
|
/// A credentials provider that stores credentials in the system keychain.
|
||||||
pub struct KeyringProvider;
|
pub struct KeyringProvider;
|
||||||
|
|
||||||
impl KeyStore for KeyringProvider {
|
impl KeyBackend for KeyringProvider {
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
"keyring"
|
"keyring"
|
||||||
}
|
}
|
||||||
@@ -139,7 +135,7 @@ impl Default for FileProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KeyStore for FileProvider {
|
impl KeyBackend for FileProvider {
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
"file"
|
"file"
|
||||||
}
|
}
|
||||||
96
crates/key_store/src/lib.rs
Normal file
96
crates/key_store/src/lib.rs
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
use std::sync::{Arc, LazyLock};
|
||||||
|
|
||||||
|
use gpui::{App, AppContext, Context, Entity, Global, Task};
|
||||||
|
use smallvec::{smallvec, SmallVec};
|
||||||
|
|
||||||
|
use crate::backend::{FileProvider, KeyBackend, KeyringProvider};
|
||||||
|
|
||||||
|
pub mod backend;
|
||||||
|
|
||||||
|
static DISABLE_KEYRING: LazyLock<bool> =
|
||||||
|
LazyLock::new(|| std::env::var("DISABLE_KEYRING").is_ok_and(|value| !value.is_empty()));
|
||||||
|
|
||||||
|
pub fn init(cx: &mut App) {
|
||||||
|
KeyStore::set_global(cx.new(KeyStore::new), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GlobalKeyStore(Entity<KeyStore>);
|
||||||
|
|
||||||
|
impl Global for GlobalKeyStore {}
|
||||||
|
|
||||||
|
pub struct KeyStore {
|
||||||
|
/// Key Store for storing credentials
|
||||||
|
pub backend: Arc<dyn KeyBackend>,
|
||||||
|
|
||||||
|
/// Whether the keystore has been initialized
|
||||||
|
pub initialized: bool,
|
||||||
|
|
||||||
|
/// Tasks for asynchronous operations
|
||||||
|
_tasks: SmallVec<[Task<()>; 1]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyStore {
|
||||||
|
/// Retrieve the global keys state
|
||||||
|
pub fn global(cx: &App) -> Entity<Self> {
|
||||||
|
cx.global::<GlobalKeyStore>().0.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the global keys instance
|
||||||
|
pub(crate) fn set_global(state: Entity<Self>, cx: &mut App) {
|
||||||
|
cx.set_global(GlobalKeyStore(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new keys instance
|
||||||
|
pub(crate) fn new(cx: &mut Context<Self>) -> Self {
|
||||||
|
// Use the file system for keystore in development or when the user specifies it
|
||||||
|
let use_file_keystore = cfg!(debug_assertions) || *DISABLE_KEYRING;
|
||||||
|
|
||||||
|
// Construct the key backend
|
||||||
|
let backend: Arc<dyn KeyBackend> = if use_file_keystore {
|
||||||
|
Arc::new(FileProvider::default())
|
||||||
|
} else {
|
||||||
|
Arc::new(KeyringProvider)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only used for testing keyring availability on the user's system
|
||||||
|
let read_credential = cx.read_credentials("Coop");
|
||||||
|
|
||||||
|
let mut tasks = smallvec![];
|
||||||
|
|
||||||
|
tasks.push(
|
||||||
|
// Verify the keyring availability
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
let result = read_credential.await;
|
||||||
|
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
if let Err(e) = result {
|
||||||
|
log::error!("Keyring error: {e}");
|
||||||
|
// For Linux:
|
||||||
|
// The user has not installed secret service on their system
|
||||||
|
// Fall back to the file provider
|
||||||
|
this.backend = Arc::new(FileProvider::default());
|
||||||
|
}
|
||||||
|
this.initialized = true;
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
backend,
|
||||||
|
initialized: false,
|
||||||
|
_tasks: tasks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the key backend.
|
||||||
|
pub fn backend(&self) -> Arc<dyn KeyBackend> {
|
||||||
|
Arc::clone(&self.backend)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the keystore is a file key backend.
|
||||||
|
pub fn is_using_file_keystore(&self) -> bool {
|
||||||
|
self.backend.name() == "file"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,16 +12,13 @@ settings = { path = "../settings" }
|
|||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
nostr.workspace = true
|
nostr.workspace = true
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
nostr-lmdb.workspace = true
|
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
flume.workspace = true
|
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
|
||||||
fuzzy-matcher = "0.3.7"
|
fuzzy-matcher = "0.3.7"
|
||||||
rustls = "0.23.23"
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use std::cmp::Reverse;
|
use std::cmp::Reverse;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::sync::{Arc, LazyLock};
|
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use common::event::EventUtils;
|
use common::event::EventUtils;
|
||||||
@@ -14,19 +13,12 @@ use room::RoomKind;
|
|||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use states::app_state;
|
use states::app_state;
|
||||||
use states::constants::KEYRING_URL;
|
|
||||||
use states::state::UnwrappingStatus;
|
|
||||||
|
|
||||||
use crate::keystore::{FileProvider, KeyStore, KeyringProvider};
|
|
||||||
use crate::room::Room;
|
use crate::room::Room;
|
||||||
|
|
||||||
pub mod keystore;
|
|
||||||
pub mod message;
|
pub mod message;
|
||||||
pub mod room;
|
pub mod room;
|
||||||
|
|
||||||
pub static DISABLE_KEYRING: LazyLock<bool> =
|
|
||||||
LazyLock::new(|| std::env::var("DISABLE_KEYRING").is_ok_and(|value| !value.is_empty()));
|
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
Registry::set_global(cx.new(Registry::new), cx);
|
Registry::set_global(cx.new(Registry::new), cx);
|
||||||
}
|
}
|
||||||
@@ -49,14 +41,8 @@ pub struct Registry {
|
|||||||
/// Collection of all persons (user profiles)
|
/// Collection of all persons (user profiles)
|
||||||
pub persons: HashMap<PublicKey, Entity<Profile>>,
|
pub persons: HashMap<PublicKey, Entity<Profile>>,
|
||||||
|
|
||||||
/// Status of the unwrapping process
|
/// Loading status of the registry
|
||||||
pub unwrapping_status: Entity<UnwrappingStatus>,
|
pub loading: bool,
|
||||||
|
|
||||||
/// Key Store for storing credentials
|
|
||||||
pub keystore: Arc<dyn KeyStore>,
|
|
||||||
|
|
||||||
/// Whether the keystore has been initialized
|
|
||||||
pub initialized_keystore: bool,
|
|
||||||
|
|
||||||
/// Public Key of the currently activated signer
|
/// Public Key of the currently activated signer
|
||||||
signer_pubkey: Option<PublicKey>,
|
signer_pubkey: Option<PublicKey>,
|
||||||
@@ -85,39 +71,8 @@ impl Registry {
|
|||||||
|
|
||||||
/// Create a new registry instance
|
/// Create a new registry instance
|
||||||
pub(crate) fn new(cx: &mut Context<Self>) -> Self {
|
pub(crate) fn new(cx: &mut Context<Self>) -> Self {
|
||||||
let unwrapping_status = cx.new(|_| UnwrappingStatus::default());
|
|
||||||
let read_credential = cx.read_credentials(KEYRING_URL);
|
|
||||||
let initialized_keystore = cfg!(debug_assertions) || *DISABLE_KEYRING;
|
|
||||||
let keystore: Arc<dyn KeyStore> = if cfg!(debug_assertions) || *DISABLE_KEYRING {
|
|
||||||
Arc::new(FileProvider::default())
|
|
||||||
} else {
|
|
||||||
Arc::new(KeyringProvider)
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut tasks = smallvec![];
|
let mut tasks = smallvec![];
|
||||||
|
|
||||||
if !(cfg!(debug_assertions) || *DISABLE_KEYRING) {
|
|
||||||
tasks.push(
|
|
||||||
// Verify the keyring access
|
|
||||||
cx.spawn(async move |this, cx| {
|
|
||||||
let result = read_credential.await;
|
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
if let Err(e) = result {
|
|
||||||
log::error!("Keyring error: {e}");
|
|
||||||
// For Linux:
|
|
||||||
// The user has not installed secret service on their system
|
|
||||||
// Fall back to the file provider
|
|
||||||
this.keystore = Arc::new(FileProvider::default());
|
|
||||||
}
|
|
||||||
this.initialized_keystore = true;
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.push(
|
tasks.push(
|
||||||
// Load all user profiles from the database
|
// Load all user profiles from the database
|
||||||
cx.spawn(async move |this, cx| {
|
cx.spawn(async move |this, cx| {
|
||||||
@@ -136,12 +91,10 @@ impl Registry {
|
|||||||
);
|
);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
unwrapping_status,
|
|
||||||
keystore,
|
|
||||||
initialized_keystore,
|
|
||||||
rooms: vec![],
|
rooms: vec![],
|
||||||
persons: HashMap::new(),
|
persons: HashMap::new(),
|
||||||
signer_pubkey: None,
|
signer_pubkey: None,
|
||||||
|
loading: true,
|
||||||
_tasks: tasks,
|
_tasks: tasks,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,16 +118,6 @@ impl Registry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the keystore.
|
|
||||||
pub fn keystore(&self) -> Arc<dyn KeyStore> {
|
|
||||||
Arc::clone(&self.keystore)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if the keystore is a file keystore.
|
|
||||||
pub fn is_using_file_keystore(&self) -> bool {
|
|
||||||
self.keystore.name() == "file"
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the public key of the currently activated signer.
|
/// Returns the public key of the currently activated signer.
|
||||||
pub fn signer_pubkey(&self) -> Option<PublicKey> {
|
pub fn signer_pubkey(&self) -> Option<PublicKey> {
|
||||||
self.signer_pubkey
|
self.signer_pubkey
|
||||||
@@ -233,6 +176,11 @@ impl Registry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_loading(&mut self, loading: bool, cx: &mut Context<Self>) {
|
||||||
|
self.loading = loading;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
/// Get a room by its ID.
|
/// Get a room by its ID.
|
||||||
pub fn room(&self, id: &u64, cx: &App) -> Option<Entity<Room>> {
|
pub fn room(&self, id: &u64, cx: &App) -> Option<Entity<Room>> {
|
||||||
self.rooms
|
self.rooms
|
||||||
@@ -297,24 +245,13 @@ impl Registry {
|
|||||||
pub fn search_by_public_key(&self, public_key: PublicKey, cx: &App) -> Vec<Entity<Room>> {
|
pub fn search_by_public_key(&self, public_key: PublicKey, cx: &App) -> Vec<Entity<Room>> {
|
||||||
self.rooms
|
self.rooms
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|room| room.read(cx).members.contains(&public_key))
|
.filter(|room| room.read(cx).members.contains_key(&public_key))
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the loading status of the registry.
|
|
||||||
pub fn set_unwrapping_status(&mut self, status: UnwrappingStatus, cx: &mut Context<Self>) {
|
|
||||||
self.unwrapping_status.update(cx, |this, cx| {
|
|
||||||
*this = status;
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reset the registry.
|
/// Reset the registry.
|
||||||
pub fn reset(&mut self, cx: &mut Context<Self>) {
|
pub fn reset(&mut self, cx: &mut Context<Self>) {
|
||||||
// Reset the unwrapping status
|
|
||||||
self.set_unwrapping_status(UnwrappingStatus::default(), cx);
|
|
||||||
|
|
||||||
// Clear the current identity
|
// Clear the current identity
|
||||||
self.signer_pubkey = None;
|
self.signer_pubkey = None;
|
||||||
|
|
||||||
@@ -339,10 +276,7 @@ impl Registry {
|
|||||||
|
|
||||||
let authored_filter = Filter::new()
|
let authored_filter = Filter::new()
|
||||||
.kind(Kind::ApplicationSpecificData)
|
.kind(Kind::ApplicationSpecificData)
|
||||||
.custom_tag(
|
.custom_tag(SingleLetterTag::lowercase(Alphabet::A), public_key);
|
||||||
SingleLetterTag::lowercase(Alphabet::A),
|
|
||||||
public_key.to_hex(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let addressed_filter = Filter::new()
|
let addressed_filter = Filter::new()
|
||||||
.kind(Kind::ApplicationSpecificData)
|
.kind(Kind::ApplicationSpecificData)
|
||||||
|
|||||||
@@ -7,20 +7,57 @@ use anyhow::{anyhow, Error};
|
|||||||
use common::display::RenderedProfile;
|
use common::display::RenderedProfile;
|
||||||
use common::event::EventUtils;
|
use common::event::EventUtils;
|
||||||
use gpui::{App, AppContext, Context, EventEmitter, SharedString, SharedUri, Task};
|
use gpui::{App, AppContext, Context, EventEmitter, SharedString, SharedUri, Task};
|
||||||
use itertools::Itertools;
|
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use states::app_state;
|
use states::app_state;
|
||||||
use states::constants::SEND_RETRY;
|
use states::constants::SEND_RETRY;
|
||||||
|
|
||||||
use crate::Registry;
|
use crate::Registry;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Deserialize, Serialize)]
|
||||||
|
pub enum SignerKind {
|
||||||
|
Encryption,
|
||||||
|
User,
|
||||||
|
#[default]
|
||||||
|
Auto,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct SendOptions {
|
||||||
|
pub backup: bool,
|
||||||
|
pub signer_kind: SignerKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SendOptions {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
backup: true,
|
||||||
|
signer_kind: SignerKind::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn backup(&self) -> bool {
|
||||||
|
self.backup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SendOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SendReport {
|
pub struct SendReport {
|
||||||
pub receiver: PublicKey,
|
pub receiver: PublicKey,
|
||||||
|
|
||||||
pub status: Option<Output<EventId>>,
|
pub status: Option<Output<EventId>>,
|
||||||
pub error: Option<SharedString>,
|
pub error: Option<SharedString>,
|
||||||
pub on_hold: Option<Event>,
|
|
||||||
pub relays_not_found: bool,
|
pub relays_not_found: bool,
|
||||||
|
pub device_not_found: bool,
|
||||||
|
|
||||||
|
pub on_hold: Option<Event>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SendReport {
|
impl SendReport {
|
||||||
@@ -31,18 +68,17 @@ impl SendReport {
|
|||||||
error: None,
|
error: None,
|
||||||
on_hold: None,
|
on_hold: None,
|
||||||
relays_not_found: false,
|
relays_not_found: false,
|
||||||
|
device_not_found: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn status(mut self, output: Output<EventId>) -> Self {
|
pub fn status(mut self, output: Output<EventId>) -> Self {
|
||||||
self.status = Some(output);
|
self.status = Some(output);
|
||||||
self.relays_not_found = false;
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn error(mut self, error: impl Into<SharedString>) -> Self {
|
pub fn error(mut self, error: impl Into<SharedString>) -> Self {
|
||||||
self.error = Some(error.into());
|
self.error = Some(error.into());
|
||||||
self.relays_not_found = false;
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,11 +87,16 @@ impl SendReport {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn not_found(mut self) -> Self {
|
pub fn relays_not_found(mut self) -> Self {
|
||||||
self.relays_not_found = true;
|
self.relays_not_found = true;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn device_not_found(mut self) -> Self {
|
||||||
|
self.device_not_found = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_relay_error(&self) -> bool {
|
pub fn is_relay_error(&self) -> bool {
|
||||||
self.error.is_some() || self.relays_not_found
|
self.error.is_some() || self.relays_not_found
|
||||||
}
|
}
|
||||||
@@ -82,6 +123,8 @@ pub enum RoomKind {
|
|||||||
Request,
|
Request,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DevicePublicKey = PublicKey;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Room {
|
pub struct Room {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
@@ -89,7 +132,7 @@ pub struct Room {
|
|||||||
/// Subject of the room
|
/// Subject of the room
|
||||||
pub subject: Option<String>,
|
pub subject: Option<String>,
|
||||||
/// All members of the room
|
/// All members of the room
|
||||||
pub members: Vec<PublicKey>,
|
pub members: HashMap<PublicKey, Option<DevicePublicKey>>,
|
||||||
/// Kind
|
/// Kind
|
||||||
pub kind: RoomKind,
|
pub kind: RoomKind,
|
||||||
}
|
}
|
||||||
@@ -128,7 +171,11 @@ impl From<&Event> for Room {
|
|||||||
let created_at = val.created_at;
|
let created_at = val.created_at;
|
||||||
|
|
||||||
// Get the members from the event's tags and event's pubkey
|
// Get the members from the event's tags and event's pubkey
|
||||||
let members = val.all_pubkeys();
|
let members: HashMap<PublicKey, Option<DevicePublicKey>> = val
|
||||||
|
.all_pubkeys()
|
||||||
|
.into_iter()
|
||||||
|
.map(|public_key| (public_key, None))
|
||||||
|
.collect();
|
||||||
|
|
||||||
// Get subject from tags
|
// Get subject from tags
|
||||||
let subject = val
|
let subject = val
|
||||||
@@ -152,7 +199,11 @@ impl From<&UnsignedEvent> for Room {
|
|||||||
let created_at = val.created_at;
|
let created_at = val.created_at;
|
||||||
|
|
||||||
// Get the members from the event's tags and event's pubkey
|
// Get the members from the event's tags and event's pubkey
|
||||||
let members = val.all_pubkeys();
|
let members: HashMap<PublicKey, Option<DevicePublicKey>> = val
|
||||||
|
.all_pubkeys()
|
||||||
|
.into_iter()
|
||||||
|
.map(|public_key| (public_key, None))
|
||||||
|
.collect();
|
||||||
|
|
||||||
// Get subject from tags
|
// Get subject from tags
|
||||||
let subject = val
|
let subject = val
|
||||||
@@ -233,8 +284,8 @@ impl Room {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the members of the room
|
/// Returns the members of the room
|
||||||
pub fn members(&self) -> &Vec<PublicKey> {
|
pub fn members(&self) -> Vec<PublicKey> {
|
||||||
&self.members
|
self.members.keys().cloned().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if the room has more than two members (group)
|
/// Checks if the room has more than two members (group)
|
||||||
@@ -264,17 +315,17 @@ impl Room {
|
|||||||
///
|
///
|
||||||
/// This member is always different from the current user.
|
/// This member is always different from the current user.
|
||||||
fn display_member(&self, cx: &App) -> Profile {
|
fn display_member(&self, cx: &App) -> Profile {
|
||||||
let registry = Registry::read_global(cx);
|
let registry = Registry::global(cx);
|
||||||
|
let signer_pubkey = registry.read(cx).signer_pubkey();
|
||||||
|
|
||||||
if let Some(public_key) = registry.signer_pubkey() {
|
let target_member = self
|
||||||
for member in self.members() {
|
.members
|
||||||
if member != &public_key {
|
.keys()
|
||||||
return registry.get_person(member, cx);
|
.find(|&member| Some(member) != signer_pubkey.as_ref())
|
||||||
}
|
.or_else(|| self.members.keys().next())
|
||||||
}
|
.expect("Room should have at least one member");
|
||||||
}
|
|
||||||
|
|
||||||
registry.get_person(&self.members[0], cx)
|
registry.read(cx).get_person(target_member, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Merge the names of the first two members of the room.
|
/// Merge the names of the first two members of the room.
|
||||||
@@ -284,7 +335,7 @@ impl Room {
|
|||||||
if self.is_group() {
|
if self.is_group() {
|
||||||
let profiles: Vec<Profile> = self
|
let profiles: Vec<Profile> = self
|
||||||
.members
|
.members
|
||||||
.iter()
|
.keys()
|
||||||
.map(|public_key| registry.get_person(public_key, cx))
|
.map(|public_key| registry.get_person(public_key, cx))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -305,91 +356,9 @@ impl Room {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Connects to all members's messaging relays
|
|
||||||
pub fn connect(&self, cx: &App) -> Task<Result<HashMap<PublicKey, Vec<RelayUrl>>, Error>> {
|
|
||||||
let members = self.members.clone();
|
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
|
||||||
let client = app_state().client();
|
|
||||||
let signer = client.signer().await?;
|
|
||||||
let public_key = signer.get_public_key().await?;
|
|
||||||
|
|
||||||
let mut relays = HashMap::new();
|
|
||||||
let mut processed = HashSet::new();
|
|
||||||
|
|
||||||
for member in members.into_iter() {
|
|
||||||
if member == public_key {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
relays.insert(member, vec![]);
|
|
||||||
|
|
||||||
let filter = Filter::new()
|
|
||||||
.kind(Kind::InboxRelays)
|
|
||||||
.author(member)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
let mut stream = client
|
|
||||||
.stream_events(filter, Duration::from_secs(10))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if let Some(event) = stream.next().await {
|
|
||||||
if processed.insert(event.id) {
|
|
||||||
let public_key = event.pubkey;
|
|
||||||
let urls: Vec<RelayUrl> = nip17::extract_owned_relay_list(event).collect();
|
|
||||||
|
|
||||||
// Check if at least one URL exists
|
|
||||||
if urls.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect to relays
|
|
||||||
for url in urls.iter() {
|
|
||||||
client.add_relay(url).await?;
|
|
||||||
client.connect_relay(url).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
relays.entry(public_key).and_modify(|v| v.extend(urls));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(relays)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Loads all messages for this room from the database
|
|
||||||
pub fn load_messages(&self, cx: &App) -> Task<Result<Vec<UnsignedEvent>, Error>> {
|
|
||||||
let conversation_id = self.id.to_string();
|
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
|
||||||
let client = app_state().client();
|
|
||||||
let filter = Filter::new()
|
|
||||||
.kind(Kind::ApplicationSpecificData)
|
|
||||||
.custom_tag(
|
|
||||||
SingleLetterTag::lowercase(Alphabet::C),
|
|
||||||
conversation_id.as_str(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let stored = client.database().query(filter).await?;
|
|
||||||
let mut messages = Vec::with_capacity(stored.len());
|
|
||||||
|
|
||||||
for event in stored {
|
|
||||||
match UnsignedEvent::from_json(&event.content) {
|
|
||||||
Ok(rumor) => messages.push(rumor),
|
|
||||||
Err(e) => log::warn!("Failed to parse stored rumor: {e}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
messages.sort_by_key(|message| message.created_at);
|
|
||||||
|
|
||||||
Ok(messages)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Emits a new message signal to the current room
|
/// Emits a new message signal to the current room
|
||||||
pub fn emit_message(&self, gift_wrap_id: EventId, event: UnsignedEvent, cx: &mut Context<Self>) {
|
pub fn emit_message(&self, id: EventId, event: UnsignedEvent, cx: &mut Context<Self>) {
|
||||||
cx.emit(RoomSignal::NewMessage((gift_wrap_id, event)));
|
cx.emit(RoomSignal::NewMessage((id, event)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Emits a signal to refresh the current room's messages.
|
/// Emits a signal to refresh the current room's messages.
|
||||||
@@ -397,9 +366,69 @@ impl Room {
|
|||||||
cx.emit(RoomSignal::Refresh);
|
cx.emit(RoomSignal::Refresh);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get messaging relays and encryption keys announcement for each member
|
||||||
|
pub fn connect(&self, cx: &App) -> Task<Result<(), Error>> {
|
||||||
|
let members = self.members();
|
||||||
|
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let client = app_state().client();
|
||||||
|
let signer = client.signer().await?;
|
||||||
|
let public_key = signer.get_public_key().await?;
|
||||||
|
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||||
|
|
||||||
|
for member in members.into_iter() {
|
||||||
|
if member == public_key {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kind(Kind::InboxRelays)
|
||||||
|
.author(member)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
// Subscribe to get members messaging relays
|
||||||
|
client.subscribe(filter, Some(opts)).await?;
|
||||||
|
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kind(Kind::Custom(10044))
|
||||||
|
.author(member)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
// Subscribe to get members encryption keys announcement
|
||||||
|
client.subscribe(filter, Some(opts)).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all messages belonging to the room
|
||||||
|
pub fn get_messages(&self, cx: &App) -> Task<Result<Vec<UnsignedEvent>, Error>> {
|
||||||
|
let conversation_id = self.id.to_string();
|
||||||
|
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let client = app_state().client();
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kind(Kind::ApplicationSpecificData)
|
||||||
|
.custom_tag(SingleLetterTag::lowercase(Alphabet::C), conversation_id);
|
||||||
|
|
||||||
|
let stored = client.database().query(filter).await?;
|
||||||
|
|
||||||
|
let mut messages: Vec<UnsignedEvent> = stored
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|event| UnsignedEvent::from_json(&event.content).ok())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
messages.sort_by_key(|message| message.created_at);
|
||||||
|
|
||||||
|
Ok(messages)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a new message event (unsigned)
|
/// Create a new message event (unsigned)
|
||||||
pub fn create_message(&self, content: &str, replies: &[EventId], cx: &App) -> UnsignedEvent {
|
pub fn create_message(&self, content: &str, replies: &[EventId], cx: &App) -> UnsignedEvent {
|
||||||
let public_key = Registry::read_global(cx).signer_pubkey().unwrap();
|
let registry = Registry::global(cx);
|
||||||
|
let public_key = registry.read(cx).signer_pubkey().unwrap();
|
||||||
let subject = self.subject.clone();
|
let subject = self.subject.clone();
|
||||||
|
|
||||||
let mut tags = vec![];
|
let mut tags = vec![];
|
||||||
@@ -407,7 +436,7 @@ impl Room {
|
|||||||
// Add receivers
|
// Add receivers
|
||||||
//
|
//
|
||||||
// NOTE: current user will be removed from the list of receivers
|
// NOTE: current user will be removed from the list of receivers
|
||||||
for member in self.members.iter() {
|
for (member, _) in self.members.iter() {
|
||||||
tags.push(Tag::public_key(member.to_owned()));
|
tags.push(Tag::public_key(member.to_owned()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,34 +476,42 @@ impl Room {
|
|||||||
/// Create a task to send a message to all room members
|
/// Create a task to send a message to all room members
|
||||||
pub fn send_message(
|
pub fn send_message(
|
||||||
&self,
|
&self,
|
||||||
rumor: UnsignedEvent,
|
rumor: &UnsignedEvent,
|
||||||
backup: bool,
|
opts: &SendOptions,
|
||||||
cx: &App,
|
cx: &App,
|
||||||
) -> Task<Result<Vec<SendReport>, Error>> {
|
) -> Task<Result<Vec<SendReport>, Error>> {
|
||||||
let mut members = self.members.clone();
|
let mut members = self.members.clone();
|
||||||
|
let rumor = rumor.to_owned();
|
||||||
|
let opts = opts.to_owned();
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let states = app_state();
|
let states = app_state();
|
||||||
let client = states.client();
|
let client = states.client();
|
||||||
let signer = client.signer().await?;
|
let device = states.device.read().await.encryption_keys.clone();
|
||||||
let public_key = signer.get_public_key().await?;
|
|
||||||
|
let user_signer = client.signer().await?;
|
||||||
|
let user_pubkey = user_signer.get_public_key().await?;
|
||||||
|
|
||||||
// Collect relay hints for all participants (including current user)
|
// Collect relay hints for all participants (including current user)
|
||||||
let mut participants = members.clone();
|
let mut participants: Vec<PublicKey> = members.keys().cloned().collect();
|
||||||
if !participants.contains(&public_key) {
|
|
||||||
participants.push(public_key);
|
if !participants.contains(&user_pubkey) {
|
||||||
|
participants.push(user_pubkey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize relay cache
|
||||||
let mut relay_cache: HashMap<PublicKey, Vec<RelayUrl>> = HashMap::new();
|
let mut relay_cache: HashMap<PublicKey, Vec<RelayUrl>> = HashMap::new();
|
||||||
|
|
||||||
for participant in participants.iter().cloned() {
|
for participant in participants.iter().cloned() {
|
||||||
let urls = Self::messaging_relays(participant).await;
|
let urls = states.messaging_relays(participant).await;
|
||||||
relay_cache.insert(participant, urls);
|
relay_cache.insert(participant, urls);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update rumor with relay hints for each receiver
|
// Update rumor with relay hints for each receiver
|
||||||
let mut rumor = rumor;
|
let mut rumor = rumor;
|
||||||
let mut tags_with_hints = Vec::new();
|
let mut tags_with_hints = Vec::new();
|
||||||
for tag in rumor.tags.to_vec() {
|
|
||||||
|
for tag in rumor.tags.into_iter() {
|
||||||
if let Some(standard) = tag.as_standardized().cloned() {
|
if let Some(standard) = tag.as_standardized().cloned() {
|
||||||
match standard {
|
match standard {
|
||||||
TagStandard::PublicKey {
|
TagStandard::PublicKey {
|
||||||
@@ -483,18 +520,18 @@ impl Room {
|
|||||||
uppercase,
|
uppercase,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let relay_url =
|
let relay_url = relay_cache
|
||||||
relay_cache
|
|
||||||
.get(&public_key)
|
.get(&public_key)
|
||||||
.and_then(|urls| urls.first().cloned());
|
.and_then(|urls| urls.first().cloned());
|
||||||
|
|
||||||
let updated = TagStandard::PublicKey {
|
let updated = TagStandard::PublicKey {
|
||||||
public_key,
|
public_key,
|
||||||
relay_url,
|
relay_url,
|
||||||
alias,
|
alias,
|
||||||
uppercase,
|
uppercase,
|
||||||
};
|
};
|
||||||
tags_with_hints
|
|
||||||
.push(Tag::from_standardized_without_cell(updated));
|
tags_with_hints.push(Tag::from_standardized_without_cell(updated));
|
||||||
}
|
}
|
||||||
_ => tags_with_hints.push(tag),
|
_ => tags_with_hints.push(tag),
|
||||||
}
|
}
|
||||||
@@ -506,29 +543,42 @@ impl Room {
|
|||||||
|
|
||||||
// Remove the current user's public key from the list of receivers
|
// Remove the current user's public key from the list of receivers
|
||||||
// Current user will be handled separately
|
// Current user will be handled separately
|
||||||
members.retain(|&pk| pk != public_key);
|
let (public_key, device_pubkey) = members.remove_entry(&user_pubkey).unwrap();
|
||||||
|
|
||||||
|
// Determine the signer will be used based on the provided options
|
||||||
|
let signer = Self::select_signer(&opts.signer_kind, device, user_signer)?;
|
||||||
|
|
||||||
|
// Collect the send reports
|
||||||
let mut reports: Vec<SendReport> = vec![];
|
let mut reports: Vec<SendReport> = vec![];
|
||||||
|
|
||||||
for receiver in members.into_iter() {
|
for (receiver, device_pubkey) in members.into_iter() {
|
||||||
let rumor = rumor.clone();
|
|
||||||
let event = EventBuilder::gift_wrap(&signer, &receiver, rumor, vec![]).await?;
|
|
||||||
let urls = relay_cache.get(&receiver).cloned().unwrap_or_default();
|
let urls = relay_cache.get(&receiver).cloned().unwrap_or_default();
|
||||||
|
|
||||||
// Check if there are any relays to send the event to
|
// Check if there are any relays to send the message to
|
||||||
if urls.is_empty() {
|
if urls.is_empty() {
|
||||||
reports.push(SendReport::new(receiver).not_found());
|
reports.push(SendReport::new(receiver).relays_not_found());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip sending if using encryption keys but device not found
|
||||||
|
if device_pubkey.is_none() && matches!(opts.signer_kind, SignerKind::Encryption) {
|
||||||
|
reports.push(SendReport::new(receiver).device_not_found());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the receiver based on the signer kind
|
||||||
|
let rumor = rumor.clone();
|
||||||
|
let target = Self::select_receiver(&opts.signer_kind, receiver, device_pubkey);
|
||||||
|
let event = EventBuilder::gift_wrap(&signer, &target, rumor, vec![]).await?;
|
||||||
|
|
||||||
// Send the event to the messaging relays
|
// Send the event to the messaging relays
|
||||||
match client.send_event_to(urls, &event).await {
|
match client.send_event_to(urls, &event).await {
|
||||||
Ok(output) => {
|
Ok(output) => {
|
||||||
let id = output.id().to_owned();
|
let id = output.id().to_owned();
|
||||||
let auth_required = output.failed.iter().any(|m| m.1.starts_with("auth-"));
|
let auth = output.failed.iter().any(|(_, s)| s.starts_with("auth-"));
|
||||||
let report = SendReport::new(receiver).status(output);
|
let report = SendReport::new(receiver).status(output);
|
||||||
|
|
||||||
if auth_required {
|
if auth {
|
||||||
// Wait for authenticated and resent event successfully
|
// Wait for authenticated and resent event successfully
|
||||||
for attempt in 0..=SEND_RETRY {
|
for attempt in 0..=SEND_RETRY {
|
||||||
let retry_manager = states.tracker().read().await;
|
let retry_manager = states.tracker().read().await;
|
||||||
@@ -561,15 +611,16 @@ impl Room {
|
|||||||
|
|
||||||
// Construct a gift wrap to back up to current user's owned messaging relays
|
// Construct a gift wrap to back up to current user's owned messaging relays
|
||||||
let rumor = rumor.clone();
|
let rumor = rumor.clone();
|
||||||
let event = EventBuilder::gift_wrap(&signer, &public_key, rumor, vec![]).await?;
|
let target = Self::select_receiver(&opts.signer_kind, public_key, device_pubkey);
|
||||||
|
let event = EventBuilder::gift_wrap(&signer, &target, rumor, vec![]).await?;
|
||||||
|
|
||||||
// Only send a backup message to current user if sent successfully to others
|
// Only send a backup message to current user if sent successfully to others
|
||||||
if reports.iter().all(|r| r.is_sent_success()) && backup {
|
if opts.backup() && reports.iter().all(|r| r.is_sent_success()) {
|
||||||
let urls = relay_cache.get(&public_key).cloned().unwrap_or_default();
|
let urls = relay_cache.get(&public_key).cloned().unwrap_or_default();
|
||||||
|
|
||||||
// Check if there are any relays to send the event to
|
// Check if there are any relays to send the event to
|
||||||
if urls.is_empty() {
|
if urls.is_empty() {
|
||||||
reports.push(SendReport::new(public_key).not_found());
|
reports.push(SendReport::new(public_key).relays_not_found());
|
||||||
} else {
|
} else {
|
||||||
// Send the event to the messaging relays
|
// Send the event to the messaging relays
|
||||||
match client.send_event_to(urls, &event).await {
|
match client.send_event_to(urls, &event).await {
|
||||||
@@ -596,7 +647,8 @@ impl Room {
|
|||||||
cx: &App,
|
cx: &App,
|
||||||
) -> Task<Result<Vec<SendReport>, Error>> {
|
) -> Task<Result<Vec<SendReport>, Error>> {
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let client = app_state().client();
|
let states = app_state();
|
||||||
|
let client = states.client();
|
||||||
let mut resend_reports = vec![];
|
let mut resend_reports = vec![];
|
||||||
|
|
||||||
for report in reports.into_iter() {
|
for report in reports.into_iter() {
|
||||||
@@ -625,11 +677,11 @@ impl Room {
|
|||||||
|
|
||||||
// Process the on hold event if it exists
|
// Process the on hold event if it exists
|
||||||
if let Some(event) = report.on_hold {
|
if let Some(event) = report.on_hold {
|
||||||
let urls = Self::messaging_relays(receiver).await;
|
let urls = states.messaging_relays(receiver).await;
|
||||||
|
|
||||||
// Check if there are any relays to send the event to
|
// Check if there are any relays to send the event to
|
||||||
if urls.is_empty() {
|
if urls.is_empty() {
|
||||||
resend_reports.push(SendReport::new(receiver).not_found());
|
resend_reports.push(SendReport::new(receiver).relays_not_found());
|
||||||
} else {
|
} else {
|
||||||
// Send the event to the messaging relays
|
// Send the event to the messaging relays
|
||||||
match client.send_event_to(urls, &event).await {
|
match client.send_event_to(urls, &event).await {
|
||||||
@@ -648,36 +700,24 @@ impl Room {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets messaging relays for public key
|
fn select_signer<T>(kind: &SignerKind, device: Option<T>, user: T) -> Result<T, Error>
|
||||||
async fn messaging_relays(public_key: PublicKey) -> Vec<RelayUrl> {
|
where
|
||||||
let client = app_state().client();
|
T: NostrSigner,
|
||||||
let mut relay_urls = vec![];
|
{
|
||||||
|
match kind {
|
||||||
let filter = Filter::new()
|
SignerKind::Encryption => {
|
||||||
.kind(Kind::InboxRelays)
|
Ok(device.ok_or_else(|| anyhow!("No encryption keys found"))?)
|
||||||
.author(public_key)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if let Ok(events) = client.database().query(filter).await {
|
|
||||||
if let Some(event) = events.first_owned() {
|
|
||||||
let urls: Vec<RelayUrl> = nip17::extract_owned_relay_list(event).collect();
|
|
||||||
|
|
||||||
// Connect to relays
|
|
||||||
for url in urls.iter() {
|
|
||||||
client.add_relay(url).await.ok();
|
|
||||||
client.connect_relay(url).await.ok();
|
|
||||||
}
|
}
|
||||||
|
SignerKind::User => Ok(user),
|
||||||
relay_urls.extend(urls.into_iter().take(3).unique());
|
SignerKind::Auto => Ok(device.unwrap_or(user)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
relay_urls
|
fn select_receiver(kind: &SignerKind, user: PublicKey, device: Option<PublicKey>) -> PublicKey {
|
||||||
|
match kind {
|
||||||
|
SignerKind::Encryption => device.unwrap(),
|
||||||
|
SignerKind::User => user,
|
||||||
|
SignerKind::Auto => device.unwrap_or(user),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ publish.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
nostr-lmdb.workspace = true
|
nostr-lmdb.workspace = true
|
||||||
|
|
||||||
dirs.workspace = true
|
dirs.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
flume.workspace = true
|
flume.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
|
||||||
whoami = "1.5.2"
|
whoami = "1.6.1"
|
||||||
rustls = "0.23.23"
|
rustls = "0.23.23"
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
pub const APP_NAME: &str = "Coop";
|
pub const CLIENT_NAME: &str = "Coop";
|
||||||
pub const APP_ID: &str = "su.reya.coop";
|
pub const APP_ID: &str = "su.reya.coop";
|
||||||
pub const APP_PUBKEY: &str = "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDc4MkNFRkQ2RkVGQURGNzUKUldSMTMvcisxdThzZUZraHc4Vno3NVNJek81VkJFUEV3MkJweGFxQXhpekdSU1JIekpqMG4yemMK";
|
pub const APP_PUBKEY: &str = "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDc4MkNFRkQ2RkVGQURGNzUKUldSMTMvcisxdThzZUZraHc4Vno3NVNJek81VkJFUEV3MkJweGFxQXhpekdSU1JIekpqMG4yemMK";
|
||||||
pub const APP_UPDATER_ENDPOINT: &str = "https://coop-updater.reya.su/";
|
pub const APP_UPDATER_ENDPOINT: &str = "https://coop-updater.reya.su/";
|
||||||
|
|
||||||
pub const KEYRING_URL: &str = "Coop Safe Storage";
|
|
||||||
pub const SETTINGS_IDENTIFIER: &str = "coop:settings";
|
pub const SETTINGS_IDENTIFIER: &str = "coop:settings";
|
||||||
|
|
||||||
/// Bootstrap Relays.
|
/// Bootstrap Relays.
|
||||||
@@ -33,6 +32,9 @@ pub const NOSTR_CONNECT_TIMEOUT: u64 = 200;
|
|||||||
/// Default timeout (in seconds) for Nostr Connect (Bunker)
|
/// Default timeout (in seconds) for Nostr Connect (Bunker)
|
||||||
pub const BUNKER_TIMEOUT: u64 = 30;
|
pub const BUNKER_TIMEOUT: u64 = 30;
|
||||||
|
|
||||||
|
/// Default timeout (in seconds) for fetching events
|
||||||
|
pub const QUERY_TIMEOUT: u64 = 3;
|
||||||
|
|
||||||
/// Total metadata requests will be grouped.
|
/// Total metadata requests will be grouped.
|
||||||
pub const METADATA_BATCH_LIMIT: usize = 100;
|
pub const METADATA_BATCH_LIMIT: usize = 100;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
use whoami::{devicename, platform};
|
||||||
|
|
||||||
|
use crate::constants::CLIENT_NAME;
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
pub mod constants;
|
pub mod constants;
|
||||||
@@ -9,6 +11,7 @@ pub mod paths;
|
|||||||
pub mod state;
|
pub mod state;
|
||||||
|
|
||||||
static APP_STATE: OnceLock<AppState> = OnceLock::new();
|
static APP_STATE: OnceLock<AppState> = OnceLock::new();
|
||||||
|
static APP_NAME: OnceLock<String> = OnceLock::new();
|
||||||
static NIP65_RELAYS: OnceLock<Vec<(RelayUrl, Option<RelayMetadata>)>> = OnceLock::new();
|
static NIP65_RELAYS: OnceLock<Vec<(RelayUrl, Option<RelayMetadata>)>> = OnceLock::new();
|
||||||
static NIP17_RELAYS: OnceLock<Vec<RelayUrl>> = OnceLock::new();
|
static NIP17_RELAYS: OnceLock<Vec<RelayUrl>> = OnceLock::new();
|
||||||
|
|
||||||
@@ -17,6 +20,15 @@ pub fn app_state() -> &'static AppState {
|
|||||||
APP_STATE.get_or_init(AppState::new)
|
APP_STATE.get_or_init(AppState::new)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn app_name() -> &'static String {
|
||||||
|
APP_NAME.get_or_init(|| {
|
||||||
|
let devicename = devicename();
|
||||||
|
let platform = platform();
|
||||||
|
|
||||||
|
format!("{CLIENT_NAME} on {platform} ({devicename})")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Default NIP-65 Relays. Used for new account
|
/// Default NIP-65 Relays. Used for new account
|
||||||
pub fn default_nip65_relays() -> &'static Vec<(RelayUrl, Option<RelayMetadata>)> {
|
pub fn default_nip65_relays() -> &'static Vec<(RelayUrl, Option<RelayMetadata>)> {
|
||||||
NIP65_RELAYS.get_or_init(|| {
|
NIP65_RELAYS.get_or_init(|| {
|
||||||
|
|||||||
25
crates/states/src/state/device.rs
Normal file
25
crates/states/src/state/device.rs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct Device {
|
||||||
|
/// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
|
||||||
|
///
|
||||||
|
/// The client keys that used for communication between devices
|
||||||
|
pub client_keys: Option<Arc<dyn NostrSigner>>,
|
||||||
|
|
||||||
|
/// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
|
||||||
|
///
|
||||||
|
/// The encryption keys that used for encryption and decryption
|
||||||
|
pub encryption_keys: Option<Arc<dyn NostrSigner>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Device {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
client_keys: None,
|
||||||
|
encryption_keys: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
crates/states/src/state/ingester.rs
Normal file
31
crates/states/src/state/ingester.rs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
use flume::{Receiver, Sender};
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Ingester {
|
||||||
|
rx: Receiver<PublicKey>,
|
||||||
|
tx: Sender<PublicKey>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Ingester {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ingester {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let (tx, rx) = flume::bounded::<PublicKey>(1024);
|
||||||
|
Self { rx, tx }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn receiver(&self) -> &Receiver<PublicKey> {
|
||||||
|
&self.rx
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send(&self, public_key: PublicKey) {
|
||||||
|
if let Err(e) = self.tx.send_async(public_key).await {
|
||||||
|
log::error!("Failed to send public key: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,169 +1,31 @@
|
|||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::{hash_map::DefaultHasher, HashMap, HashSet};
|
use std::collections::hash_map::DefaultHasher;
|
||||||
|
use std::collections::HashSet;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{anyhow, Error};
|
use anyhow::{anyhow, Context, Error};
|
||||||
use flume::{Receiver, Sender};
|
|
||||||
use nostr_lmdb::NostrLMDB;
|
use nostr_lmdb::NostrLMDB;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use smol::lock::RwLock;
|
use smol::lock::RwLock;
|
||||||
|
|
||||||
|
use crate::app_name;
|
||||||
use crate::constants::{
|
use crate::constants::{
|
||||||
BOOTSTRAP_RELAYS, METADATA_BATCH_LIMIT, METADATA_BATCH_TIMEOUT, SEARCH_RELAYS,
|
BOOTSTRAP_RELAYS, METADATA_BATCH_LIMIT, METADATA_BATCH_TIMEOUT, QUERY_TIMEOUT, SEARCH_RELAYS,
|
||||||
};
|
};
|
||||||
use crate::paths::config_dir;
|
use crate::paths::config_dir;
|
||||||
|
use crate::state::device::Device;
|
||||||
|
use crate::state::ingester::Ingester;
|
||||||
|
use crate::state::tracker::EventTracker;
|
||||||
|
|
||||||
const TIMEOUT: u64 = 5;
|
mod device;
|
||||||
|
mod ingester;
|
||||||
|
mod signal;
|
||||||
|
mod tracker;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
pub use signal::*;
|
||||||
pub struct AuthRequest {
|
|
||||||
pub url: RelayUrl,
|
|
||||||
pub challenge: String,
|
|
||||||
pub sending: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AuthRequest {
|
|
||||||
pub fn new(challenge: impl Into<String>, url: RelayUrl) -> Self {
|
|
||||||
Self {
|
|
||||||
challenge: challenge.into(),
|
|
||||||
sending: false,
|
|
||||||
url,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
pub enum UnwrappingStatus {
|
|
||||||
#[default]
|
|
||||||
Initialized,
|
|
||||||
Processing,
|
|
||||||
Complete,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Signals sent through the global event channel to notify UI
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum SignalKind {
|
|
||||||
/// A signal to notify UI that the client's signer has been set
|
|
||||||
SignerSet(PublicKey),
|
|
||||||
|
|
||||||
/// A signal to notify UI that the client's signer has been unset
|
|
||||||
SignerUnset,
|
|
||||||
|
|
||||||
/// A signal to notify UI that the relay requires authentication
|
|
||||||
Auth(AuthRequest),
|
|
||||||
|
|
||||||
/// A signal to notify UI that a new profile has been received
|
|
||||||
NewProfile(Profile),
|
|
||||||
|
|
||||||
/// A signal to notify UI that a new gift wrap event has been received
|
|
||||||
NewMessage((EventId, UnsignedEvent)),
|
|
||||||
|
|
||||||
/// A signal to notify UI that no messaging relays for current user was found
|
|
||||||
MessagingRelaysNotFound,
|
|
||||||
|
|
||||||
/// A signal to notify UI that no gossip relays for current user was found
|
|
||||||
GossipRelaysNotFound,
|
|
||||||
|
|
||||||
/// A signal to notify UI that gift wrap status has changed
|
|
||||||
GiftWrapStatus(UnwrappingStatus),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Signal {
|
|
||||||
rx: Receiver<SignalKind>,
|
|
||||||
tx: Sender<SignalKind>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Signal {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Signal {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
let (tx, rx) = flume::bounded::<SignalKind>(2048);
|
|
||||||
Self { rx, tx }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn receiver(&self) -> &Receiver<SignalKind> {
|
|
||||||
&self.rx
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sender(&self) -> &Sender<SignalKind> {
|
|
||||||
&self.tx
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn send(&self, kind: SignalKind) {
|
|
||||||
if let Err(e) = self.tx.send_async(kind).await {
|
|
||||||
log::error!("Failed to send signal: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Ingester {
|
|
||||||
rx: Receiver<PublicKey>,
|
|
||||||
tx: Sender<PublicKey>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Ingester {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Ingester {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
let (tx, rx) = flume::bounded::<PublicKey>(1024);
|
|
||||||
Self { rx, tx }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn receiver(&self) -> &Receiver<PublicKey> {
|
|
||||||
&self.rx
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn send(&self, public_key: PublicKey) {
|
|
||||||
if let Err(e) = self.tx.send_async(public_key).await {
|
|
||||||
log::error!("Failed to send public key: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
|
||||||
pub struct EventTracker {
|
|
||||||
/// Tracking events that have been resent by Coop in the current session
|
|
||||||
pub resent_ids: Vec<Output<EventId>>,
|
|
||||||
|
|
||||||
/// Temporarily store events that need to be resent later
|
|
||||||
pub resend_queue: HashMap<EventId, RelayUrl>,
|
|
||||||
|
|
||||||
/// Tracking events sent by Coop in the current session
|
|
||||||
pub sent_ids: HashSet<EventId>,
|
|
||||||
|
|
||||||
/// Tracking events seen on which relays in the current session
|
|
||||||
pub seen_on_relays: HashMap<EventId, HashSet<RelayUrl>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventTracker {
|
|
||||||
pub fn resent_ids(&self) -> &Vec<Output<EventId>> {
|
|
||||||
&self.resent_ids
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resend_queue(&self) -> &HashMap<EventId, RelayUrl> {
|
|
||||||
&self.resend_queue
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sent_ids(&self) -> &HashSet<EventId> {
|
|
||||||
&self.sent_ids
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn seen_on_relays(&self) -> &HashMap<EventId, HashSet<RelayUrl>> {
|
|
||||||
&self.seen_on_relays
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
@@ -179,6 +41,9 @@ pub struct AppState {
|
|||||||
/// Ingester channel for processing public keys
|
/// Ingester channel for processing public keys
|
||||||
ingester: Ingester,
|
ingester: Ingester,
|
||||||
|
|
||||||
|
/// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
|
||||||
|
pub device: RwLock<Device>,
|
||||||
|
|
||||||
/// The timestamp when the application was initialized.
|
/// The timestamp when the application was initialized.
|
||||||
pub initialized_at: Timestamp,
|
pub initialized_at: Timestamp,
|
||||||
|
|
||||||
@@ -213,6 +78,7 @@ impl AppState {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let client = ClientBuilder::default().database(lmdb).opts(opts).build();
|
let client = ClientBuilder::default().database(lmdb).opts(opts).build();
|
||||||
|
let device = RwLock::new(Device::default());
|
||||||
let event_tracker = RwLock::new(EventTracker::default());
|
let event_tracker = RwLock::new(EventTracker::default());
|
||||||
|
|
||||||
let signal = Signal::default();
|
let signal = Signal::default();
|
||||||
@@ -220,6 +86,7 @@ impl AppState {
|
|||||||
|
|
||||||
Self {
|
Self {
|
||||||
client,
|
client,
|
||||||
|
device,
|
||||||
event_tracker,
|
event_tracker,
|
||||||
signal,
|
signal,
|
||||||
ingester,
|
ingester,
|
||||||
@@ -233,6 +100,11 @@ impl AppState {
|
|||||||
&self.client
|
&self.client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a reference to the device
|
||||||
|
pub fn device(&'static self) -> &'static RwLock<Device> {
|
||||||
|
&self.device
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns a reference to the event tracker
|
/// Returns a reference to the event tracker
|
||||||
pub fn tracker(&'static self) -> &'static RwLock<EventTracker> {
|
pub fn tracker(&'static self) -> &'static RwLock<EventTracker> {
|
||||||
&self.event_tracker
|
&self.event_tracker
|
||||||
@@ -262,7 +134,10 @@ impl AppState {
|
|||||||
// Get user's gossip relays
|
// Get user's gossip relays
|
||||||
self.get_nip65(pk).await.ok();
|
self.get_nip65(pk).await.ok();
|
||||||
|
|
||||||
// Exit the current loop
|
// Initialize client keys
|
||||||
|
self.init_client_keys().await.ok();
|
||||||
|
|
||||||
|
// Exit the loop
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -355,6 +230,36 @@ impl AppState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match event.kind {
|
match event.kind {
|
||||||
|
// Encryption Keys announcement event
|
||||||
|
Kind::Custom(10044) => {
|
||||||
|
if let Ok(true) = self.is_self_authored(&event).await {
|
||||||
|
if let Ok(announcement) = self.extract_announcement(&event) {
|
||||||
|
self.signal
|
||||||
|
.send(SignalKind::EncryptionSet(announcement))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Encryption Keys request event
|
||||||
|
Kind::Custom(4454) => {
|
||||||
|
if let Ok(true) = self.is_self_authored(&event).await {
|
||||||
|
if let Ok(announcement) = self.extract_announcement(&event) {
|
||||||
|
self.signal
|
||||||
|
.send(SignalKind::EncryptionRequest(announcement))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Encryption Keys response event
|
||||||
|
Kind::Custom(4455) => {
|
||||||
|
if let Ok(true) = self.is_self_authored(&event).await {
|
||||||
|
if let Ok(response) = self.extract_response(&event) {
|
||||||
|
self.signal
|
||||||
|
.send(SignalKind::EncryptionResponse(response))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Kind::RelayList => {
|
Kind::RelayList => {
|
||||||
// Get events if relay list belongs to current user
|
// Get events if relay list belongs to current user
|
||||||
if let Ok(true) = self.is_self_authored(&event).await {
|
if let Ok(true) = self.is_self_authored(&event).await {
|
||||||
@@ -370,6 +275,11 @@ impl AppState {
|
|||||||
log::error!("Failed to subscribe to contact list event: {e}");
|
log::error!("Failed to subscribe to contact list event: {e}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch user's encryption announcement event
|
||||||
|
if let Err(e) = self.get_announcement(author).await {
|
||||||
|
log::error!("Failed to fetch encryption event: {e}");
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch user's messaging relays event
|
// Fetch user's messaging relays event
|
||||||
if let Err(e) = self.get_nip17(author).await {
|
if let Err(e) = self.get_nip17(author).await {
|
||||||
log::error!("Failed to fetch messaging relays event: {e}");
|
log::error!("Failed to fetch messaging relays event: {e}");
|
||||||
@@ -404,7 +314,9 @@ impl AppState {
|
|||||||
self.signal.send(SignalKind::NewProfile(profile)).await;
|
self.signal.send(SignalKind::NewProfile(profile)).await;
|
||||||
}
|
}
|
||||||
Kind::GiftWrap => {
|
Kind::GiftWrap => {
|
||||||
self.extract_rumor(&event).await;
|
if let Err(e) = self.extract_rumor(&event).await {
|
||||||
|
log::error!("Failed to extract rumor: {e}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -428,14 +340,14 @@ impl AppState {
|
|||||||
event_id, message, ..
|
event_id, message, ..
|
||||||
} => {
|
} => {
|
||||||
let msg = MachineReadablePrefix::parse(&message);
|
let msg = MachineReadablePrefix::parse(&message);
|
||||||
let mut event_tracker = self.event_tracker.write().await;
|
let mut tracker = self.event_tracker.write().await;
|
||||||
|
|
||||||
// Keep track of events sent by Coop
|
// Keep track of events sent by Coop
|
||||||
event_tracker.sent_ids.insert(event_id);
|
tracker.sent_ids.insert(event_id);
|
||||||
|
|
||||||
// Keep track of events that need to be resend after auth
|
// Keep track of events that need to be resend after auth
|
||||||
if let Some(MachineReadablePrefix::AuthRequired) = msg {
|
if let Some(MachineReadablePrefix::AuthRequired) = msg {
|
||||||
event_tracker.resend_queue.insert(event_id, relay_url);
|
tracker.resend_queue.insert(event_id, relay_url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -504,6 +416,47 @@ impl AppState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Encrypt and store a key in the local database.
|
||||||
|
pub async fn set_keys(&self, kind: impl Into<String>, value: String) -> Result<(), Error> {
|
||||||
|
let signer = self.client.signer().await?;
|
||||||
|
let public_key = signer.get_public_key().await?;
|
||||||
|
|
||||||
|
// Encrypt the value
|
||||||
|
let content = signer.nip44_encrypt(&public_key, value.as_ref()).await?;
|
||||||
|
|
||||||
|
// Construct the application data event
|
||||||
|
let event = EventBuilder::new(Kind::ApplicationSpecificData, content)
|
||||||
|
.tag(Tag::identifier(format!("coop:{}", kind.into())))
|
||||||
|
.build(public_key)
|
||||||
|
.sign(&Keys::generate())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Save the event to the database
|
||||||
|
self.client.database().save_event(&event).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get and decrypt a key from the local database.
|
||||||
|
pub async fn get_keys(&self, kind: impl Into<String>) -> Result<Keys, Error> {
|
||||||
|
let signer = self.client.signer().await?;
|
||||||
|
let public_key = signer.get_public_key().await?;
|
||||||
|
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kind(Kind::ApplicationSpecificData)
|
||||||
|
.identifier(format!("coop:{}", kind.into()));
|
||||||
|
|
||||||
|
if let Some(event) = self.client.database().query(filter).await?.first() {
|
||||||
|
let content = signer.nip44_decrypt(&public_key, &event.content).await?;
|
||||||
|
let secret = SecretKey::parse(&content)?;
|
||||||
|
let keys = Keys::new(secret);
|
||||||
|
|
||||||
|
Ok(keys)
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Key not found"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if event is published by current user
|
/// Check if event is published by current user
|
||||||
async fn is_self_authored(&self, event: &Event) -> Result<bool, Error> {
|
async fn is_self_authored(&self, event: &Event) -> Result<bool, Error> {
|
||||||
let signer = self.client.signer().await?;
|
let signer = self.client.signer().await?;
|
||||||
@@ -552,7 +505,7 @@ impl AppState {
|
|||||||
|
|
||||||
/// Get and verify NIP-65 relays for a given public key
|
/// Get and verify NIP-65 relays for a given public key
|
||||||
pub async fn get_nip65(&self, public_key: PublicKey) -> Result<(), Error> {
|
pub async fn get_nip65(&self, public_key: PublicKey) -> Result<(), Error> {
|
||||||
let timeout = Duration::from_secs(TIMEOUT);
|
let timeout = Duration::from_secs(QUERY_TIMEOUT);
|
||||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||||
|
|
||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
@@ -608,9 +561,269 @@ impl AppState {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Initialize the client keys to communicate between clients
|
||||||
|
///
|
||||||
|
/// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
|
||||||
|
pub async fn init_client_keys(&self) -> Result<(), Error> {
|
||||||
|
// Get the keys from the database or generate new ones
|
||||||
|
let keys = self
|
||||||
|
.get_keys("client")
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|_| Keys::generate());
|
||||||
|
|
||||||
|
// Initialize the client keys
|
||||||
|
let mut device = self.device.write().await;
|
||||||
|
device.client_keys = Some(Arc::new(keys));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get and verify encryption announcement for a given public key
|
||||||
|
///
|
||||||
|
/// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
|
||||||
|
pub async fn get_announcement(&self, public_key: PublicKey) -> Result<(), Error> {
|
||||||
|
let timeout = Duration::from_secs(QUERY_TIMEOUT);
|
||||||
|
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||||
|
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kind(Kind::Custom(10044))
|
||||||
|
.author(public_key)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
// Subscribe to events from user's nip65 relays
|
||||||
|
self.client.subscribe(filter.clone(), Some(opts)).await?;
|
||||||
|
|
||||||
|
let tx = self.signal.sender().clone();
|
||||||
|
let database = self.client.database().clone();
|
||||||
|
|
||||||
|
// Verify the received data after a timeout
|
||||||
|
smol::spawn(async move {
|
||||||
|
smol::Timer::after(timeout).await;
|
||||||
|
|
||||||
|
if database.count(filter).await.unwrap_or(0) < 1 {
|
||||||
|
tx.send_async(SignalKind::EncryptionNotSet).await.ok();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate encryption keys and announce them
|
||||||
|
///
|
||||||
|
/// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
|
||||||
|
pub async fn init_encryption_keys(&self) -> Result<(), Error> {
|
||||||
|
let signer = self.client.signer().await?;
|
||||||
|
let keys = Keys::generate();
|
||||||
|
let public_key = keys.public_key();
|
||||||
|
let secret = keys.secret_key().to_secret_hex();
|
||||||
|
|
||||||
|
// Initialize the encryption keys
|
||||||
|
let mut device = self.device.write().await;
|
||||||
|
device.encryption_keys = Some(Arc::new(keys));
|
||||||
|
|
||||||
|
// Store the encryption keys for future use
|
||||||
|
self.set_keys("encryption", secret).await?;
|
||||||
|
|
||||||
|
// Construct the announcement event
|
||||||
|
let event = EventBuilder::new(Kind::Custom(10044), "")
|
||||||
|
.tags(vec![
|
||||||
|
Tag::client(app_name()),
|
||||||
|
Tag::custom(TagKind::custom("n"), vec![public_key]),
|
||||||
|
])
|
||||||
|
.sign(&signer)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Send the announcement event to the relays
|
||||||
|
self.client.send_event(&event).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// User has previously set encryption keys, load them from storage
|
||||||
|
///
|
||||||
|
/// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
|
||||||
|
pub async fn load_encryption_keys(&self, announcement: &Announcement) -> Result<(), Error> {
|
||||||
|
let keys = self.get_keys("encryption").await?;
|
||||||
|
|
||||||
|
// Check if the encryption keys match the announcement
|
||||||
|
if announcement.public_key() == keys.public_key() {
|
||||||
|
let mut device = self.device.write().await;
|
||||||
|
device.encryption_keys = Some(Arc::new(keys));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Not found"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request encryption keys from other clients
|
||||||
|
///
|
||||||
|
/// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
|
||||||
|
pub async fn request_encryption_keys(&self) -> Result<bool, Error> {
|
||||||
|
let mut wait_for_approval = false;
|
||||||
|
let device = self.device.read().await;
|
||||||
|
|
||||||
|
// Client Keys are always known at this point
|
||||||
|
let Some(client_keys) = device.client_keys.as_ref() else {
|
||||||
|
return Err(anyhow!("Client Keys is required"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let signer = self.client.signer().await?;
|
||||||
|
let public_key = signer.get_public_key().await?;
|
||||||
|
let client_pubkey = client_keys.get_public_key().await?;
|
||||||
|
|
||||||
|
// Get the encryption keys response from the database first
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kind(Kind::Custom(4455))
|
||||||
|
.author(public_key)
|
||||||
|
.pubkey(client_pubkey)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
match self.client.database().query(filter).await?.first_owned() {
|
||||||
|
// Found encryption keys that shared by other clients
|
||||||
|
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 = client_keys.nip44_decrypt(&root_device, payload).await?;
|
||||||
|
|
||||||
|
let secret = SecretKey::from_hex(&decrypted)?;
|
||||||
|
let keys = Keys::new(secret);
|
||||||
|
|
||||||
|
// No longer need to hold the reader for device
|
||||||
|
drop(device);
|
||||||
|
|
||||||
|
let mut device = self.device.write().await;
|
||||||
|
device.encryption_keys = Some(Arc::new(keys));
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// Construct encryption keys request event
|
||||||
|
let event = EventBuilder::new(Kind::Custom(4454), "")
|
||||||
|
.tags(vec![
|
||||||
|
Tag::client(app_name()),
|
||||||
|
Tag::custom(TagKind::custom("pubkey"), vec![client_pubkey]),
|
||||||
|
])
|
||||||
|
.sign(&signer)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Send a request for encryption keys from other devices
|
||||||
|
self.client.send_event(&event).await?;
|
||||||
|
|
||||||
|
// Create a unique ID to control the subscription later
|
||||||
|
let subscription_id = SubscriptionId::new("request");
|
||||||
|
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kind(Kind::Custom(4455))
|
||||||
|
.author(public_key)
|
||||||
|
.pubkey(client_pubkey)
|
||||||
|
.since(Timestamp::now());
|
||||||
|
|
||||||
|
// Subscribe to the approval response event
|
||||||
|
self.client
|
||||||
|
.subscribe_with_id(subscription_id, filter, None)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
wait_for_approval = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(wait_for_approval)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Receive the encryption keys from other clients
|
||||||
|
///
|
||||||
|
/// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
|
||||||
|
pub async fn receive_encryption_keys(&self, res: Response) -> Result<(), Error> {
|
||||||
|
let device = self.device.read().await;
|
||||||
|
|
||||||
|
// Client Keys are always known at this point
|
||||||
|
let Some(client_keys) = device.client_keys.as_ref() else {
|
||||||
|
return Err(anyhow!("Client Keys is required"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let public_key = res.public_key();
|
||||||
|
let payload = res.payload();
|
||||||
|
|
||||||
|
// Decrypt the payload using the client keys
|
||||||
|
let decrypted = client_keys.nip44_decrypt(&public_key, payload).await?;
|
||||||
|
let secret = SecretKey::parse(&decrypted)?;
|
||||||
|
let keys = Keys::new(secret);
|
||||||
|
|
||||||
|
// No longer need to hold the reader for device
|
||||||
|
drop(device);
|
||||||
|
|
||||||
|
let mut device = self.device.write().await;
|
||||||
|
device.encryption_keys = Some(Arc::new(keys));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Response the encryption keys request from other clients
|
||||||
|
///
|
||||||
|
/// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
|
||||||
|
pub async fn response_encryption_keys(&self, target: PublicKey) -> Result<(), Error> {
|
||||||
|
let device = self.device.read().await;
|
||||||
|
|
||||||
|
// Client Keys are always known at this point
|
||||||
|
let Some(client_keys) = device.client_keys.as_ref() else {
|
||||||
|
return Err(anyhow!("Client Keys is required"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let encryption = self.get_keys("encryption").await?;
|
||||||
|
let client_pubkey = client_keys.get_public_key().await?;
|
||||||
|
|
||||||
|
// Encrypt the encryption keys with the client's signer
|
||||||
|
let payload = client_keys
|
||||||
|
.nip44_encrypt(&target, &encryption.secret_key().to_secret_hex())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Construct the response event
|
||||||
|
//
|
||||||
|
// P tag: the current client's public key
|
||||||
|
// p tag: the requester's public key
|
||||||
|
let event = EventBuilder::new(Kind::Custom(4455), payload)
|
||||||
|
.tags(vec![
|
||||||
|
Tag::custom(TagKind::custom("P"), vec![client_pubkey]),
|
||||||
|
Tag::public_key(target),
|
||||||
|
])
|
||||||
|
.sign(client_keys)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Get the current user's signer and public key
|
||||||
|
let signer = self.client.signer().await?;
|
||||||
|
let public_key = signer.get_public_key().await?;
|
||||||
|
|
||||||
|
// Get the current user's relay list
|
||||||
|
let urls: Vec<RelayUrl> = self
|
||||||
|
.client
|
||||||
|
.database()
|
||||||
|
.relay_list(public_key)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(url, metadata)| {
|
||||||
|
if metadata.is_none() || metadata == Some(RelayMetadata::Read) {
|
||||||
|
Some(url)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Send the response event to the user's relay list
|
||||||
|
self.client.send_event_to(urls, &event).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Get and verify NIP-17 relays for a given public key
|
/// Get and verify NIP-17 relays for a given public key
|
||||||
pub async fn get_nip17(&self, public_key: PublicKey) -> Result<(), Error> {
|
pub async fn get_nip17(&self, public_key: PublicKey) -> Result<(), Error> {
|
||||||
let timeout = Duration::from_secs(TIMEOUT);
|
let timeout = Duration::from_secs(QUERY_TIMEOUT);
|
||||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||||
|
|
||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
@@ -685,33 +898,87 @@ impl AppState {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gets messaging relays for public key
|
||||||
|
pub async fn messaging_relays(&self, public_key: PublicKey) -> Vec<RelayUrl> {
|
||||||
|
let mut relay_urls = vec![];
|
||||||
|
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kind(Kind::InboxRelays)
|
||||||
|
.author(public_key)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if let Ok(events) = self.client.database().query(filter).await {
|
||||||
|
if let Some(event) = events.first_owned() {
|
||||||
|
let urls: Vec<RelayUrl> = nip17::extract_owned_relay_list(event).collect();
|
||||||
|
|
||||||
|
// Connect to relays
|
||||||
|
for url in urls.iter() {
|
||||||
|
self.client.add_relay(url).await.ok();
|
||||||
|
self.client.connect_relay(url).await.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
relay_urls.extend(urls.into_iter().take(3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
relay_urls
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Re-subscribes to gift wrap events
|
||||||
|
pub async fn resubscribe_messages(&self) -> Result<(), Error> {
|
||||||
|
let signer = self.client.signer().await?;
|
||||||
|
let public_key = signer.get_public_key().await?;
|
||||||
|
let urls = self.messaging_relays(public_key).await;
|
||||||
|
|
||||||
|
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
|
||||||
|
let id = SubscriptionId::new("inbox");
|
||||||
|
|
||||||
|
// Unsubscribe the previous subscription
|
||||||
|
self.client.unsubscribe(&id).await;
|
||||||
|
|
||||||
|
// Subscribe to gift wrap events
|
||||||
|
self.client
|
||||||
|
.subscribe_with_id_to(urls, id, filter, None)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Stores an unwrapped event in local database with reference to original
|
/// Stores an unwrapped event in local database with reference to original
|
||||||
async fn set_rumor(&self, id: EventId, rumor: &UnsignedEvent) -> Result<(), Error> {
|
async fn set_rumor(&self, id: EventId, rumor: &UnsignedEvent) -> Result<(), Error> {
|
||||||
let rumor_id = rumor
|
let rumor_id = rumor.id.context("Rumor is missing an event id")?;
|
||||||
.id
|
let author = rumor.pubkey;
|
||||||
.ok_or_else(|| anyhow!("Rumor is missing an event id"))?;
|
let conversation = self.conversation_id(rumor).to_string();
|
||||||
let author_hex = rumor.pubkey.to_hex();
|
|
||||||
let conversation = Self::conversation_id(rumor).to_string();
|
|
||||||
|
|
||||||
let mut tags = rumor.tags.clone().to_vec();
|
let mut tags = rumor.tags.clone().to_vec();
|
||||||
|
|
||||||
|
// Add a unique identifier
|
||||||
tags.push(Tag::identifier(id));
|
tags.push(Tag::identifier(id));
|
||||||
|
|
||||||
|
// Add a reference to the rumor's author
|
||||||
tags.push(Tag::custom(
|
tags.push(Tag::custom(
|
||||||
TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::A)),
|
TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::A)),
|
||||||
[author_hex],
|
[author],
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// Add a conversation id
|
||||||
tags.push(Tag::custom(
|
tags.push(Tag::custom(
|
||||||
TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::C)),
|
TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::C)),
|
||||||
[conversation],
|
[conversation],
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// Add a reference to the rumor's id
|
||||||
tags.push(Tag::event(rumor_id));
|
tags.push(Tag::event(rumor_id));
|
||||||
|
|
||||||
|
// Add references to the rumor's participants
|
||||||
for receiver in rumor.tags.public_keys().copied() {
|
for receiver in rumor.tags.public_keys().copied() {
|
||||||
tags.push(Tag::custom(
|
tags.push(Tag::custom(
|
||||||
TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::P)),
|
TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::P)),
|
||||||
[receiver.to_hex()],
|
[receiver],
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert rumor to json
|
||||||
let content = rumor.as_json();
|
let content = rumor.as_json();
|
||||||
|
|
||||||
let event = EventBuilder::new(Kind::ApplicationSpecificData, content)
|
let event = EventBuilder::new(Kind::ApplicationSpecificData, content)
|
||||||
@@ -739,34 +1006,55 @@ impl AppState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Unwraps a gift-wrapped event and processes its contents.
|
// Unwraps a gift-wrapped event and processes its contents.
|
||||||
async fn extract_rumor(&self, gift_wrap: &Event) {
|
async fn extract_rumor(&self, gift_wrap: &Event) -> Result<(), Error> {
|
||||||
let mut rumor: Option<UnsignedEvent> = None;
|
// Try to get cached rumor first
|
||||||
|
|
||||||
if let Ok(event) = self.get_rumor(gift_wrap.id).await {
|
if let Ok(event) = self.get_rumor(gift_wrap.id).await {
|
||||||
rumor = Some(event);
|
self.process_rumor(gift_wrap.id, event).await?;
|
||||||
} else if let Ok(unwrapped) = self.client.unwrap_gift_wrap(gift_wrap).await {
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to unwrap with the available signer
|
||||||
|
if let Ok(unwrapped) = self.try_unwrap_gift(gift_wrap).await {
|
||||||
let sender = unwrapped.sender;
|
let sender = unwrapped.sender;
|
||||||
let mut rumor_unsigned = unwrapped.rumor;
|
let mut rumor_unsigned = unwrapped.rumor;
|
||||||
|
|
||||||
if !Self::verify_rumor_sender(sender, &rumor_unsigned) {
|
if !self.verify_rumor_sender(sender, &rumor_unsigned) {
|
||||||
log::warn!(
|
return Err(anyhow!("Invalid rumor"));
|
||||||
"Ignoring gift wrap {}: seal pubkey {} mismatches rumor pubkey {}",
|
};
|
||||||
gift_wrap.id,
|
|
||||||
sender,
|
// Generate event id for the rumor if it doesn't have one
|
||||||
rumor_unsigned.pubkey
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
rumor_unsigned.ensure_id();
|
rumor_unsigned.ensure_id();
|
||||||
|
|
||||||
if let Err(e) = self.set_rumor(gift_wrap.id, &rumor_unsigned).await {
|
self.set_rumor(gift_wrap.id, &rumor_unsigned).await?;
|
||||||
log::warn!("Failed to cache unwrapped event: {e}")
|
self.process_rumor(gift_wrap.id, rumor_unsigned).await?;
|
||||||
} else {
|
|
||||||
rumor = Some(rumor_unsigned);
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to try unwrapping with different signers
|
||||||
|
async fn try_unwrap_gift(&self, gift_wrap: &Event) -> Result<UnwrappedGift, Error> {
|
||||||
|
// Try to unwrap with the device's encryption keys first
|
||||||
|
// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
|
||||||
|
if let Some(signer) = self.device.read().await.encryption_keys.as_ref() {
|
||||||
|
if let Ok(unwrapped) = UnwrappedGift::from_gift_wrap(signer, gift_wrap).await {
|
||||||
|
return Ok(unwrapped);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(event) = rumor {
|
// Try to unwrap with the user's signer
|
||||||
|
let signer = self.client.signer().await?;
|
||||||
|
if let Ok(unwrapped) = UnwrappedGift::from_gift_wrap(&signer, gift_wrap).await {
|
||||||
|
return Ok(unwrapped);
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(anyhow!("No signer available"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process a rumor event.
|
||||||
|
async fn process_rumor(&self, id: EventId, event: UnsignedEvent) -> Result<(), Error> {
|
||||||
// Send all pubkeys to the metadata batch to sync data
|
// Send all pubkeys to the metadata batch to sync data
|
||||||
for public_key in event.tags.public_keys().copied() {
|
for public_key in event.tags.public_keys().copied() {
|
||||||
self.ingester.send(public_key).await;
|
self.ingester.send(public_key).await;
|
||||||
@@ -775,53 +1063,92 @@ impl AppState {
|
|||||||
match event.created_at >= self.initialized_at {
|
match event.created_at >= self.initialized_at {
|
||||||
// New message: send a signal to notify the UI
|
// New message: send a signal to notify the UI
|
||||||
true => {
|
true => {
|
||||||
self.signal
|
self.signal.send(SignalKind::NewMessage((id, event))).await;
|
||||||
.send(SignalKind::NewMessage((gift_wrap.id, event)))
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
// Old message: Coop is probably processing the user's messages during initial load
|
// Old message: Coop is probably processing the user's messages during initial load
|
||||||
false => {
|
false => {
|
||||||
self.gift_wrap_processing.store(true, Ordering::Release);
|
self.gift_wrap_processing.store(true, Ordering::Release);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn conversation_id(rumor: &UnsignedEvent) -> u64 {
|
/// Get the conversation ID for a given rumor (message).
|
||||||
|
fn conversation_id(&self, rumor: &UnsignedEvent) -> u64 {
|
||||||
let mut hasher = DefaultHasher::new();
|
let mut hasher = DefaultHasher::new();
|
||||||
let mut pubkeys: Vec<PublicKey> = rumor.tags.public_keys().copied().collect();
|
let mut pubkeys: Vec<PublicKey> = rumor.tags.public_keys().copied().collect();
|
||||||
pubkeys.push(rumor.pubkey);
|
pubkeys.push(rumor.pubkey);
|
||||||
pubkeys.sort();
|
pubkeys.sort();
|
||||||
pubkeys.dedup();
|
pubkeys.dedup();
|
||||||
pubkeys.hash(&mut hasher);
|
pubkeys.hash(&mut hasher);
|
||||||
|
|
||||||
hasher.finish()
|
hasher.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verify_rumor_sender(sender: PublicKey, rumor: &UnsignedEvent) -> bool {
|
/// Verify that the sender of a rumor is the same as the sender of the event.
|
||||||
|
fn verify_rumor_sender(&self, sender: PublicKey, rumor: &UnsignedEvent) -> bool {
|
||||||
rumor.pubkey == sender
|
rumor.pubkey == sender
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extract an encryption keys announcement from an event.
|
||||||
|
fn extract_announcement(&self, event: &Event) -> Result<Announcement, Error> {
|
||||||
|
let public_key = event
|
||||||
|
.tags
|
||||||
|
.iter()
|
||||||
|
.find(|tag| tag.kind().as_str() == "n" || tag.kind().as_str() == "pubkey")
|
||||||
|
.and_then(|tag| tag.content())
|
||||||
|
.and_then(|c| PublicKey::parse(c).ok())
|
||||||
|
.context("Cannot parse public key from the event's tags")?;
|
||||||
|
|
||||||
|
let client_name = event
|
||||||
|
.tags
|
||||||
|
.find(TagKind::Client)
|
||||||
|
.and_then(|tag| tag.content())
|
||||||
|
.map(|c| c.to_string())
|
||||||
|
.context("Cannot parse client name from the event's tags")?;
|
||||||
|
|
||||||
|
Ok(Announcement::new(event.id, client_name, public_key))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract an encryption keys response from an event.
|
||||||
|
fn extract_response(&self, event: &Event) -> Result<Response, Error> {
|
||||||
|
let payload = event.content.clone();
|
||||||
|
let root_device = event
|
||||||
|
.tags
|
||||||
|
.find(TagKind::custom("P"))
|
||||||
|
.and_then(|tag| tag.content())
|
||||||
|
.and_then(|c| PublicKey::parse(c).ok())
|
||||||
|
.context("Cannot parse public key from the event's tags")?;
|
||||||
|
|
||||||
|
Ok(Response::new(payload, root_device))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::app_state;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn verify_rumor_sender_accepts_matching_sender() {
|
fn verify_rumor_sender_accepts_matching_sender() {
|
||||||
|
let state = app_state();
|
||||||
|
|
||||||
let keys = Keys::generate();
|
let keys = Keys::generate();
|
||||||
let public_key = keys.public_key();
|
let public_key = keys.public_key();
|
||||||
let rumor = EventBuilder::text_note("hello").build(public_key);
|
let rumor = EventBuilder::text_note("hello").build(public_key);
|
||||||
assert!(AppState::verify_rumor_sender(public_key, &rumor));
|
|
||||||
|
assert!(state.verify_rumor_sender(public_key, &rumor));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn verify_rumor_sender_rejects_mismatched_sender() {
|
fn verify_rumor_sender_rejects_mismatched_sender() {
|
||||||
|
let state = app_state();
|
||||||
|
|
||||||
let sender_keys = Keys::generate();
|
let sender_keys = Keys::generate();
|
||||||
let rumor_keys = Keys::generate();
|
let rumor_keys = Keys::generate();
|
||||||
let rumor = EventBuilder::text_note("spoof").build(rumor_keys.public_key());
|
let rumor = EventBuilder::text_note("spoof").build(rumor_keys.public_key());
|
||||||
assert!(!AppState::verify_rumor_sender(
|
|
||||||
sender_keys.public_key(),
|
assert!(!state.verify_rumor_sender(sender_keys.public_key(), &rumor));
|
||||||
&rumor
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
157
crates/states/src/state/signal.rs
Normal file
157
crates/states/src/state/signal.rs
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
use flume::{Receiver, Sender};
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct AuthRequest {
|
||||||
|
pub url: RelayUrl,
|
||||||
|
pub challenge: String,
|
||||||
|
pub sending: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthRequest {
|
||||||
|
pub fn new(challenge: impl Into<String>, url: RelayUrl) -> Self {
|
||||||
|
Self {
|
||||||
|
challenge: challenge.into(),
|
||||||
|
sending: false,
|
||||||
|
url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub enum UnwrappingStatus {
|
||||||
|
#[default]
|
||||||
|
Initialized,
|
||||||
|
Processing,
|
||||||
|
Complete,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct Announcement {
|
||||||
|
id: EventId,
|
||||||
|
client: String,
|
||||||
|
public_key: PublicKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Announcement {
|
||||||
|
pub fn new(id: EventId, client_name: String, public_key: PublicKey) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
client: client_name,
|
||||||
|
public_key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id(&self) -> EventId {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn public_key(&self) -> PublicKey {
|
||||||
|
self.public_key
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn client(&self) -> &str {
|
||||||
|
self.client.as_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct Response {
|
||||||
|
payload: String,
|
||||||
|
public_key: PublicKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Response {
|
||||||
|
pub fn new(payload: String, public_key: PublicKey) -> Self {
|
||||||
|
Self {
|
||||||
|
payload,
|
||||||
|
public_key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn public_key(&self) -> PublicKey {
|
||||||
|
self.public_key
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn payload(&self) -> &str {
|
||||||
|
self.payload.as_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signals sent through the global event channel to notify UI
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum SignalKind {
|
||||||
|
/// NIP-4e
|
||||||
|
///
|
||||||
|
/// A signal to notify UI that the user has not set encryption keys yet
|
||||||
|
EncryptionNotSet,
|
||||||
|
|
||||||
|
/// NIP-4e
|
||||||
|
///
|
||||||
|
/// A signal to notify UI that the user has set encryption keys
|
||||||
|
EncryptionSet(Announcement),
|
||||||
|
|
||||||
|
/// NIP-4e
|
||||||
|
///
|
||||||
|
/// A signal to notify UI that the user has responded to an encryption request
|
||||||
|
EncryptionResponse(Response),
|
||||||
|
|
||||||
|
/// NIP-4e
|
||||||
|
///
|
||||||
|
/// A signal to notify UI that the user has requested encryption keys from other devices
|
||||||
|
EncryptionRequest(Announcement),
|
||||||
|
|
||||||
|
/// A signal to notify UI that the client's signer has been set
|
||||||
|
SignerSet(PublicKey),
|
||||||
|
|
||||||
|
/// A signal to notify UI that the relay requires authentication
|
||||||
|
Auth(AuthRequest),
|
||||||
|
|
||||||
|
/// A signal to notify UI that a new profile has been received
|
||||||
|
NewProfile(Profile),
|
||||||
|
|
||||||
|
/// A signal to notify UI that a new gift wrap event has been received
|
||||||
|
NewMessage((EventId, UnsignedEvent)),
|
||||||
|
|
||||||
|
/// A signal to notify UI that no messaging relays for current user was found
|
||||||
|
MessagingRelaysNotFound,
|
||||||
|
|
||||||
|
/// A signal to notify UI that no gossip relays for current user was found
|
||||||
|
GossipRelaysNotFound,
|
||||||
|
|
||||||
|
/// A signal to notify UI that gift wrap status has changed
|
||||||
|
GiftWrapStatus(UnwrappingStatus),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Signal {
|
||||||
|
rx: Receiver<SignalKind>,
|
||||||
|
tx: Sender<SignalKind>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Signal {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Signal {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let (tx, rx) = flume::bounded::<SignalKind>(2048);
|
||||||
|
Self { rx, tx }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn receiver(&self) -> &Receiver<SignalKind> {
|
||||||
|
&self.rx
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sender(&self) -> &Sender<SignalKind> {
|
||||||
|
&self.tx
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send(&self, kind: SignalKind) {
|
||||||
|
if let Err(e) = self.tx.send_async(kind).await {
|
||||||
|
log::error!("Failed to send signal: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
crates/states/src/state/tracker.rs
Normal file
36
crates/states/src/state/tracker.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct EventTracker {
|
||||||
|
/// Tracking events that have been resent by Coop in the current session
|
||||||
|
pub resent_ids: Vec<Output<EventId>>,
|
||||||
|
|
||||||
|
/// Temporarily store events that need to be resent later
|
||||||
|
pub resend_queue: HashMap<EventId, RelayUrl>,
|
||||||
|
|
||||||
|
/// Tracking events sent by Coop in the current session
|
||||||
|
pub sent_ids: HashSet<EventId>,
|
||||||
|
|
||||||
|
/// Tracking events seen on which relays in the current session
|
||||||
|
pub seen_on_relays: HashMap<EventId, HashSet<RelayUrl>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventTracker {
|
||||||
|
pub fn resent_ids(&self) -> &Vec<Output<EventId>> {
|
||||||
|
&self.resent_ids
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resend_queue(&self) -> &HashMap<EventId, RelayUrl> {
|
||||||
|
&self.resend_queue
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sent_ids(&self) -> &HashSet<EventId> {
|
||||||
|
&self.sent_ids
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn seen_on_relays(&self) -> &HashMap<EventId, HashSet<RelayUrl>> {
|
||||||
|
&self.seen_on_relays
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -76,6 +76,7 @@ pub trait PopupMenuExt: Styled + Selectable + InteractiveElement + IntoElement +
|
|||||||
impl PopupMenuExt for Button {}
|
impl PopupMenuExt for Button {}
|
||||||
|
|
||||||
enum PopupMenuItem {
|
enum PopupMenuItem {
|
||||||
|
Title(SharedString),
|
||||||
Separator,
|
Separator,
|
||||||
Item {
|
Item {
|
||||||
icon: Option<Icon>,
|
icon: Option<Icon>,
|
||||||
@@ -314,6 +315,20 @@ impl PopupMenu {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add a title menu item
|
||||||
|
pub fn title(mut self, label: impl Into<SharedString>) -> Self {
|
||||||
|
if self.menu_items.is_empty() {
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(PopupMenuItem::Title(_)) = self.menu_items.last() {
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.menu_items.push(PopupMenuItem::Title(label.into()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Add a separator Menu Item
|
/// Add a separator Menu Item
|
||||||
pub fn separator(mut self) -> Self {
|
pub fn separator(mut self) -> Self {
|
||||||
if self.menu_items.is_empty() {
|
if self.menu_items.is_empty() {
|
||||||
@@ -588,6 +603,15 @@ impl Render for PopupMenu {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
match item {
|
match item {
|
||||||
|
PopupMenuItem::Title(label) => {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.font_semibold()
|
||||||
|
.text_color(cx.theme().text_muted)
|
||||||
|
.child(label.clone())
|
||||||
|
)
|
||||||
|
},
|
||||||
PopupMenuItem::Separator => this.h_auto().p_0().disabled(true).child(
|
PopupMenuItem::Separator => this.h_auto().p_0().disabled(true).child(
|
||||||
div()
|
div()
|
||||||
.rounded_none()
|
.rounded_none()
|
||||||
|
|||||||
@@ -59,6 +59,10 @@ common:
|
|||||||
en: "Use default"
|
en: "Use default"
|
||||||
configure:
|
configure:
|
||||||
en: "Configure"
|
en: "Configure"
|
||||||
|
hide:
|
||||||
|
en: "Hide"
|
||||||
|
reset:
|
||||||
|
en: "Reset"
|
||||||
|
|
||||||
keyring_disable:
|
keyring_disable:
|
||||||
label:
|
label:
|
||||||
@@ -74,6 +78,30 @@ keyring_disable:
|
|||||||
body_5:
|
body_5:
|
||||||
en: "By clicking continue, you agree to store your credentials as plain text."
|
en: "By clicking continue, you agree to store your credentials as plain text."
|
||||||
|
|
||||||
|
pending_encryption:
|
||||||
|
label:
|
||||||
|
en: "Wait for Approval"
|
||||||
|
body_1:
|
||||||
|
en: "Please open %{c} and approve the request for sharing encryption keys. Without access to them, Coop cannot decrypt your messages that are encrypted with encryption keys."
|
||||||
|
body_2:
|
||||||
|
en: "Or you can click the 'Reset' button to reset the encryption keys."
|
||||||
|
body_3:
|
||||||
|
en: "By resetting the encryption keys, you will not be able to view your messages that were encrypted with the old encryption keys."
|
||||||
|
|
||||||
|
request_encryption:
|
||||||
|
label:
|
||||||
|
en: "Encryption Keys Request"
|
||||||
|
body:
|
||||||
|
en: "You've requested for the encryption keys from:"
|
||||||
|
|
||||||
|
encryption:
|
||||||
|
notice:
|
||||||
|
en: "Encryption keys are being generated"
|
||||||
|
success:
|
||||||
|
en: "Encryption keys have been successfully set up"
|
||||||
|
reinit:
|
||||||
|
en: "Encryption keys are being reinitialized"
|
||||||
|
|
||||||
auto_update:
|
auto_update:
|
||||||
updating:
|
updating:
|
||||||
en: "Installing the new update..."
|
en: "Installing the new update..."
|
||||||
@@ -381,6 +409,8 @@ chat:
|
|||||||
en: "Sent Reports"
|
en: "Sent Reports"
|
||||||
nip17_not_found:
|
nip17_not_found:
|
||||||
en: "%{u} has not set up Messaging Relays, so they won't receive your message."
|
en: "%{u} has not set up Messaging Relays, so they won't receive your message."
|
||||||
|
device_not_found:
|
||||||
|
en: "You're sending with an encryption key, but %{u} has not set up an encryption key yet. Try sending with your identity instead."
|
||||||
|
|
||||||
sidebar:
|
sidebar:
|
||||||
reload_menu:
|
reload_menu:
|
||||||
|
|||||||
Reference in New Issue
Block a user