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 {
|
||||
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,
|
||||
_ => return SharedString::from("9999"),
|
||||
};
|
||||
@@ -85,7 +85,7 @@ impl RenderedTimestamp for Timestamp {
|
||||
}
|
||||
|
||||
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,
|
||||
_ => return SharedString::from("1m"),
|
||||
};
|
||||
|
||||
@@ -33,6 +33,7 @@ title_bar = { path = "../title_bar" }
|
||||
theme = { path = "../theme" }
|
||||
common = { path = "../common" }
|
||||
states = { path = "../states" }
|
||||
key_store = { path = "../key_store" }
|
||||
registry = { path = "../registry" }
|
||||
settings = { path = "../settings" }
|
||||
auto_update = { path = "../auto_update" }
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use std::sync::Mutex;
|
||||
|
||||
use gpui::{actions, App, AppContext};
|
||||
use key_store::backend::KeyItem;
|
||||
use key_store::KeyStore;
|
||||
use nostr_connect::prelude::*;
|
||||
use registry::keystore::KeyItem;
|
||||
use registry::Registry;
|
||||
use states::app_state;
|
||||
|
||||
@@ -48,7 +49,7 @@ pub fn load_embedded_fonts(cx: &App) {
|
||||
|
||||
pub fn reset(cx: &mut App) {
|
||||
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.background_spawn(async move {
|
||||
@@ -57,12 +58,12 @@ pub fn reset(cx: &mut App) {
|
||||
})
|
||||
.await;
|
||||
|
||||
keystore
|
||||
backend
|
||||
.delete_credentials(&KeyItem::User.to_string(), cx)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
keystore
|
||||
backend
|
||||
.delete_credentials(&KeyItem::Bunker.to_string(), cx)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Error};
|
||||
use auto_update::AutoUpdater;
|
||||
use common::display::RenderedProfile;
|
||||
use common::display::{shorten_pubkey, RenderedProfile};
|
||||
use common::event::EventUtils;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
@@ -14,14 +14,15 @@ use gpui::{
|
||||
};
|
||||
use i18n::{shared_t, t};
|
||||
use itertools::Itertools;
|
||||
use key_store::backend::KeyItem;
|
||||
use key_store::KeyStore;
|
||||
use nostr_connect::prelude::*;
|
||||
use nostr_sdk::prelude::*;
|
||||
use registry::keystore::KeyItem;
|
||||
use registry::{Registry, RegistryEvent};
|
||||
use settings::AppSettings;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
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 theme::{ActiveTheme, Theme, ThemeMode};
|
||||
use title_bar::TitleBar;
|
||||
@@ -74,7 +75,7 @@ pub struct ChatSpace {
|
||||
nip65_ready: bool,
|
||||
|
||||
/// All subscriptions for observing the app state
|
||||
_subscriptions: SmallVec<[Subscription; 4]>,
|
||||
_subscriptions: SmallVec<[Subscription; 3]>,
|
||||
|
||||
/// All long running tasks
|
||||
_tasks: SmallVec<[Task<()>; 5]>,
|
||||
@@ -83,7 +84,7 @@ pub struct ChatSpace {
|
||||
impl ChatSpace {
|
||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
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 dock = cx.new(|cx| DockArea::new(window, cx));
|
||||
@@ -100,21 +101,18 @@ impl ChatSpace {
|
||||
);
|
||||
|
||||
subscriptions.push(
|
||||
// Observe the keystore
|
||||
cx.observe_in(®istry, window, |this, registry, window, cx| {
|
||||
let has_keyring = registry.read(cx).initialized_keystore;
|
||||
let use_filestore = registry.read(cx).is_using_file_keystore();
|
||||
let not_logged_in = registry.read(cx).signer_pubkey().is_none();
|
||||
// Observe device changes
|
||||
cx.observe_in(&keystore, window, move |this, state, window, cx| {
|
||||
if state.read(cx).initialized {
|
||||
let backend = state.read(cx).backend();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if has_keyring && not_logged_in {
|
||||
let keystore = registry.read(cx).keystore();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let result = keystore
|
||||
let result = backend
|
||||
.read_credentials(&KeyItem::User.to_string(), cx)
|
||||
.await;
|
||||
|
||||
@@ -123,6 +121,7 @@ impl ChatSpace {
|
||||
Ok(Some((user, secret))) => {
|
||||
let public_key = PublicKey::parse(&user).unwrap();
|
||||
let secret = String::from_utf8(secret).unwrap();
|
||||
|
||||
this.set_account_layout(public_key, secret, window, cx);
|
||||
}
|
||||
_ => {
|
||||
@@ -134,23 +133,6 @@ impl ChatSpace {
|
||||
})
|
||||
.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);
|
||||
|
||||
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) => {
|
||||
// Close the latest modal if it exists
|
||||
window.close_modal(cx);
|
||||
// Close all opened modals
|
||||
window.close_all_modals(cx);
|
||||
|
||||
// Load user's settings
|
||||
settings.update(cx, |this, cx| {
|
||||
@@ -256,15 +250,6 @@ impl ChatSpace {
|
||||
// Setup the default layout for current workspace
|
||||
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) => {
|
||||
let url = &req.url;
|
||||
let auto_auth = AppSettings::get_auto_auth(cx);
|
||||
@@ -281,11 +266,20 @@ impl ChatSpace {
|
||||
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| {
|
||||
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) => {
|
||||
registry.update(cx, |this, 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>) {
|
||||
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) {
|
||||
let relays = default_nip65_relays();
|
||||
|
||||
@@ -937,15 +1143,15 @@ impl ChatSpace {
|
||||
_window: &mut Window,
|
||||
cx: &Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let registry = Registry::read_global(cx);
|
||||
let status = registry.unwrapping_status.read(cx);
|
||||
let registry = Registry::global(cx);
|
||||
let status = registry.read(cx).loading;
|
||||
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.h_6()
|
||||
.w_full()
|
||||
.child(compose_button())
|
||||
.when(status != &UnwrappingStatus::Complete, |this| {
|
||||
.when(status, |this| {
|
||||
this.child(deferred(
|
||||
h_flex()
|
||||
.px_2()
|
||||
|
||||
@@ -7,7 +7,7 @@ use gpui::{
|
||||
WindowOptions,
|
||||
};
|
||||
use states::app_state;
|
||||
use states::constants::{APP_ID, APP_NAME};
|
||||
use states::constants::{APP_ID, CLIENT_NAME};
|
||||
use ui::Root;
|
||||
|
||||
use crate::actions::{load_embedded_fonts, quit, Quit};
|
||||
@@ -63,7 +63,7 @@ fn main() {
|
||||
kind: WindowKind::Normal,
|
||||
app_id: Some(APP_ID.to_owned()),
|
||||
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))),
|
||||
appears_transparent: true,
|
||||
}),
|
||||
@@ -86,6 +86,9 @@ fn main() {
|
||||
// Initialize app registry
|
||||
registry::init(cx);
|
||||
|
||||
// Initialize backend for credentials storage
|
||||
key_store::init(cx);
|
||||
|
||||
// Initialize settings
|
||||
settings::init(cx);
|
||||
|
||||
|
||||
@@ -9,8 +9,9 @@ use gpui::{
|
||||
Window,
|
||||
};
|
||||
use i18n::{shared_t, t};
|
||||
use key_store::backend::KeyItem;
|
||||
use key_store::KeyStore;
|
||||
use nostr_connect::prelude::*;
|
||||
use registry::keystore::KeyItem;
|
||||
use registry::Registry;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use states::app_state;
|
||||
@@ -116,7 +117,7 @@ impl Account {
|
||||
window: &mut Window,
|
||||
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
|
||||
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 common::display::{RenderedProfile, RenderedTimestamp};
|
||||
@@ -17,7 +17,7 @@ use indexset::{BTreeMap, BTreeSet};
|
||||
use itertools::Itertools;
|
||||
use nostr_sdk::prelude::*;
|
||||
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 serde::Deserialize;
|
||||
use settings::AppSettings;
|
||||
@@ -47,6 +47,10 @@ mod subject;
|
||||
#[action(namespace = chat, no_json)]
|
||||
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> {
|
||||
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 {
|
||||
// Chat Room
|
||||
room: Entity<Room>,
|
||||
relays: Entity<HashMap<PublicKey, Vec<RelayUrl>>>,
|
||||
|
||||
// Messages
|
||||
list_state: ListState,
|
||||
@@ -64,6 +67,7 @@ pub struct Chat {
|
||||
|
||||
// New Message
|
||||
input: Entity<InputState>,
|
||||
options: Entity<SendOptions>,
|
||||
replies_to: Entity<HashSet<EventId>>,
|
||||
|
||||
// Media Attachment
|
||||
@@ -75,20 +79,12 @@ pub struct Chat {
|
||||
focus_handle: FocusHandle,
|
||||
image_cache: Entity<RetainAllImageCache>,
|
||||
|
||||
_subscriptions: SmallVec<[Subscription; 4]>,
|
||||
_subscriptions: SmallVec<[Subscription; 3]>,
|
||||
_tasks: SmallVec<[Task<()>; 2]>,
|
||||
}
|
||||
|
||||
impl Chat {
|
||||
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| {
|
||||
InputState::new(window, cx)
|
||||
.placeholder(t!("chat.placeholder"))
|
||||
@@ -97,11 +93,16 @@ impl Chat {
|
||||
.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 list_state = ListState::new(messages.len(), ListAlignment::Bottom, px(1024.));
|
||||
|
||||
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 tasks = smallvec![];
|
||||
@@ -109,7 +110,7 @@ impl Chat {
|
||||
tasks.push(
|
||||
// Load all messages belonging to this room
|
||||
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| {
|
||||
match result {
|
||||
@@ -126,24 +127,11 @@ impl Chat {
|
||||
);
|
||||
|
||||
tasks.push(
|
||||
// Get messaging relays for all members
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let result = connect.await;
|
||||
|
||||
this.update_in(cx, |this, _window, cx| {
|
||||
match result {
|
||||
Ok(relays) => {
|
||||
this.relays.update(cx, |this, cx| {
|
||||
this.extend(relays);
|
||||
cx.notify();
|
||||
});
|
||||
// Get messaging relays and encryption keys announcement for all members
|
||||
cx.background_spawn(async move {
|
||||
if let Err(e) = connect.await {
|
||||
log::error!("Failed to initialize room: {e}");
|
||||
}
|
||||
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(
|
||||
// Observe when user close chat panel
|
||||
cx.on_release_in(window, move |this, window, cx| {
|
||||
@@ -219,19 +190,19 @@ impl Chat {
|
||||
);
|
||||
|
||||
Self {
|
||||
id: room.read(cx).id.to_string().into(),
|
||||
image_cache: RetainAllImageCache::new(cx),
|
||||
focus_handle: cx.focus_handle(),
|
||||
rendered_texts_by_id: BTreeMap::new(),
|
||||
reports_by_id: BTreeMap::new(),
|
||||
relays,
|
||||
id,
|
||||
messages,
|
||||
room,
|
||||
list_state,
|
||||
input,
|
||||
replies_to,
|
||||
attachments,
|
||||
options,
|
||||
rendered_texts_by_id: BTreeMap::new(),
|
||||
reports_by_id: BTreeMap::new(),
|
||||
uploading: false,
|
||||
image_cache: RetainAllImageCache::new(cx),
|
||||
focus_handle: cx.focus_handle(),
|
||||
_subscriptions: subscriptions,
|
||||
_tasks: tasks,
|
||||
}
|
||||
@@ -239,12 +210,12 @@ impl Chat {
|
||||
|
||||
/// Load all messages belonging to this room
|
||||
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(
|
||||
// Run the task in the background
|
||||
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| {
|
||||
match result {
|
||||
@@ -303,9 +274,6 @@ impl Chat {
|
||||
this.set_value("", window, cx);
|
||||
});
|
||||
|
||||
// Get the backup setting
|
||||
let backup = AppSettings::get_backup_messages(cx);
|
||||
|
||||
// Get replies_to if it's present
|
||||
let replies: Vec<EventId> = self.replies_to.read(cx).iter().copied().collect();
|
||||
|
||||
@@ -317,14 +285,17 @@ impl Chat {
|
||||
let rumor_id = rumor.id.unwrap();
|
||||
|
||||
// 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
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(100))
|
||||
.await;
|
||||
let delay = Duration::from_millis(100);
|
||||
|
||||
// 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.insert_message(Message::user(rumor), true, cx);
|
||||
this.remove_all_replies(cx);
|
||||
@@ -339,6 +310,7 @@ impl Chat {
|
||||
})
|
||||
.detach();
|
||||
|
||||
self._tasks.push(
|
||||
// Continue sending the message in the background
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let result = send_message.await;
|
||||
@@ -346,10 +318,11 @@ impl Chat {
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
match result {
|
||||
Ok(reports) => {
|
||||
// Update room's status
|
||||
this.room.update(cx, |this, cx| {
|
||||
if this.kind != RoomKind::Ongoing {
|
||||
// Update the room kind to ongoing
|
||||
// But keep the room kind if send failed
|
||||
// Update the room kind to ongoing,
|
||||
// but keep the room kind if send failed
|
||||
if reports.iter().all(|r| !r.is_sent_success()) {
|
||||
this.kind = RoomKind::Ongoing;
|
||||
cx.notify();
|
||||
@@ -368,8 +341,8 @@ impl Chat {
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/// Resend a failed message
|
||||
@@ -432,6 +405,7 @@ impl Chat {
|
||||
}
|
||||
|
||||
/// Insert a warning message into the chat panel
|
||||
#[allow(dead_code)]
|
||||
fn insert_warning(&mut self, content: impl Into<String>, cx: &mut Context<Self>) {
|
||||
let m = Message::warning(content.into());
|
||||
self.insert_message(m, true, cx);
|
||||
@@ -473,6 +447,10 @@ impl Chat {
|
||||
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) {
|
||||
if let Some(ix) = self.messages.iter().position(|m| {
|
||||
if let Message::User(msg) = m {
|
||||
@@ -543,30 +521,25 @@ impl Chat {
|
||||
})
|
||||
.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)) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.add_attachment(url, cx);
|
||||
this.set_uploading(false, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Ok(None) => {
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
window.push_notification("Failed to upload file", cx);
|
||||
this.set_uploading(false, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(e) => {
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
window.push_notification(Notification::error(e.to_string()), cx);
|
||||
this.set_uploading(false, cx);
|
||||
}
|
||||
};
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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| {
|
||||
this.child(
|
||||
h_flex()
|
||||
@@ -1291,6 +1285,13 @@ impl Chat {
|
||||
})
|
||||
.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 {
|
||||
@@ -1334,8 +1335,11 @@ impl Focusable for Chat {
|
||||
|
||||
impl Render for Chat {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let kind = self.signer_kind(cx);
|
||||
|
||||
v_flex()
|
||||
.on_action(cx.listener(Self::on_open_seen_on))
|
||||
.on_action(cx.listener(Self::on_set_encryption))
|
||||
.image_cache(self.image_cache.clone())
|
||||
.size_full()
|
||||
.child(
|
||||
@@ -1384,9 +1388,7 @@ impl Render for Chat {
|
||||
.items_end()
|
||||
.gap_2p5()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(
|
||||
@@ -1408,7 +1410,31 @@ impl Render for Chat {
|
||||
.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,
|
||||
};
|
||||
use i18n::{shared_t, t};
|
||||
use key_store::backend::KeyItem;
|
||||
use key_store::KeyStore;
|
||||
use nostr_connect::prelude::*;
|
||||
use registry::keystore::KeyItem;
|
||||
use registry::Registry;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use states::app_state;
|
||||
use states::constants::BUNKER_TIMEOUT;
|
||||
@@ -174,7 +174,7 @@ impl Login {
|
||||
window: &mut Window,
|
||||
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 secret = keys.secret_key().to_secret_bytes();
|
||||
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>) {
|
||||
let keystore = Registry::global(cx).read(cx).keystore();
|
||||
let keystore = KeyStore::global(cx).read(cx).backend();
|
||||
let username = keys.public_key().to_hex();
|
||||
let secret = keys.secret_key().to_secret_hex().into_bytes();
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ use gpui::{
|
||||
};
|
||||
use gpui_tokio::Tokio;
|
||||
use i18n::{shared_t, t};
|
||||
use key_store::backend::KeyItem;
|
||||
use key_store::KeyStore;
|
||||
use nostr_sdk::prelude::*;
|
||||
use registry::keystore::KeyItem;
|
||||
use registry::Registry;
|
||||
use settings::AppSettings;
|
||||
use smol::fs;
|
||||
use states::constants::BOOTSTRAP_RELAYS;
|
||||
@@ -106,7 +106,7 @@ impl NewAccount {
|
||||
}
|
||||
|
||||
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 username = keys.public_key().to_hex();
|
||||
|
||||
@@ -9,12 +9,12 @@ use gpui::{
|
||||
SharedString, StatefulInteractiveElement, Styled, Task, Window,
|
||||
};
|
||||
use i18n::{shared_t, t};
|
||||
use key_store::backend::KeyItem;
|
||||
use key_store::KeyStore;
|
||||
use nostr_connect::prelude::*;
|
||||
use registry::keystore::KeyItem;
|
||||
use registry::Registry;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
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 ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
@@ -81,7 +81,7 @@ impl Onboarding {
|
||||
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
|
||||
|
||||
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();
|
||||
|
||||
// NIP46: https://github.com/nostr-protocol/nips/blob/master/46.md
|
||||
@@ -126,7 +126,7 @@ impl Onboarding {
|
||||
window: &mut Window,
|
||||
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 secret = self.app_keys.secret_key().to_secret_bytes();
|
||||
let mut clean_uri = uri.to_string();
|
||||
|
||||
@@ -52,8 +52,8 @@ impl RoomListItem {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn public_key(mut self, public_key: PublicKey) -> Self {
|
||||
self.public_key = Some(public_key);
|
||||
pub fn public_key(mut self, public_key: &PublicKey) -> Self {
|
||||
self.public_key = Some(public_key.to_owned());
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ use settings::AppSettings;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use states::app_state;
|
||||
use states::constants::{BOOTSTRAP_RELAYS, SEARCH_RELAYS};
|
||||
use states::state::UnwrappingStatus;
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
@@ -627,7 +626,7 @@ impl Sidebar {
|
||||
.name(this.display_name(cx))
|
||||
.avatar(this.display_image(proxy, cx))
|
||||
.created_at(this.created_at.to_ago())
|
||||
.public_key(this.members[0])
|
||||
.public_key(this.members.iter().nth(0).unwrap().0)
|
||||
.kind(this.kind)
|
||||
.on_click(handler),
|
||||
)
|
||||
@@ -669,7 +668,7 @@ impl Focusable for Sidebar {
|
||||
impl Render for Sidebar {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
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
|
||||
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 {
|
||||
User,
|
||||
Bunker,
|
||||
Client,
|
||||
Encryption,
|
||||
}
|
||||
|
||||
impl Display for KeyItem {
|
||||
@@ -23,8 +21,6 @@ impl Display for KeyItem {
|
||||
match self {
|
||||
Self::User => write!(f, "coop-user"),
|
||||
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;
|
||||
|
||||
/// 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.
|
||||
pub struct KeyringProvider;
|
||||
|
||||
impl KeyStore for KeyringProvider {
|
||||
impl KeyBackend for KeyringProvider {
|
||||
fn name(&self) -> &str {
|
||||
"keyring"
|
||||
}
|
||||
@@ -139,7 +135,7 @@ impl Default for FileProvider {
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyStore for FileProvider {
|
||||
impl KeyBackend for FileProvider {
|
||||
fn name(&self) -> &str {
|
||||
"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
|
||||
nostr.workspace = true
|
||||
nostr-sdk.workspace = true
|
||||
nostr-lmdb.workspace = true
|
||||
anyhow.workspace = true
|
||||
itertools.workspace = true
|
||||
smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
log.workspace = true
|
||||
flume.workspace = true
|
||||
futures.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
fuzzy-matcher = "0.3.7"
|
||||
rustls = "0.23.23"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
use anyhow::Error;
|
||||
use common::event::EventUtils;
|
||||
@@ -14,19 +13,12 @@ use room::RoomKind;
|
||||
use settings::AppSettings;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use states::app_state;
|
||||
use states::constants::KEYRING_URL;
|
||||
use states::state::UnwrappingStatus;
|
||||
|
||||
use crate::keystore::{FileProvider, KeyStore, KeyringProvider};
|
||||
use crate::room::Room;
|
||||
|
||||
pub mod keystore;
|
||||
pub mod message;
|
||||
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) {
|
||||
Registry::set_global(cx.new(Registry::new), cx);
|
||||
}
|
||||
@@ -49,14 +41,8 @@ pub struct Registry {
|
||||
/// Collection of all persons (user profiles)
|
||||
pub persons: HashMap<PublicKey, Entity<Profile>>,
|
||||
|
||||
/// Status of the unwrapping process
|
||||
pub unwrapping_status: Entity<UnwrappingStatus>,
|
||||
|
||||
/// Key Store for storing credentials
|
||||
pub keystore: Arc<dyn KeyStore>,
|
||||
|
||||
/// Whether the keystore has been initialized
|
||||
pub initialized_keystore: bool,
|
||||
/// Loading status of the registry
|
||||
pub loading: bool,
|
||||
|
||||
/// Public Key of the currently activated signer
|
||||
signer_pubkey: Option<PublicKey>,
|
||||
@@ -85,39 +71,8 @@ impl Registry {
|
||||
|
||||
/// Create a new registry instance
|
||||
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![];
|
||||
|
||||
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(
|
||||
// Load all user profiles from the database
|
||||
cx.spawn(async move |this, cx| {
|
||||
@@ -136,12 +91,10 @@ impl Registry {
|
||||
);
|
||||
|
||||
Self {
|
||||
unwrapping_status,
|
||||
keystore,
|
||||
initialized_keystore,
|
||||
rooms: vec![],
|
||||
persons: HashMap::new(),
|
||||
signer_pubkey: None,
|
||||
loading: true,
|
||||
_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.
|
||||
pub fn signer_pubkey(&self) -> Option<PublicKey> {
|
||||
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.
|
||||
pub fn room(&self, id: &u64, cx: &App) -> Option<Entity<Room>> {
|
||||
self.rooms
|
||||
@@ -297,24 +245,13 @@ impl Registry {
|
||||
pub fn search_by_public_key(&self, public_key: PublicKey, cx: &App) -> Vec<Entity<Room>> {
|
||||
self.rooms
|
||||
.iter()
|
||||
.filter(|room| room.read(cx).members.contains(&public_key))
|
||||
.filter(|room| room.read(cx).members.contains_key(&public_key))
|
||||
.cloned()
|
||||
.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.
|
||||
pub fn reset(&mut self, cx: &mut Context<Self>) {
|
||||
// Reset the unwrapping status
|
||||
self.set_unwrapping_status(UnwrappingStatus::default(), cx);
|
||||
|
||||
// Clear the current identity
|
||||
self.signer_pubkey = None;
|
||||
|
||||
@@ -339,10 +276,7 @@ impl Registry {
|
||||
|
||||
let authored_filter = Filter::new()
|
||||
.kind(Kind::ApplicationSpecificData)
|
||||
.custom_tag(
|
||||
SingleLetterTag::lowercase(Alphabet::A),
|
||||
public_key.to_hex(),
|
||||
);
|
||||
.custom_tag(SingleLetterTag::lowercase(Alphabet::A), public_key);
|
||||
|
||||
let addressed_filter = Filter::new()
|
||||
.kind(Kind::ApplicationSpecificData)
|
||||
|
||||
@@ -7,20 +7,57 @@ use anyhow::{anyhow, Error};
|
||||
use common::display::RenderedProfile;
|
||||
use common::event::EventUtils;
|
||||
use gpui::{App, AppContext, Context, EventEmitter, SharedString, SharedUri, Task};
|
||||
use itertools::Itertools;
|
||||
use nostr_sdk::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use states::app_state;
|
||||
use states::constants::SEND_RETRY;
|
||||
|
||||
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)]
|
||||
pub struct SendReport {
|
||||
pub receiver: PublicKey,
|
||||
|
||||
pub status: Option<Output<EventId>>,
|
||||
pub error: Option<SharedString>,
|
||||
pub on_hold: Option<Event>,
|
||||
|
||||
pub relays_not_found: bool,
|
||||
pub device_not_found: bool,
|
||||
|
||||
pub on_hold: Option<Event>,
|
||||
}
|
||||
|
||||
impl SendReport {
|
||||
@@ -31,18 +68,17 @@ impl SendReport {
|
||||
error: None,
|
||||
on_hold: None,
|
||||
relays_not_found: false,
|
||||
device_not_found: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn status(mut self, output: Output<EventId>) -> Self {
|
||||
self.status = Some(output);
|
||||
self.relays_not_found = false;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn error(mut self, error: impl Into<SharedString>) -> Self {
|
||||
self.error = Some(error.into());
|
||||
self.relays_not_found = false;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -51,11 +87,16 @@ impl SendReport {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn not_found(mut self) -> Self {
|
||||
pub fn relays_not_found(mut self) -> Self {
|
||||
self.relays_not_found = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn device_not_found(mut self) -> Self {
|
||||
self.device_not_found = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_relay_error(&self) -> bool {
|
||||
self.error.is_some() || self.relays_not_found
|
||||
}
|
||||
@@ -82,6 +123,8 @@ pub enum RoomKind {
|
||||
Request,
|
||||
}
|
||||
|
||||
type DevicePublicKey = PublicKey;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Room {
|
||||
pub id: u64,
|
||||
@@ -89,7 +132,7 @@ pub struct Room {
|
||||
/// Subject of the room
|
||||
pub subject: Option<String>,
|
||||
/// All members of the room
|
||||
pub members: Vec<PublicKey>,
|
||||
pub members: HashMap<PublicKey, Option<DevicePublicKey>>,
|
||||
/// Kind
|
||||
pub kind: RoomKind,
|
||||
}
|
||||
@@ -128,7 +171,11 @@ impl From<&Event> for Room {
|
||||
let created_at = val.created_at;
|
||||
|
||||
// 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
|
||||
let subject = val
|
||||
@@ -152,7 +199,11 @@ impl From<&UnsignedEvent> for Room {
|
||||
let created_at = val.created_at;
|
||||
|
||||
// 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
|
||||
let subject = val
|
||||
@@ -233,8 +284,8 @@ impl Room {
|
||||
}
|
||||
|
||||
/// Returns the members of the room
|
||||
pub fn members(&self) -> &Vec<PublicKey> {
|
||||
&self.members
|
||||
pub fn members(&self) -> Vec<PublicKey> {
|
||||
self.members.keys().cloned().collect()
|
||||
}
|
||||
|
||||
/// 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.
|
||||
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() {
|
||||
for member in self.members() {
|
||||
if member != &public_key {
|
||||
return registry.get_person(member, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
let target_member = self
|
||||
.members
|
||||
.keys()
|
||||
.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.
|
||||
@@ -284,7 +335,7 @@ impl Room {
|
||||
if self.is_group() {
|
||||
let profiles: Vec<Profile> = self
|
||||
.members
|
||||
.iter()
|
||||
.keys()
|
||||
.map(|public_key| registry.get_person(public_key, cx))
|
||||
.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
|
||||
pub fn emit_message(&self, gift_wrap_id: EventId, event: UnsignedEvent, cx: &mut Context<Self>) {
|
||||
cx.emit(RoomSignal::NewMessage((gift_wrap_id, event)));
|
||||
pub fn emit_message(&self, id: EventId, event: UnsignedEvent, cx: &mut Context<Self>) {
|
||||
cx.emit(RoomSignal::NewMessage((id, event)));
|
||||
}
|
||||
|
||||
/// Emits a signal to refresh the current room's messages.
|
||||
@@ -397,9 +366,69 @@ impl Room {
|
||||
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)
|
||||
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 mut tags = vec![];
|
||||
@@ -407,7 +436,7 @@ impl Room {
|
||||
// Add 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()));
|
||||
}
|
||||
|
||||
@@ -447,34 +476,42 @@ impl Room {
|
||||
/// Create a task to send a message to all room members
|
||||
pub fn send_message(
|
||||
&self,
|
||||
rumor: UnsignedEvent,
|
||||
backup: bool,
|
||||
rumor: &UnsignedEvent,
|
||||
opts: &SendOptions,
|
||||
cx: &App,
|
||||
) -> Task<Result<Vec<SendReport>, Error>> {
|
||||
let mut members = self.members.clone();
|
||||
let rumor = rumor.to_owned();
|
||||
let opts = opts.to_owned();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let states = app_state();
|
||||
let client = states.client();
|
||||
let signer = client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
let device = states.device.read().await.encryption_keys.clone();
|
||||
|
||||
let user_signer = client.signer().await?;
|
||||
let user_pubkey = user_signer.get_public_key().await?;
|
||||
|
||||
// Collect relay hints for all participants (including current user)
|
||||
let mut participants = members.clone();
|
||||
if !participants.contains(&public_key) {
|
||||
participants.push(public_key);
|
||||
let mut participants: Vec<PublicKey> = members.keys().cloned().collect();
|
||||
|
||||
if !participants.contains(&user_pubkey) {
|
||||
participants.push(user_pubkey);
|
||||
}
|
||||
|
||||
// Initialize relay cache
|
||||
let mut relay_cache: HashMap<PublicKey, Vec<RelayUrl>> = HashMap::new();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Update rumor with relay hints for each receiver
|
||||
let mut rumor = rumor;
|
||||
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() {
|
||||
match standard {
|
||||
TagStandard::PublicKey {
|
||||
@@ -483,18 +520,18 @@ impl Room {
|
||||
uppercase,
|
||||
..
|
||||
} => {
|
||||
let relay_url =
|
||||
relay_cache
|
||||
let relay_url = relay_cache
|
||||
.get(&public_key)
|
||||
.and_then(|urls| urls.first().cloned());
|
||||
|
||||
let updated = TagStandard::PublicKey {
|
||||
public_key,
|
||||
relay_url,
|
||||
alias,
|
||||
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),
|
||||
}
|
||||
@@ -506,29 +543,42 @@ impl Room {
|
||||
|
||||
// Remove the current user's public key from the list of receivers
|
||||
// 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![];
|
||||
|
||||
for receiver in members.into_iter() {
|
||||
let rumor = rumor.clone();
|
||||
let event = EventBuilder::gift_wrap(&signer, &receiver, rumor, vec![]).await?;
|
||||
for (receiver, device_pubkey) in members.into_iter() {
|
||||
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() {
|
||||
reports.push(SendReport::new(receiver).not_found());
|
||||
reports.push(SendReport::new(receiver).relays_not_found());
|
||||
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
|
||||
match client.send_event_to(urls, &event).await {
|
||||
Ok(output) => {
|
||||
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);
|
||||
|
||||
if auth_required {
|
||||
if auth {
|
||||
// Wait for authenticated and resent event successfully
|
||||
for attempt in 0..=SEND_RETRY {
|
||||
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
|
||||
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
|
||||
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();
|
||||
|
||||
// Check if there are any relays to send the event to
|
||||
if urls.is_empty() {
|
||||
reports.push(SendReport::new(public_key).not_found());
|
||||
reports.push(SendReport::new(public_key).relays_not_found());
|
||||
} else {
|
||||
// Send the event to the messaging relays
|
||||
match client.send_event_to(urls, &event).await {
|
||||
@@ -596,7 +647,8 @@ impl Room {
|
||||
cx: &App,
|
||||
) -> Task<Result<Vec<SendReport>, Error>> {
|
||||
cx.background_spawn(async move {
|
||||
let client = app_state().client();
|
||||
let states = app_state();
|
||||
let client = states.client();
|
||||
let mut resend_reports = vec![];
|
||||
|
||||
for report in reports.into_iter() {
|
||||
@@ -625,11 +677,11 @@ impl Room {
|
||||
|
||||
// Process the on hold event if it exists
|
||||
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
|
||||
if urls.is_empty() {
|
||||
resend_reports.push(SendReport::new(receiver).not_found());
|
||||
resend_reports.push(SendReport::new(receiver).relays_not_found());
|
||||
} else {
|
||||
// Send the event to the messaging relays
|
||||
match client.send_event_to(urls, &event).await {
|
||||
@@ -648,36 +700,24 @@ impl Room {
|
||||
})
|
||||
}
|
||||
|
||||
/// Gets messaging relays for public key
|
||||
async fn messaging_relays(public_key: PublicKey) -> Vec<RelayUrl> {
|
||||
let client = app_state().client();
|
||||
let mut relay_urls = vec![];
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::InboxRelays)
|
||||
.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();
|
||||
fn select_signer<T>(kind: &SignerKind, device: Option<T>, user: T) -> Result<T, Error>
|
||||
where
|
||||
T: NostrSigner,
|
||||
{
|
||||
match kind {
|
||||
SignerKind::Encryption => {
|
||||
Ok(device.ok_or_else(|| anyhow!("No encryption keys found"))?)
|
||||
}
|
||||
|
||||
relay_urls.extend(urls.into_iter().take(3).unique());
|
||||
SignerKind::User => Ok(user),
|
||||
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]
|
||||
nostr-sdk.workspace = true
|
||||
nostr-lmdb.workspace = true
|
||||
|
||||
dirs.workspace = true
|
||||
smol.workspace = true
|
||||
flume.workspace = true
|
||||
log.workspace = true
|
||||
anyhow.workspace = true
|
||||
|
||||
whoami = "1.5.2"
|
||||
whoami = "1.6.1"
|
||||
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_PUBKEY: &str = "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDc4MkNFRkQ2RkVGQURGNzUKUldSMTMvcisxdThzZUZraHc4Vno3NVNJek81VkJFUEV3MkJweGFxQXhpekdSU1JIekpqMG4yemMK";
|
||||
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";
|
||||
|
||||
/// Bootstrap Relays.
|
||||
@@ -33,6 +32,9 @@ pub const NOSTR_CONNECT_TIMEOUT: u64 = 200;
|
||||
/// Default timeout (in seconds) for Nostr Connect (Bunker)
|
||||
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.
|
||||
pub const METADATA_BATCH_LIMIT: usize = 100;
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use nostr_sdk::prelude::*;
|
||||
use whoami::{devicename, platform};
|
||||
|
||||
use crate::constants::CLIENT_NAME;
|
||||
use crate::state::AppState;
|
||||
|
||||
pub mod constants;
|
||||
@@ -9,6 +11,7 @@ pub mod paths;
|
||||
pub mod state;
|
||||
|
||||
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 NIP17_RELAYS: OnceLock<Vec<RelayUrl>> = OnceLock::new();
|
||||
|
||||
@@ -17,6 +20,15 @@ pub fn app_state() -> &'static AppState {
|
||||
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
|
||||
pub fn default_nip65_relays() -> &'static Vec<(RelayUrl, Option<RelayMetadata>)> {
|
||||
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::collections::{hash_map::DefaultHasher, HashMap, HashSet};
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::collections::HashSet;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Error};
|
||||
use flume::{Receiver, Sender};
|
||||
use anyhow::{anyhow, Context, Error};
|
||||
use nostr_lmdb::NostrLMDB;
|
||||
use nostr_sdk::prelude::*;
|
||||
use smol::lock::RwLock;
|
||||
|
||||
use crate::app_name;
|
||||
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::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 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
|
||||
}
|
||||
}
|
||||
pub use signal::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AppState {
|
||||
@@ -179,6 +41,9 @@ pub struct AppState {
|
||||
/// Ingester channel for processing public keys
|
||||
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.
|
||||
pub initialized_at: Timestamp,
|
||||
|
||||
@@ -213,6 +78,7 @@ impl AppState {
|
||||
});
|
||||
|
||||
let client = ClientBuilder::default().database(lmdb).opts(opts).build();
|
||||
let device = RwLock::new(Device::default());
|
||||
let event_tracker = RwLock::new(EventTracker::default());
|
||||
|
||||
let signal = Signal::default();
|
||||
@@ -220,6 +86,7 @@ impl AppState {
|
||||
|
||||
Self {
|
||||
client,
|
||||
device,
|
||||
event_tracker,
|
||||
signal,
|
||||
ingester,
|
||||
@@ -233,6 +100,11 @@ impl AppState {
|
||||
&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
|
||||
pub fn tracker(&'static self) -> &'static RwLock<EventTracker> {
|
||||
&self.event_tracker
|
||||
@@ -262,7 +134,10 @@ impl AppState {
|
||||
// Get user's gossip relays
|
||||
self.get_nip65(pk).await.ok();
|
||||
|
||||
// Exit the current loop
|
||||
// Initialize client keys
|
||||
self.init_client_keys().await.ok();
|
||||
|
||||
// Exit the loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -355,6 +230,36 @@ impl AppState {
|
||||
}
|
||||
|
||||
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 => {
|
||||
// Get events if relay list belongs to current user
|
||||
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}");
|
||||
}
|
||||
|
||||
// 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
|
||||
if let Err(e) = self.get_nip17(author).await {
|
||||
log::error!("Failed to fetch messaging relays event: {e}");
|
||||
@@ -404,7 +314,9 @@ impl AppState {
|
||||
self.signal.send(SignalKind::NewProfile(profile)).await;
|
||||
}
|
||||
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, ..
|
||||
} => {
|
||||
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
|
||||
event_tracker.sent_ids.insert(event_id);
|
||||
tracker.sent_ids.insert(event_id);
|
||||
|
||||
// Keep track of events that need to be resend after auth
|
||||
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
|
||||
async fn is_self_authored(&self, event: &Event) -> Result<bool, Error> {
|
||||
let signer = self.client.signer().await?;
|
||||
@@ -552,7 +505,7 @@ impl AppState {
|
||||
|
||||
/// Get and verify NIP-65 relays for a given public key
|
||||
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 filter = Filter::new()
|
||||
@@ -608,9 +561,269 @@ impl AppState {
|
||||
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
|
||||
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 filter = Filter::new()
|
||||
@@ -685,33 +898,87 @@ impl AppState {
|
||||
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
|
||||
async fn set_rumor(&self, id: EventId, rumor: &UnsignedEvent) -> Result<(), Error> {
|
||||
let rumor_id = rumor
|
||||
.id
|
||||
.ok_or_else(|| anyhow!("Rumor is missing an event id"))?;
|
||||
let author_hex = rumor.pubkey.to_hex();
|
||||
let conversation = Self::conversation_id(rumor).to_string();
|
||||
let rumor_id = rumor.id.context("Rumor is missing an event id")?;
|
||||
let author = rumor.pubkey;
|
||||
let conversation = self.conversation_id(rumor).to_string();
|
||||
|
||||
let mut tags = rumor.tags.clone().to_vec();
|
||||
|
||||
// Add a unique identifier
|
||||
tags.push(Tag::identifier(id));
|
||||
|
||||
// Add a reference to the rumor's author
|
||||
tags.push(Tag::custom(
|
||||
TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::A)),
|
||||
[author_hex],
|
||||
[author],
|
||||
));
|
||||
|
||||
// Add a conversation id
|
||||
tags.push(Tag::custom(
|
||||
TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::C)),
|
||||
[conversation],
|
||||
));
|
||||
|
||||
// Add a reference to the rumor's id
|
||||
tags.push(Tag::event(rumor_id));
|
||||
|
||||
// Add references to the rumor's participants
|
||||
for receiver in rumor.tags.public_keys().copied() {
|
||||
tags.push(Tag::custom(
|
||||
TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::P)),
|
||||
[receiver.to_hex()],
|
||||
[receiver],
|
||||
));
|
||||
}
|
||||
|
||||
// Convert rumor to json
|
||||
let content = rumor.as_json();
|
||||
|
||||
let event = EventBuilder::new(Kind::ApplicationSpecificData, content)
|
||||
@@ -739,34 +1006,55 @@ impl AppState {
|
||||
}
|
||||
|
||||
// Unwraps a gift-wrapped event and processes its contents.
|
||||
async fn extract_rumor(&self, gift_wrap: &Event) {
|
||||
let mut rumor: Option<UnsignedEvent> = None;
|
||||
|
||||
async fn extract_rumor(&self, gift_wrap: &Event) -> Result<(), Error> {
|
||||
// Try to get cached rumor first
|
||||
if let Ok(event) = self.get_rumor(gift_wrap.id).await {
|
||||
rumor = Some(event);
|
||||
} else if let Ok(unwrapped) = self.client.unwrap_gift_wrap(gift_wrap).await {
|
||||
self.process_rumor(gift_wrap.id, event).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 mut rumor_unsigned = unwrapped.rumor;
|
||||
|
||||
if !Self::verify_rumor_sender(sender, &rumor_unsigned) {
|
||||
log::warn!(
|
||||
"Ignoring gift wrap {}: seal pubkey {} mismatches rumor pubkey {}",
|
||||
gift_wrap.id,
|
||||
sender,
|
||||
rumor_unsigned.pubkey
|
||||
);
|
||||
} else {
|
||||
if !self.verify_rumor_sender(sender, &rumor_unsigned) {
|
||||
return Err(anyhow!("Invalid rumor"));
|
||||
};
|
||||
|
||||
// Generate event id for the rumor if it doesn't have one
|
||||
rumor_unsigned.ensure_id();
|
||||
|
||||
if let Err(e) = self.set_rumor(gift_wrap.id, &rumor_unsigned).await {
|
||||
log::warn!("Failed to cache unwrapped event: {e}")
|
||||
} else {
|
||||
rumor = Some(rumor_unsigned);
|
||||
self.set_rumor(gift_wrap.id, &rumor_unsigned).await?;
|
||||
self.process_rumor(gift_wrap.id, rumor_unsigned).await?;
|
||||
|
||||
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
|
||||
for public_key in event.tags.public_keys().copied() {
|
||||
self.ingester.send(public_key).await;
|
||||
@@ -775,53 +1063,92 @@ impl AppState {
|
||||
match event.created_at >= self.initialized_at {
|
||||
// New message: send a signal to notify the UI
|
||||
true => {
|
||||
self.signal
|
||||
.send(SignalKind::NewMessage((gift_wrap.id, event)))
|
||||
.await;
|
||||
self.signal.send(SignalKind::NewMessage((id, event))).await;
|
||||
}
|
||||
// Old message: Coop is probably processing the user's messages during initial load
|
||||
false => {
|
||||
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 pubkeys: Vec<PublicKey> = rumor.tags.public_keys().copied().collect();
|
||||
pubkeys.push(rumor.pubkey);
|
||||
pubkeys.sort();
|
||||
pubkeys.dedup();
|
||||
pubkeys.hash(&mut hasher);
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/// 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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::app_state;
|
||||
|
||||
#[test]
|
||||
fn verify_rumor_sender_accepts_matching_sender() {
|
||||
let state = app_state();
|
||||
|
||||
let keys = Keys::generate();
|
||||
let public_key = keys.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]
|
||||
fn verify_rumor_sender_rejects_mismatched_sender() {
|
||||
let state = app_state();
|
||||
|
||||
let sender_keys = Keys::generate();
|
||||
let rumor_keys = Keys::generate();
|
||||
let rumor = EventBuilder::text_note("spoof").build(rumor_keys.public_key());
|
||||
assert!(!AppState::verify_rumor_sender(
|
||||
sender_keys.public_key(),
|
||||
&rumor
|
||||
));
|
||||
|
||||
assert!(!state.verify_rumor_sender(sender_keys.public_key(), &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 {}
|
||||
|
||||
enum PopupMenuItem {
|
||||
Title(SharedString),
|
||||
Separator,
|
||||
Item {
|
||||
icon: Option<Icon>,
|
||||
@@ -314,6 +315,20 @@ impl PopupMenu {
|
||||
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
|
||||
pub fn separator(mut self) -> Self {
|
||||
if self.menu_items.is_empty() {
|
||||
@@ -588,6 +603,15 @@ impl Render for PopupMenu {
|
||||
}));
|
||||
|
||||
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(
|
||||
div()
|
||||
.rounded_none()
|
||||
|
||||
@@ -59,6 +59,10 @@ common:
|
||||
en: "Use default"
|
||||
configure:
|
||||
en: "Configure"
|
||||
hide:
|
||||
en: "Hide"
|
||||
reset:
|
||||
en: "Reset"
|
||||
|
||||
keyring_disable:
|
||||
label:
|
||||
@@ -74,6 +78,30 @@ keyring_disable:
|
||||
body_5:
|
||||
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:
|
||||
updating:
|
||||
en: "Installing the new update..."
|
||||
@@ -381,6 +409,8 @@ chat:
|
||||
en: "Sent Reports"
|
||||
nip17_not_found:
|
||||
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:
|
||||
reload_menu:
|
||||
|
||||
Reference in New Issue
Block a user