chore: clean up codebase (#186)
* refactor app state * clean up * clean up * .
This commit is contained in:
@@ -6,7 +6,7 @@ publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
common = { path = "../common" }
|
||||
app_state = { path = "../app_state" }
|
||||
states = { path = "../states" }
|
||||
|
||||
gpui.workspace = true
|
||||
nostr-sdk.workspace = true
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use anyhow::Error;
|
||||
use app_state::constants::{APP_PUBKEY, APP_UPDATER_ENDPOINT};
|
||||
use cargo_packager_updater::semver::Version;
|
||||
use cargo_packager_updater::{check_update, Config, Update};
|
||||
use gpui::http_client::Url;
|
||||
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task, Window};
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use states::constants::{APP_PUBKEY, APP_UPDATER_ENDPOINT};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
AutoUpdater::set_global(cx.new(AutoUpdater::new), cx);
|
||||
|
||||
@@ -5,7 +5,7 @@ edition.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
app_state = { path = "../app_state" }
|
||||
states = { path = "../states" }
|
||||
|
||||
nostr-sdk.workspace = true
|
||||
gpui.workspace = true
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use app_state::app_state;
|
||||
use app_state::constants::KEYRING_URL;
|
||||
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Window};
|
||||
use nostr_sdk::prelude::*;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use states::constants::KEYRING_URL;
|
||||
use states::paths::config_dir;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
ClientKeys::set_global(cx.new(ClientKeys::new), cx);
|
||||
@@ -61,7 +59,6 @@ impl ClientKeys {
|
||||
return;
|
||||
}
|
||||
|
||||
let app_state = app_state();
|
||||
let read_client_keys = cx.read_credentials(KEYRING_URL);
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
@@ -76,7 +73,7 @@ impl ClientKeys {
|
||||
this.set_keys(Some(keys), false, true, cx);
|
||||
})
|
||||
.ok();
|
||||
} else if app_state.is_first_run.load(Ordering::Acquire) {
|
||||
} else if Self::first_run() {
|
||||
// If this is the first run, generate new keys and use them for the client keys
|
||||
this.update(cx, |this, cx| {
|
||||
this.new_keys(cx);
|
||||
@@ -139,4 +136,9 @@ impl ClientKeys {
|
||||
pub fn has_keys(&self) -> bool {
|
||||
self.keys.is_some()
|
||||
}
|
||||
|
||||
fn first_run() -> bool {
|
||||
let flag = config_dir().join(".first_run");
|
||||
!flag.exists() && std::fs::write(&flag, "").is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ edition.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
app_state = { path = "../app_state" }
|
||||
states = { path = "../states" }
|
||||
|
||||
gpui.workspace = true
|
||||
nostr-connect.workspace = true
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Error};
|
||||
use app_state::constants::IMAGE_RESIZE_SERVICE;
|
||||
use chrono::{Local, TimeZone};
|
||||
use gpui::{Image, ImageFormat, SharedString, SharedUri};
|
||||
use nostr_sdk::prelude::*;
|
||||
use qrcode::render::svg;
|
||||
use qrcode::QrCode;
|
||||
use states::constants::IMAGE_RESIZE_SERVICE;
|
||||
|
||||
const NOW: &str = "now";
|
||||
const SECONDS_IN_MINUTE: i64 = 60;
|
||||
|
||||
@@ -32,12 +32,11 @@ ui = { path = "../ui" }
|
||||
title_bar = { path = "../title_bar" }
|
||||
theme = { path = "../theme" }
|
||||
common = { path = "../common" }
|
||||
app_state = { path = "../app_state" }
|
||||
states = { path = "../states" }
|
||||
registry = { path = "../registry" }
|
||||
settings = { path = "../settings" }
|
||||
client_keys = { path = "../client_keys" }
|
||||
auto_update = { path = "../auto_update" }
|
||||
signer_proxy = { path = "../signer_proxy" }
|
||||
|
||||
rust-i18n.workspace = true
|
||||
i18n.workspace = true
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
use std::borrow::Cow;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Error};
|
||||
use app_state::constants::{ACCOUNT_IDENTIFIER, BOOTSTRAP_RELAYS, DEFAULT_SIDEBAR_WIDTH};
|
||||
use app_state::state::{AuthRequest, SignalKind, UnwrappingStatus};
|
||||
use app_state::{app_state, default_nip17_relays, default_nip65_relays, nostr_client};
|
||||
use auto_update::AutoUpdater;
|
||||
use client_keys::ClientKeys;
|
||||
use common::display::RenderedProfile;
|
||||
@@ -24,8 +19,10 @@ use nostr_connect::prelude::*;
|
||||
use nostr_sdk::prelude::*;
|
||||
use registry::{Registry, RegistryEvent};
|
||||
use settings::AppSettings;
|
||||
use signer_proxy::{BrowserSignerProxy, BrowserSignerProxyOptions};
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use states::constants::{ACCOUNT_IDENTIFIER, BOOTSTRAP_RELAYS, DEFAULT_SIDEBAR_WIDTH};
|
||||
use states::state::{AuthRequest, SignalKind, UnwrappingStatus};
|
||||
use states::{app_state, default_nip17_relays, default_nip65_relays};
|
||||
use theme::{ActiveTheme, Theme, ThemeMode};
|
||||
use title_bar::TitleBar;
|
||||
use ui::actions::{CopyPublicKey, OpenPublicKey};
|
||||
@@ -121,21 +118,15 @@ impl ChatSpace {
|
||||
let status = status.read(cx);
|
||||
let all_panels = this.get_all_panel_ids(cx);
|
||||
|
||||
match status {
|
||||
UnwrappingStatus::Processing => {
|
||||
registry.update(cx, |this, cx| {
|
||||
this.load_rooms(window, cx);
|
||||
this.refresh_rooms(all_panels, cx);
|
||||
});
|
||||
}
|
||||
UnwrappingStatus::Complete => {
|
||||
registry.update(cx, |this, cx| {
|
||||
this.load_rooms(window, cx);
|
||||
this.refresh_rooms(all_panels, cx);
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
if matches!(
|
||||
status,
|
||||
UnwrappingStatus::Processing | UnwrappingStatus::Complete
|
||||
) {
|
||||
registry.update(cx, |this, cx| {
|
||||
this.load_rooms(window, cx);
|
||||
this.refresh_rooms(all_panels, cx);
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -184,14 +175,14 @@ impl ChatSpace {
|
||||
// Wait for the signer to be set
|
||||
// Also verify NIP-65 and NIP-17 relays after the signer is set
|
||||
cx.background_spawn(async move {
|
||||
Self::observe_signer().await;
|
||||
app_state().observe_signer().await;
|
||||
}),
|
||||
);
|
||||
|
||||
tasks.push(
|
||||
// Observe gift wrap process in the background
|
||||
cx.background_spawn(async move {
|
||||
Self::observe_giftwrap().await;
|
||||
app_state().observe_giftwrap().await;
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -213,90 +204,19 @@ impl ChatSpace {
|
||||
}
|
||||
}
|
||||
|
||||
async fn observe_signer() {
|
||||
let client = nostr_client();
|
||||
let app_state = app_state();
|
||||
let loop_duration = Duration::from_millis(800);
|
||||
|
||||
loop {
|
||||
if let Ok(signer) = client.signer().await {
|
||||
if let Ok(pk) = signer.get_public_key().await {
|
||||
// Notify the app that the signer has been set
|
||||
app_state.signal.send(SignalKind::SignerSet(pk)).await;
|
||||
|
||||
// Get user's gossip relays
|
||||
app_state.get_nip65(pk).await.ok();
|
||||
|
||||
// Exit the current loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
smol::Timer::after(loop_duration).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn observe_giftwrap() {
|
||||
let client = nostr_client();
|
||||
let app_state = app_state();
|
||||
let loop_duration = Duration::from_secs(20);
|
||||
let mut is_start_processing = false;
|
||||
let mut total_loops = 0;
|
||||
|
||||
loop {
|
||||
if client.has_signer().await {
|
||||
total_loops += 1;
|
||||
|
||||
if app_state.gift_wrap_processing.load(Ordering::Acquire) {
|
||||
is_start_processing = true;
|
||||
|
||||
// Reset gift wrap processing flag
|
||||
let _ = app_state.gift_wrap_processing.compare_exchange(
|
||||
true,
|
||||
false,
|
||||
Ordering::Release,
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
|
||||
let signal = SignalKind::GiftWrapStatus(UnwrappingStatus::Processing);
|
||||
app_state.signal.send(signal).await;
|
||||
} else {
|
||||
// Only run further if we are already processing
|
||||
// Wait until after 2 loops to prevent exiting early while events are still being processed
|
||||
if is_start_processing && total_loops >= 2 {
|
||||
let signal = SignalKind::GiftWrapStatus(UnwrappingStatus::Complete);
|
||||
app_state.signal.send(signal).await;
|
||||
|
||||
// Reset the counter
|
||||
is_start_processing = false;
|
||||
total_loops = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
smol::Timer::after(loop_duration).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_signals(view: WeakEntity<ChatSpace>, cx: &mut AsyncWindowContext) {
|
||||
let app_state = app_state();
|
||||
let mut is_open_proxy_modal = false;
|
||||
let states = app_state();
|
||||
|
||||
while let Ok(signal) = app_state.signal.receiver().recv_async().await {
|
||||
cx.update(|window, cx| {
|
||||
while let Ok(signal) = states.signal().receiver().recv_async().await {
|
||||
view.update_in(cx, |this, window, cx| {
|
||||
let registry = Registry::global(cx);
|
||||
let settings = AppSettings::global(cx);
|
||||
|
||||
match signal {
|
||||
SignalKind::SignerSet(public_key) => {
|
||||
// Close the latest modal if it exists
|
||||
window.close_modal(cx);
|
||||
|
||||
// Setup the default layout for current workspace
|
||||
view.update(cx, |this, cx| {
|
||||
this.set_default_layout(window, cx);
|
||||
})
|
||||
.ok();
|
||||
|
||||
// Load user's settings
|
||||
settings.update(cx, |this, cx| {
|
||||
this.load_settings(cx);
|
||||
@@ -307,45 +227,33 @@ impl ChatSpace {
|
||||
this.set_signer_pubkey(public_key, cx);
|
||||
this.load_rooms(window, cx);
|
||||
});
|
||||
|
||||
// Setup the default layout for current workspace
|
||||
this.set_default_layout(window, cx);
|
||||
}
|
||||
SignalKind::SignerUnset => {
|
||||
// Setup the onboarding layout for current workspace
|
||||
view.update(cx, |this, cx| {
|
||||
this.set_onboarding_layout(window, cx);
|
||||
})
|
||||
.ok();
|
||||
|
||||
// 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);
|
||||
let is_authenticated = AppSettings::read_global(cx).is_authenticated(url);
|
||||
|
||||
view.update(cx, |this, cx| {
|
||||
this.push_auth_request(&req, cx);
|
||||
// Store the auth request in the current view
|
||||
this.push_auth_request(&req, cx);
|
||||
|
||||
if auto_auth && is_authenticated {
|
||||
// Automatically authenticate if the relay is authenticated before
|
||||
this.auth(req, window, cx);
|
||||
} else {
|
||||
// Otherwise open the auth request popup
|
||||
this.open_auth_request(req, window, cx);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
SignalKind::ProxyDown => {
|
||||
if !is_open_proxy_modal {
|
||||
is_open_proxy_modal = true;
|
||||
|
||||
view.update(cx, |this, cx| {
|
||||
this.render_proxy_modal(window, cx);
|
||||
})
|
||||
.ok();
|
||||
if auto_auth && is_authenticated {
|
||||
// Automatically authenticate if the relay is authenticated before
|
||||
this.auth(req, window, cx);
|
||||
} else {
|
||||
// Otherwise open the auth request popup
|
||||
this.open_auth_request(req, window, cx);
|
||||
}
|
||||
}
|
||||
SignalKind::GiftWrapStatus(status) => {
|
||||
@@ -364,17 +272,11 @@ impl ChatSpace {
|
||||
});
|
||||
}
|
||||
SignalKind::GossipRelaysNotFound => {
|
||||
view.update(cx, |this, cx| {
|
||||
this.set_required_gossip_relays(cx);
|
||||
this.render_setup_gossip_relays_modal(window, cx);
|
||||
})
|
||||
.ok();
|
||||
this.set_required_gossip_relays(cx);
|
||||
this.render_setup_gossip_relays_modal(window, cx);
|
||||
}
|
||||
SignalKind::MessagingRelaysNotFound => {
|
||||
view.update(cx, |this, cx| {
|
||||
this.set_required_dm_relays(cx);
|
||||
})
|
||||
.ok();
|
||||
this.set_required_dm_relays(cx);
|
||||
}
|
||||
};
|
||||
})
|
||||
@@ -395,8 +297,8 @@ impl ChatSpace {
|
||||
self.sending_auth_request(&challenge, cx);
|
||||
|
||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let app_state = app_state();
|
||||
let states = app_state();
|
||||
let client = states.client();
|
||||
let signer = client.signer().await?;
|
||||
|
||||
// Construct event
|
||||
@@ -427,9 +329,9 @@ impl ChatSpace {
|
||||
relay.resubscribe().await?;
|
||||
|
||||
// Get all failed events that need to be resent
|
||||
let mut event_tracker = app_state.event_tracker.write().await;
|
||||
let mut tracker = states.tracker().write().await;
|
||||
|
||||
let ids: Vec<EventId> = event_tracker
|
||||
let ids: Vec<EventId> = tracker
|
||||
.resend_queue
|
||||
.iter()
|
||||
.filter(|(_, url)| relay_url == *url)
|
||||
@@ -437,7 +339,7 @@ impl ChatSpace {
|
||||
.collect();
|
||||
|
||||
for id in ids.into_iter() {
|
||||
if let Some(relay_url) = event_tracker.resend_queue.remove(&id) {
|
||||
if let Some(relay_url) = tracker.resend_queue.remove(&id) {
|
||||
if let Some(event) = client.database().event_by_id(&id).await? {
|
||||
let event_id = relay.send_event(&event).await?;
|
||||
|
||||
@@ -447,8 +349,8 @@ impl ChatSpace {
|
||||
success: HashSet::from([relay_url]),
|
||||
};
|
||||
|
||||
event_tracker.sent_ids.insert(event_id);
|
||||
event_tracker.resent_ids.push(output);
|
||||
tracker.sent_ids.insert(event_id);
|
||||
tracker.resent_ids.push(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -656,7 +558,8 @@ impl ChatSpace {
|
||||
|
||||
fn load_local_account(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let task = cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let client = app_state().client();
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::ApplicationSpecificData)
|
||||
.identifier(ACCOUNT_IDENTIFIER)
|
||||
@@ -717,9 +620,10 @@ impl ChatSpace {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let app_state = app_state();
|
||||
let states = app_state();
|
||||
let client = states.client();
|
||||
|
||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||
let filter = Filter::new().kind(Kind::PrivateDirectMessage);
|
||||
|
||||
let pubkeys: Vec<PublicKey> = client
|
||||
@@ -737,7 +641,7 @@ impl ChatSpace {
|
||||
.authors(pubkeys);
|
||||
|
||||
client
|
||||
.subscribe_to(BOOTSTRAP_RELAYS, filter, app_state.auto_close_opts)
|
||||
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
@@ -756,8 +660,8 @@ impl ChatSpace {
|
||||
|
||||
fn on_sign_out(&mut self, _e: &Logout, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let app_state = app_state();
|
||||
let states = app_state();
|
||||
let client = states.client();
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::ApplicationSpecificData)
|
||||
@@ -770,7 +674,7 @@ impl ChatSpace {
|
||||
client.reset().await;
|
||||
|
||||
// Notify the channel about the signer being unset
|
||||
app_state.signal.send(SignalKind::SignerUnset).await;
|
||||
states.signal().send(SignalKind::SignerUnset).await;
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
@@ -799,6 +703,37 @@ impl ChatSpace {
|
||||
window.push_notification(t!("common.copied"), cx);
|
||||
}
|
||||
|
||||
fn get_all_panel_ids(&self, cx: &App) -> Option<Vec<u64>> {
|
||||
let ids: Vec<u64> = self
|
||||
.dock
|
||||
.read(cx)
|
||||
.items
|
||||
.panel_ids(cx)
|
||||
.into_iter()
|
||||
.filter_map(|panel| panel.parse::<u64>().ok())
|
||||
.collect();
|
||||
|
||||
Some(ids)
|
||||
}
|
||||
|
||||
fn set_center_panel<P>(panel: P, window: &mut Window, cx: &mut App)
|
||||
where
|
||||
P: PanelView,
|
||||
{
|
||||
if let Some(Some(root)) = window.root::<Root>() {
|
||||
if let Ok(chatspace) = root.read(cx).view().clone().downcast::<ChatSpace>() {
|
||||
let panel = Arc::new(panel);
|
||||
let center = DockItem::panel(panel);
|
||||
|
||||
chatspace.update(cx, |this, cx| {
|
||||
this.dock.update(cx, |this, cx| {
|
||||
this.set_center(center, window, cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_setup_gossip_relays_modal(&mut self, window: &mut Window, cx: &mut App) {
|
||||
let relays = default_nip65_relays();
|
||||
|
||||
@@ -875,9 +810,9 @@ impl ChatSpace {
|
||||
.on_ok(|_, window, cx| {
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
let app_state = app_state();
|
||||
let states = app_state();
|
||||
let relays = default_nip65_relays();
|
||||
let result = app_state.set_nip65(relays).await;
|
||||
let result = states.set_nip65(relays).await;
|
||||
|
||||
cx.update(|window, cx| {
|
||||
match result {
|
||||
@@ -977,9 +912,9 @@ impl ChatSpace {
|
||||
.on_ok(|_, window, cx| {
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
let app_state = app_state();
|
||||
let states = app_state();
|
||||
let relays = default_nip17_relays();
|
||||
let result = app_state.set_nip17(relays).await;
|
||||
let result = states.set_nip17(relays).await;
|
||||
|
||||
cx.update(|window, cx| {
|
||||
match result {
|
||||
@@ -1001,32 +936,6 @@ impl ChatSpace {
|
||||
})
|
||||
}
|
||||
|
||||
fn render_proxy_modal(&mut self, window: &mut Window, cx: &mut App) {
|
||||
window.open_modal(cx, |this, _window, _cx| {
|
||||
this.overlay_closable(false)
|
||||
.show_close(false)
|
||||
.keyboard(false)
|
||||
.alert()
|
||||
.button_props(ModalButtonProps::default().ok_text(t!("common.open_browser")))
|
||||
.title(shared_t!("proxy.label"))
|
||||
.child(
|
||||
v_flex()
|
||||
.p_3()
|
||||
.gap_1()
|
||||
.w_full()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.text_center()
|
||||
.text_sm()
|
||||
.child(shared_t!("proxy.description")),
|
||||
)
|
||||
.on_ok(move |_e, _window, cx| {
|
||||
cx.open_url("http://localhost:7400");
|
||||
false
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn render_client_keys_modal(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
window.open_modal(cx, move |this, _window, cx| {
|
||||
this.overlay_closable(false)
|
||||
@@ -1196,92 +1105,6 @@ impl ChatSpace {
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn proxy_signer(window: &mut Window, cx: &mut App) {
|
||||
let Some(Some(root)) = window.root::<Root>() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(chatspace) = root.read(cx).view().clone().downcast::<ChatSpace>() else {
|
||||
return;
|
||||
};
|
||||
|
||||
chatspace.update(cx, |this, cx| {
|
||||
let proxy = BrowserSignerProxy::new(BrowserSignerProxyOptions::default());
|
||||
let url = proxy.url();
|
||||
|
||||
this._tasks.push(cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let app_state = app_state();
|
||||
|
||||
if proxy.start().await.is_ok() {
|
||||
webbrowser::open(&url).ok();
|
||||
|
||||
loop {
|
||||
if proxy.is_session_active() {
|
||||
// Save the signer to disk for further logins
|
||||
if let Ok(public_key) = proxy.get_public_key().await {
|
||||
let keys = Keys::generate();
|
||||
let tags = vec![Tag::identifier(ACCOUNT_IDENTIFIER)];
|
||||
let kind = Kind::ApplicationSpecificData;
|
||||
|
||||
let builder = EventBuilder::new(kind, "extension")
|
||||
.tags(tags)
|
||||
.build(public_key)
|
||||
.sign(&keys)
|
||||
.await;
|
||||
|
||||
if let Ok(event) = builder {
|
||||
if let Err(e) = client.database().save_event(&event).await {
|
||||
log::error!("Failed to save event: {e}");
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Set the client's signer with current proxy signer
|
||||
client.set_signer(proxy.clone()).await;
|
||||
|
||||
break;
|
||||
} else {
|
||||
app_state.signal.send(SignalKind::ProxyDown).await;
|
||||
}
|
||||
smol::Timer::after(Duration::from_secs(1)).await;
|
||||
}
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
fn get_all_panel_ids(&self, cx: &App) -> Option<Vec<u64>> {
|
||||
let ids: Vec<u64> = self
|
||||
.dock
|
||||
.read(cx)
|
||||
.items
|
||||
.panel_ids(cx)
|
||||
.into_iter()
|
||||
.filter_map(|panel| panel.parse::<u64>().ok())
|
||||
.collect();
|
||||
|
||||
Some(ids)
|
||||
}
|
||||
|
||||
pub(crate) fn set_center_panel<P>(panel: P, window: &mut Window, cx: &mut App)
|
||||
where
|
||||
P: PanelView,
|
||||
{
|
||||
if let Some(Some(root)) = window.root::<Root>() {
|
||||
if let Ok(chatspace) = root.read(cx).view().clone().downcast::<ChatSpace>() {
|
||||
let panel = Arc::new(panel);
|
||||
let center = DockItem::panel(panel);
|
||||
|
||||
chatspace.update(cx, |this, cx| {
|
||||
this.dock.update(cx, |this, cx| {
|
||||
this.set_center(center, window, cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ChatSpace {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use app_state::constants::{APP_ID, APP_NAME};
|
||||
use app_state::{app_state, nostr_client};
|
||||
use assets::Assets;
|
||||
use gpui::{
|
||||
point, px, size, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem, SharedString,
|
||||
TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind,
|
||||
WindowOptions,
|
||||
};
|
||||
use states::app_state;
|
||||
use states::constants::{APP_ID, APP_NAME};
|
||||
use ui::Root;
|
||||
|
||||
use crate::actions::{load_embedded_fonts, quit, Quit};
|
||||
@@ -22,9 +22,6 @@ fn main() {
|
||||
// Initialize logging
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
// Initialize the Nostr client
|
||||
let _client = nostr_client();
|
||||
|
||||
// Initialize the coop simple storage
|
||||
let _app_state = app_state();
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Error;
|
||||
use app_state::constants::{ACCOUNT_IDENTIFIER, BUNKER_TIMEOUT};
|
||||
use app_state::state::SignalKind;
|
||||
use app_state::{app_state, nostr_client};
|
||||
use client_keys::ClientKeys;
|
||||
use common::display::RenderedProfile;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
@@ -17,6 +14,9 @@ use i18n::{shared_t, t};
|
||||
use nostr_connect::prelude::*;
|
||||
use nostr_sdk::prelude::*;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use states::app_state;
|
||||
use states::constants::{ACCOUNT_IDENTIFIER, BUNKER_TIMEOUT};
|
||||
use states::state::SignalKind;
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
@@ -28,7 +28,6 @@ use ui::popup_menu::PopupMenu;
|
||||
use ui::{h_flex, v_flex, ContextModal, Sizable, StyledExt};
|
||||
|
||||
use crate::actions::CoopAuthUrlHandler;
|
||||
use crate::chatspace::ChatSpace;
|
||||
|
||||
pub fn init(
|
||||
profile: Profile,
|
||||
@@ -43,7 +42,6 @@ pub struct Account {
|
||||
profile: Profile,
|
||||
stored_secret: String,
|
||||
is_bunker: bool,
|
||||
is_extension: bool,
|
||||
loading: bool,
|
||||
|
||||
name: SharedString,
|
||||
@@ -57,8 +55,6 @@ pub struct Account {
|
||||
impl Account {
|
||||
fn new(secret: String, profile: Profile, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let is_bunker = secret.starts_with("bunker://");
|
||||
let is_extension = secret.starts_with("extension");
|
||||
|
||||
let mut subscriptions = smallvec![];
|
||||
|
||||
subscriptions.push(
|
||||
@@ -74,7 +70,6 @@ impl Account {
|
||||
Self {
|
||||
profile,
|
||||
is_bunker,
|
||||
is_extension,
|
||||
stored_secret: secret,
|
||||
loading: false,
|
||||
name: "Account".into(),
|
||||
@@ -92,8 +87,6 @@ impl Account {
|
||||
if let Ok(uri) = NostrConnectURI::parse(&self.stored_secret) {
|
||||
self.nostr_connect(uri, window, cx);
|
||||
}
|
||||
} else if self.is_extension {
|
||||
self.set_proxy(window, cx);
|
||||
} else if let Ok(enc) = EncryptedSecretKey::from_bech32(&self.stored_secret) {
|
||||
self.keys(enc, window, cx);
|
||||
} else {
|
||||
@@ -115,7 +108,7 @@ impl Account {
|
||||
self._tasks.push(
|
||||
// Handle connection in the background
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let client = nostr_client();
|
||||
let client = app_state().client();
|
||||
|
||||
match signer.bunker_uri().await {
|
||||
Ok(_) => {
|
||||
@@ -134,10 +127,6 @@ impl Account {
|
||||
);
|
||||
}
|
||||
|
||||
fn set_proxy(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
ChatSpace::proxy_signer(window, cx);
|
||||
}
|
||||
|
||||
fn keys(&mut self, enc: EncryptedSecretKey, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let pwd_input: Entity<InputState> = cx.new(|cx| InputState::new(window, cx).masked(true));
|
||||
let weak_input = pwd_input.downgrade();
|
||||
@@ -245,7 +234,7 @@ impl Account {
|
||||
})
|
||||
.ok();
|
||||
|
||||
let client = nostr_client();
|
||||
let client = app_state().client();
|
||||
let keys = Keys::new(secret);
|
||||
|
||||
// Set the client's signer with the current keys
|
||||
@@ -268,8 +257,8 @@ impl Account {
|
||||
self._tasks.push(
|
||||
// Reset the nostr client in the background
|
||||
cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let app_state = app_state();
|
||||
let states = app_state();
|
||||
let client = states.client();
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::ApplicationSpecificData)
|
||||
@@ -282,7 +271,7 @@ impl Account {
|
||||
client.unset_signer().await;
|
||||
|
||||
// Notify the channel about the signer being unset
|
||||
app_state.signal.send(SignalKind::SignerUnset).await;
|
||||
states.signal().send(SignalKind::SignerUnset).await;
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -392,41 +381,20 @@ impl Render for Account {
|
||||
.child(Avatar::new(avatar).size(rems(1.5)))
|
||||
.child(div().pb_px().font_semibold().child(name)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.when(self.is_bunker, |this| {
|
||||
let label = SharedString::from("Nostr Connect");
|
||||
.child(div().when(self.is_bunker, |this| {
|
||||
let label = SharedString::from("Nostr Connect");
|
||||
|
||||
this.child(
|
||||
div()
|
||||
.py_0p5()
|
||||
.px_2()
|
||||
.text_xs()
|
||||
.bg(cx.theme().secondary_active)
|
||||
.text_color(
|
||||
cx.theme().secondary_foreground,
|
||||
)
|
||||
.rounded_full()
|
||||
.child(label),
|
||||
)
|
||||
})
|
||||
.when(self.is_extension, |this| {
|
||||
let label = SharedString::from("Extension");
|
||||
|
||||
this.child(
|
||||
div()
|
||||
.py_0p5()
|
||||
.px_2()
|
||||
.text_xs()
|
||||
.bg(cx.theme().secondary_active)
|
||||
.text_color(
|
||||
cx.theme().secondary_foreground,
|
||||
)
|
||||
.rounded_full()
|
||||
.child(label),
|
||||
)
|
||||
}),
|
||||
),
|
||||
this.child(
|
||||
div()
|
||||
.py_0p5()
|
||||
.px_2()
|
||||
.text_xs()
|
||||
.bg(cx.theme().secondary_active)
|
||||
.text_color(cx.theme().secondary_foreground)
|
||||
.rounded_full()
|
||||
.child(label),
|
||||
)
|
||||
})),
|
||||
)
|
||||
})
|
||||
.active(|this| this.bg(cx.theme().element_active))
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::time::Duration;
|
||||
|
||||
use app_state::{app_state, nostr_client};
|
||||
use common::display::{RenderedProfile, RenderedTimestamp};
|
||||
use common::nip96::nip96_upload;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
@@ -24,6 +23,7 @@ use serde::Deserialize;
|
||||
use settings::AppSettings;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use smol::fs;
|
||||
use states::app_state;
|
||||
use theme::ActiveTheme;
|
||||
use ui::actions::{CopyPublicKey, OpenPublicKey};
|
||||
use ui::avatar::Avatar;
|
||||
@@ -169,8 +169,8 @@ impl Chat {
|
||||
let message = Message::user(event);
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let app_state = app_state();
|
||||
let event_tracker = app_state.event_tracker.read().await;
|
||||
let states = app_state();
|
||||
let event_tracker = states.tracker().read().await;
|
||||
let sent_ids = event_tracker.sent_ids();
|
||||
|
||||
this.update_in(cx, |this, _window, cx| {
|
||||
@@ -530,7 +530,7 @@ impl Chat {
|
||||
let path = paths.pop()?;
|
||||
|
||||
let upload = Tokio::spawn(cx, async move {
|
||||
let client = nostr_client();
|
||||
let client = app_state().client();
|
||||
let file = fs::read(path).await.ok()?;
|
||||
let url = nip96_upload(client, &nip96_server, file).await.ok()?;
|
||||
|
||||
@@ -1239,9 +1239,9 @@ impl Chat {
|
||||
let id = ev.0;
|
||||
|
||||
let task: Task<Result<Vec<RelayUrl>, Error>> = cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let app_state = app_state();
|
||||
let event_tracker = app_state.event_tracker.read().await;
|
||||
let states = app_state();
|
||||
let client = states.client();
|
||||
let event_tracker = states.tracker().read().await;
|
||||
let mut relays: Vec<RelayUrl> = vec![];
|
||||
|
||||
let filter = Filter::new()
|
||||
|
||||
@@ -2,8 +2,6 @@ use std::ops::Range;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Error};
|
||||
use app_state::constants::BOOTSTRAP_RELAYS;
|
||||
use app_state::{app_state, nostr_client};
|
||||
use common::display::{RenderedProfile, TextUtils};
|
||||
use common::nip05::nip05_profile;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
@@ -19,6 +17,8 @@ use registry::room::Room;
|
||||
use registry::Registry;
|
||||
use settings::AppSettings;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use states::app_state;
|
||||
use states::constants::BOOTSTRAP_RELAYS;
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
@@ -129,7 +129,7 @@ impl Compose {
|
||||
let mut tasks = smallvec![];
|
||||
|
||||
let get_contacts: Task<Result<Vec<Contact>, Error>> = cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let client = app_state().client();
|
||||
let signer = client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
let profiles = client.database().contacts(public_key).await?;
|
||||
@@ -195,13 +195,15 @@ impl Compose {
|
||||
}
|
||||
|
||||
async fn request_metadata(public_key: PublicKey) -> Result<(), Error> {
|
||||
let client = nostr_client();
|
||||
let app_state = app_state();
|
||||
let states = app_state();
|
||||
let client = states.client();
|
||||
|
||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||
let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList];
|
||||
let filter = Filter::new().author(public_key).kinds(kinds).limit(10);
|
||||
|
||||
client
|
||||
.subscribe_to(BOOTSTRAP_RELAYS, filter, app_state.auto_close_opts)
|
||||
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -2,7 +2,6 @@ use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Error;
|
||||
use app_state::nostr_client;
|
||||
use common::nip96::nip96_upload;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
@@ -13,6 +12,7 @@ use i18n::{shared_t, t};
|
||||
use nostr_sdk::prelude::*;
|
||||
use settings::AppSettings;
|
||||
use smol::fs;
|
||||
use states::app_state;
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::input::{InputState, TextInput};
|
||||
@@ -58,7 +58,7 @@ impl EditProfile {
|
||||
};
|
||||
|
||||
let task: Task<Result<Option<Metadata>, Error>> = cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let client = app_state().client();
|
||||
let signer = client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
let metadata = client
|
||||
@@ -125,7 +125,9 @@ impl EditProfile {
|
||||
let (tx, rx) = oneshot::channel::<Url>();
|
||||
|
||||
nostr_sdk::async_utility::task::spawn(async move {
|
||||
if let Ok(url) = nip96_upload(nostr_client(), &nip96, file_data).await {
|
||||
if let Ok(url) =
|
||||
nip96_upload(app_state().client(), &nip96, file_data).await
|
||||
{
|
||||
_ = tx.send(url);
|
||||
}
|
||||
});
|
||||
@@ -188,7 +190,7 @@ impl EditProfile {
|
||||
}
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let client = app_state().client();
|
||||
let signer = client.signer().await?;
|
||||
|
||||
// Sign the new metadata event
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use app_state::constants::{ACCOUNT_IDENTIFIER, BUNKER_TIMEOUT};
|
||||
use app_state::nostr_client;
|
||||
use client_keys::ClientKeys;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
@@ -12,6 +10,8 @@ use gpui::{
|
||||
use i18n::{shared_t, t};
|
||||
use nostr_connect::prelude::*;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use states::app_state;
|
||||
use states::constants::{ACCOUNT_IDENTIFIER, BUNKER_TIMEOUT};
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
@@ -248,7 +248,7 @@ impl Login {
|
||||
|
||||
// Set the client's signer with the current keys
|
||||
cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let client = app_state().client();
|
||||
client.set_signer(keys).await;
|
||||
})
|
||||
.detach();
|
||||
@@ -331,7 +331,7 @@ impl Login {
|
||||
}
|
||||
|
||||
let task: Task<Result<(), anyhow::Error>> = cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let client = app_state().client();
|
||||
|
||||
// Update the client's signer
|
||||
client.set_signer(signer).await;
|
||||
@@ -362,7 +362,7 @@ impl Login {
|
||||
if let Ok(enc_key) =
|
||||
EncryptedSecretKey::new(keys.secret_key(), &password, 8, KeySecurity::Unknown)
|
||||
{
|
||||
let client = nostr_client();
|
||||
let client = app_state().client();
|
||||
let value = enc_key.to_bech32().unwrap();
|
||||
let keys = Keys::generate();
|
||||
let tags = vec![Tag::identifier(ACCOUNT_IDENTIFIER)];
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use anyhow::{anyhow, Error};
|
||||
use app_state::constants::{ACCOUNT_IDENTIFIER, BOOTSTRAP_RELAYS};
|
||||
use app_state::{default_nip17_relays, default_nip65_relays, nostr_client};
|
||||
use common::nip96::nip96_upload;
|
||||
use gpui::{
|
||||
div, relative, rems, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity,
|
||||
@@ -12,6 +10,8 @@ use i18n::{shared_t, t};
|
||||
use nostr_sdk::prelude::*;
|
||||
use settings::AppSettings;
|
||||
use smol::fs;
|
||||
use states::constants::{ACCOUNT_IDENTIFIER, BOOTSTRAP_RELAYS};
|
||||
use states::{app_state, default_nip17_relays, default_nip65_relays};
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
@@ -124,7 +124,7 @@ impl NewAccount {
|
||||
|
||||
// Set the client's signer with the current keys
|
||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let client = app_state().client();
|
||||
|
||||
// Set the client's signer with the current keys
|
||||
client.set_signer(keys).await;
|
||||
@@ -176,7 +176,7 @@ impl NewAccount {
|
||||
if let Ok(enc_key) =
|
||||
EncryptedSecretKey::new(keys.secret_key(), &password, 8, KeySecurity::Unknown)
|
||||
{
|
||||
let client = nostr_client();
|
||||
let client = app_state().client();
|
||||
let value = enc_key.to_bech32().unwrap();
|
||||
let keys = Keys::generate();
|
||||
let tags = vec![Tag::identifier(ACCOUNT_IDENTIFIER)];
|
||||
@@ -217,7 +217,7 @@ impl NewAccount {
|
||||
Ok(Some(mut paths)) => {
|
||||
if let Some(path) = paths.pop() {
|
||||
let file = fs::read(path).await?;
|
||||
let url = nip96_upload(nostr_client(), &nip96_server, file).await?;
|
||||
let url = nip96_upload(app_state().client(), &nip96_server, file).await?;
|
||||
|
||||
Ok(url)
|
||||
} else {
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use app_state::constants::{
|
||||
ACCOUNT_IDENTIFIER, APP_NAME, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT,
|
||||
};
|
||||
use app_state::nostr_client;
|
||||
use client_keys::ClientKeys;
|
||||
use common::display::TextUtils;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
@@ -16,6 +12,8 @@ use gpui::{
|
||||
use i18n::{shared_t, t};
|
||||
use nostr_connect::prelude::*;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use states::app_state;
|
||||
use states::constants::{ACCOUNT_IDENTIFIER, APP_NAME, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT};
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
@@ -23,7 +21,7 @@ use ui::notification::Notification;
|
||||
use ui::popup_menu::PopupMenu;
|
||||
use ui::{divider, h_flex, v_flex, ContextModal, Icon, IconName, Sizable, StyledExt};
|
||||
|
||||
use crate::chatspace::{self, ChatSpace};
|
||||
use crate::chatspace;
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Onboarding> {
|
||||
Onboarding::new(window, cx)
|
||||
@@ -163,10 +161,6 @@ impl Onboarding {
|
||||
)
|
||||
}
|
||||
|
||||
fn set_proxy(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
ChatSpace::proxy_signer(window, cx);
|
||||
}
|
||||
|
||||
fn write_uri_to_disk(
|
||||
&mut self,
|
||||
signer: NostrConnect,
|
||||
@@ -181,7 +175,7 @@ impl Onboarding {
|
||||
}
|
||||
|
||||
let task: Task<Result<(), anyhow::Error>> = cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let client = app_state().client();
|
||||
|
||||
// Update the client's signer
|
||||
client.set_signer(signer).await;
|
||||
@@ -348,30 +342,11 @@ impl Render for Onboarding {
|
||||
.child(
|
||||
Button::new("key")
|
||||
.label(t!("onboarding.key_login"))
|
||||
.large()
|
||||
.ghost_alt()
|
||||
.on_click(cx.listener(move |_, _, window, cx| {
|
||||
chatspace::login(window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Button::new("ext")
|
||||
.label(t!("onboarding.ext_login"))
|
||||
.ghost_alt()
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.set_proxy(window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.italic()
|
||||
.text_xs()
|
||||
.text_center()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(shared_t!("onboarding.ext_login_note")),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use app_state::constants::BOOTSTRAP_RELAYS;
|
||||
use app_state::nostr_client;
|
||||
use common::display::{shorten_pubkey, RenderedProfile, RenderedTimestamp};
|
||||
use common::nip05::nip05_verify;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
@@ -15,6 +13,8 @@ use nostr_sdk::prelude::*;
|
||||
use registry::Registry;
|
||||
use settings::AppSettings;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use states::app_state;
|
||||
use states::constants::BOOTSTRAP_RELAYS;
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
@@ -43,7 +43,7 @@ impl Screening {
|
||||
|
||||
let contact_check: Task<Result<(bool, Vec<Profile>), Error>> =
|
||||
cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let client = app_state().client();
|
||||
let signer = client.signer().await?;
|
||||
let signer_pubkey = signer.get_public_key().await?;
|
||||
|
||||
@@ -68,7 +68,7 @@ impl Screening {
|
||||
});
|
||||
|
||||
let activity_check = cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let client = app_state().client();
|
||||
let filter = Filter::new().author(public_key).limit(1);
|
||||
let mut activity: Option<Timestamp> = None;
|
||||
|
||||
@@ -157,7 +157,7 @@ impl Screening {
|
||||
let public_key = self.profile.public_key();
|
||||
|
||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let client = app_state().client();
|
||||
let signer = client.signer().await?;
|
||||
|
||||
let tag = Tag::public_key_report(public_key, Report::Impersonation);
|
||||
|
||||
@@ -2,7 +2,6 @@ use std::collections::HashSet;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Error};
|
||||
use app_state::{app_state, nostr_client};
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, px, uniform_list, App, AppContext, AsyncWindowContext, Context, Entity,
|
||||
@@ -10,8 +9,10 @@ use gpui::{
|
||||
Task, TextAlign, UniformList, Window,
|
||||
};
|
||||
use i18n::{shared_t, t};
|
||||
use itertools::Itertools;
|
||||
use nostr_sdk::prelude::*;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use states::app_state;
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
@@ -80,7 +81,7 @@ impl SetupRelay {
|
||||
|
||||
fn load(cx: &AsyncWindowContext) -> Task<Result<Vec<RelayUrl>, Error>> {
|
||||
cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let client = app_state().client();
|
||||
let signer = client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
@@ -152,8 +153,8 @@ impl SetupRelay {
|
||||
let relays = self.relays.clone();
|
||||
|
||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||
let app_state = app_state();
|
||||
let client = nostr_client();
|
||||
let states = app_state();
|
||||
let client = states.client();
|
||||
let signer = client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
@@ -177,16 +178,10 @@ impl SetupRelay {
|
||||
}
|
||||
|
||||
// Fetch gift wrap events
|
||||
let sub_id = app_state.gift_wrap_sub_id.clone();
|
||||
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
|
||||
|
||||
if client
|
||||
.subscribe_with_id_to(relays.clone(), sub_id, filter, None)
|
||||
states
|
||||
.get_messages(public_key, &relays.into_iter().collect_vec())
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
log::info!("Subscribed to messages in: {relays:?}");
|
||||
};
|
||||
.ok();
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
@@ -3,9 +3,6 @@ use std::ops::Range;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Error};
|
||||
use app_state::constants::{BOOTSTRAP_RELAYS, SEARCH_RELAYS};
|
||||
use app_state::state::UnwrappingStatus;
|
||||
use app_state::{app_state, nostr_client};
|
||||
use common::debounced_delay::DebouncedDelay;
|
||||
use common::display::{RenderedTimestamp, TextUtils};
|
||||
use gpui::prelude::FluentBuilder;
|
||||
@@ -23,6 +20,9 @@ use registry::room::{Room, RoomKind};
|
||||
use registry::{Registry, RegistryEvent};
|
||||
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};
|
||||
@@ -140,7 +140,7 @@ impl Sidebar {
|
||||
}
|
||||
|
||||
async fn request_metadata(public_key: PublicKey) -> Result<(), Error> {
|
||||
let client = nostr_client();
|
||||
let client = app_state().client();
|
||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||
let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList];
|
||||
let filter = Filter::new().author(public_key).kinds(kinds).limit(10);
|
||||
@@ -165,7 +165,7 @@ impl Sidebar {
|
||||
}
|
||||
|
||||
async fn nip50(query: &str) -> Result<BTreeSet<Room>, Error> {
|
||||
let client = nostr_client();
|
||||
let client = app_state().client();
|
||||
let signer = client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
@@ -530,9 +530,8 @@ impl Sidebar {
|
||||
|
||||
fn on_manage(&mut self, _ev: &RelayStatus, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let task: Task<Result<Vec<Relay>, Error>> = cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let app_state = app_state();
|
||||
let subscription = client.subscription(&app_state.gift_wrap_sub_id).await;
|
||||
let client = app_state().client();
|
||||
let subscription = client.subscription(&SubscriptionId::new("inbox")).await;
|
||||
let mut relays: Vec<Relay> = vec![];
|
||||
|
||||
for (url, _filter) in subscription.into_iter() {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use app_state::nostr_client;
|
||||
use common::display::RenderedProfile;
|
||||
use common::nip05::nip05_verify;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
@@ -14,6 +13,7 @@ use nostr_sdk::prelude::*;
|
||||
use registry::Registry;
|
||||
use settings::AppSettings;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use states::app_state;
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
@@ -39,7 +39,7 @@ impl UserProfile {
|
||||
let mut tasks = smallvec![];
|
||||
|
||||
let check_follow: Task<Result<bool, Error>> = cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let client = app_state().client();
|
||||
let signer = client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
let contact_list = client.database().contacts_public_keys(public_key).await?;
|
||||
|
||||
@@ -6,16 +6,19 @@ publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
common = { path = "../common" }
|
||||
app_state = { path = "../app_state" }
|
||||
states = { path = "../states" }
|
||||
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
|
||||
|
||||
fuzzy-matcher = "0.3.7"
|
||||
rustls = "0.23.23"
|
||||
|
||||
@@ -2,17 +2,19 @@ use std::cmp::Reverse;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use anyhow::Error;
|
||||
use app_state::nostr_client;
|
||||
use app_state::state::UnwrappingStatus;
|
||||
use common::event::EventUtils;
|
||||
use fuzzy_matcher::skim::SkimMatcherV2;
|
||||
use fuzzy_matcher::FuzzyMatcher;
|
||||
use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Task, WeakEntity, Window};
|
||||
use gpui::{
|
||||
App, AppContext, AsyncApp, Context, Entity, EventEmitter, Global, Task, WeakEntity, Window,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use nostr_sdk::prelude::*;
|
||||
use room::RoomKind;
|
||||
use settings::AppSettings;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use states::app_state;
|
||||
use states::state::UnwrappingStatus;
|
||||
|
||||
use crate::room::Room;
|
||||
|
||||
@@ -34,7 +36,7 @@ pub enum RegistryEvent {
|
||||
NewRequest(RoomKind),
|
||||
}
|
||||
|
||||
/// Main registry for managing chat rooms and user profiles
|
||||
#[derive(Debug)]
|
||||
pub struct Registry {
|
||||
/// Collection of all chat rooms
|
||||
pub rooms: Vec<Entity<Room>>,
|
||||
@@ -45,7 +47,7 @@ pub struct Registry {
|
||||
/// Status of the unwrapping process
|
||||
pub unwrapping_status: Entity<UnwrappingStatus>,
|
||||
|
||||
/// Public key of the currently activated signer
|
||||
/// Public Key of the currently activated signer
|
||||
signer_pubkey: Option<PublicKey>,
|
||||
|
||||
/// Tasks for asynchronous operations
|
||||
@@ -55,51 +57,40 @@ pub struct Registry {
|
||||
impl EventEmitter<RegistryEvent> for Registry {}
|
||||
|
||||
impl Registry {
|
||||
/// Retrieve the Global Registry state
|
||||
/// Retrieve the global registry state
|
||||
pub fn global(cx: &App) -> Entity<Self> {
|
||||
cx.global::<GlobalRegistry>().0.clone()
|
||||
}
|
||||
|
||||
/// Retrieve the Registry instance
|
||||
/// Retrieve the registry instance
|
||||
pub fn read_global(cx: &App) -> &Self {
|
||||
cx.global::<GlobalRegistry>().0.read(cx)
|
||||
}
|
||||
|
||||
/// Set the global Registry instance
|
||||
/// Set the global registry instance
|
||||
pub(crate) fn set_global(state: Entity<Self>, cx: &mut App) {
|
||||
cx.set_global(GlobalRegistry(state));
|
||||
}
|
||||
|
||||
/// Create a new Registry instance
|
||||
/// Create a new registry instance
|
||||
pub(crate) fn new(cx: &mut Context<Self>) -> Self {
|
||||
let unwrapping_status = cx.new(|_| UnwrappingStatus::default());
|
||||
let mut tasks = smallvec![];
|
||||
|
||||
let load_local_persons: Task<Result<Vec<Profile>, Error>> =
|
||||
cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let filter = Filter::new().kind(Kind::Metadata).limit(200);
|
||||
let events = client.database().query(filter).await?;
|
||||
let mut profiles = vec![];
|
||||
|
||||
for event in events.into_iter() {
|
||||
let metadata = Metadata::from_json(event.content).unwrap_or_default();
|
||||
let profile = Profile::new(event.pubkey, metadata);
|
||||
profiles.push(profile);
|
||||
}
|
||||
|
||||
Ok(profiles)
|
||||
});
|
||||
|
||||
tasks.push(
|
||||
// Load all user profiles from the database when the Registry is created
|
||||
// Load all user profiles from the database
|
||||
cx.spawn(async move |this, cx| {
|
||||
if let Ok(profiles) = load_local_persons.await {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_persons(profiles, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
match Self::load_persons(cx).await {
|
||||
Ok(profiles) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_persons(profiles, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to load persons: {e}");
|
||||
}
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -112,6 +103,25 @@ impl Registry {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a task to load all user profiles from the database
|
||||
fn load_persons(cx: &AsyncApp) -> Task<Result<Vec<Profile>, Error>> {
|
||||
cx.background_spawn(async move {
|
||||
let client = app_state().client();
|
||||
let filter = Filter::new().kind(Kind::Metadata).limit(200);
|
||||
let events = client.database().query(filter).await?;
|
||||
|
||||
let mut profiles = vec![];
|
||||
|
||||
for event in events.into_iter() {
|
||||
let metadata = Metadata::from_json(event.content).unwrap_or_default();
|
||||
let profile = Profile::new(event.pubkey, metadata);
|
||||
profiles.push(profile);
|
||||
}
|
||||
|
||||
Ok(profiles)
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the public key of the currently activated signer.
|
||||
pub fn signer_pubkey(&self) -> Option<PublicKey> {
|
||||
self.signer_pubkey
|
||||
@@ -269,7 +279,7 @@ impl Registry {
|
||||
let bypass_setting = AppSettings::get_contact_bypass(cx);
|
||||
|
||||
let task: Task<Result<HashSet<Room>, Error>> = cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let client = app_state().client();
|
||||
let signer = client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
let contacts = client.database().contacts_public_keys(public_key).await?;
|
||||
@@ -339,11 +349,9 @@ impl Registry {
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
match task.await {
|
||||
Ok(rooms) => {
|
||||
this.update_in(cx, move |_, window, cx| {
|
||||
cx.defer_in(window, move |this, _window, cx| {
|
||||
this.extend_rooms(rooms, cx);
|
||||
this.sort(cx);
|
||||
});
|
||||
this.update_in(cx, move |this, _window, cx| {
|
||||
this.extend_rooms(rooms, cx);
|
||||
this.sort(cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@ use std::hash::{Hash, Hasher};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Error};
|
||||
use app_state::constants::SEND_RETRY;
|
||||
use app_state::{app_state, nostr_client};
|
||||
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 states::app_state;
|
||||
use states::constants::SEND_RETRY;
|
||||
|
||||
use crate::Registry;
|
||||
|
||||
@@ -171,9 +171,9 @@ impl From<&UnsignedEvent> for Room {
|
||||
}
|
||||
|
||||
impl Room {
|
||||
/// Constructs a new room instance for a private message with the given receiver and tags.
|
||||
/// Constructs a new room with the given receiver and tags.
|
||||
pub async fn new(subject: Option<String>, receivers: Vec<PublicKey>) -> Result<Self, Error> {
|
||||
let client = nostr_client();
|
||||
let client = app_state().client();
|
||||
let signer = client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
@@ -310,7 +310,7 @@ impl Room {
|
||||
let members = self.members.clone();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let client = app_state().client();
|
||||
let signer = client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
@@ -363,11 +363,11 @@ impl Room {
|
||||
let members = self.members.clone();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let client = app_state().client();
|
||||
let signer = client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
let sent_ids: Vec<EventId> = app_state()
|
||||
.event_tracker
|
||||
.tracker()
|
||||
.read()
|
||||
.await
|
||||
.sent_ids()
|
||||
@@ -482,8 +482,8 @@ impl Room {
|
||||
let mut members = self.members.clone();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let app_state = app_state();
|
||||
let client = nostr_client();
|
||||
let states = app_state();
|
||||
let client = states.client();
|
||||
let signer = client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
@@ -514,7 +514,7 @@ impl Room {
|
||||
if auth_required {
|
||||
// Wait for authenticated and resent event successfully
|
||||
for attempt in 0..=SEND_RETRY {
|
||||
let retry_manager = app_state.event_tracker.read().await;
|
||||
let retry_manager = states.tracker().read().await;
|
||||
let ids = retry_manager.resent_ids();
|
||||
|
||||
// Check if event was successfully resent
|
||||
@@ -579,7 +579,7 @@ impl Room {
|
||||
cx: &App,
|
||||
) -> Task<Result<Vec<SendReport>, Error>> {
|
||||
cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let client = app_state().client();
|
||||
let mut resend_reports = vec![];
|
||||
|
||||
for report in reports.into_iter() {
|
||||
@@ -633,7 +633,7 @@ impl Room {
|
||||
|
||||
/// Gets messaging relays for public key
|
||||
async fn messaging_relays(public_key: PublicKey) -> Vec<RelayUrl> {
|
||||
let client = nostr_client();
|
||||
let client = app_state().client();
|
||||
let mut relay_urls = vec![];
|
||||
|
||||
let filter = Filter::new()
|
||||
|
||||
@@ -5,7 +5,7 @@ edition.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
app_state = { path = "../app_state" }
|
||||
states = { path = "../states" }
|
||||
|
||||
nostr-sdk.workspace = true
|
||||
gpui.workspace = true
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use anyhow::anyhow;
|
||||
use app_state::constants::SETTINGS_IDENTIFIER;
|
||||
use app_state::nostr_client;
|
||||
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task};
|
||||
use nostr_sdk::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use states::app_state;
|
||||
use states::constants::SETTINGS_IDENTIFIER;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
let state = cx.new(AppSettings::new);
|
||||
@@ -121,7 +121,7 @@ impl AppSettings {
|
||||
|
||||
pub fn load_settings(&self, cx: &mut Context<Self>) {
|
||||
let task: Task<Result<Settings, anyhow::Error>> = cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let client = app_state().client();
|
||||
let signer = client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
@@ -153,7 +153,7 @@ impl AppSettings {
|
||||
pub fn set_settings(&self, cx: &mut Context<Self>) {
|
||||
if let Ok(content) = serde_json::to_string(&self.setting_values) {
|
||||
let task: Task<Result<(), anyhow::Error>> = cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let client = app_state().client();
|
||||
let signer = client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
[package]
|
||||
name = "signer_proxy"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
app_state = { path = "../app_state" }
|
||||
|
||||
nostr.workspace = true
|
||||
smol.workspace = true
|
||||
oneshot.workspace = true
|
||||
anyhow.workspace = true
|
||||
log.workspace = true
|
||||
futures.workspace = true
|
||||
smallvec.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
atomic-destructor = "0.3.0"
|
||||
uuid = { version = "1.17", features = ["serde", "v4"] }
|
||||
hyper = { version = "1.6", features = ["server", "http1"] }
|
||||
hyper-util = { version = "0.1", features = ["server"] }
|
||||
bytes = "1.10"
|
||||
http-body-util = "0.1"
|
||||
@@ -1,35 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>NIP-07 Proxy</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>NIP-07 Proxy</h1>
|
||||
<p>
|
||||
This page acts as a proxy between your native application and
|
||||
the NIP-07 browser extension.
|
||||
</p>
|
||||
<div class="status-box">
|
||||
<strong>Status:</strong> <span id="status">Checking...</span>
|
||||
</div>
|
||||
<p>
|
||||
<small
|
||||
>Keep this tab open while using your application. The page
|
||||
will automatically poll for requests from your native
|
||||
app.</small
|
||||
>
|
||||
</p>
|
||||
|
||||
<h3>Debug Info</h3>
|
||||
<p>
|
||||
<small
|
||||
>Check the browser console (F12) for detailed logs.</small
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script src="proxy.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,151 +0,0 @@
|
||||
let isPolling = false;
|
||||
|
||||
async function pollForRequests() {
|
||||
if (isPolling) return;
|
||||
isPolling = true;
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/pending");
|
||||
const data = await response.json();
|
||||
|
||||
console.log("Polled for requests, got:", data);
|
||||
|
||||
// Process any new requests
|
||||
if (data.requests && data.requests.length > 0) {
|
||||
console.log(`Processing ${data.requests.length} requests`);
|
||||
for (const request of data.requests) {
|
||||
await handleNip07Request(request);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Polling error:", error);
|
||||
updateStatus("Error: " + error.message, "error");
|
||||
}
|
||||
|
||||
isPolling = false;
|
||||
}
|
||||
|
||||
async function handleNip07Request(request) {
|
||||
console.log("Handling request:", request);
|
||||
|
||||
try {
|
||||
let result;
|
||||
|
||||
if (!window.nostr) {
|
||||
throw new Error("NIP-07 extension not available");
|
||||
}
|
||||
|
||||
switch (request.method) {
|
||||
case "get_public_key":
|
||||
console.log("Calling nostr.getPublicKey()");
|
||||
result = await window.nostr.getPublicKey();
|
||||
console.log("Got public key:", result);
|
||||
break;
|
||||
|
||||
case "sign_event":
|
||||
console.log("Calling nostr.signEvent() with:", request.params);
|
||||
result = await window.nostr.signEvent(request.params);
|
||||
console.log("Got signed event:", result);
|
||||
break;
|
||||
|
||||
case "nip04_encrypt":
|
||||
console.log("Calling nostr.nip04.encrypt()");
|
||||
result = await window.nostr.nip04.encrypt(
|
||||
request.params.public_key,
|
||||
request.params.content,
|
||||
);
|
||||
break;
|
||||
|
||||
case "nip04_decrypt":
|
||||
console.log("Calling nostr.nip04.decrypt()");
|
||||
result = await window.nostr.nip04.decrypt(
|
||||
request.params.public_key,
|
||||
request.params.content,
|
||||
);
|
||||
break;
|
||||
|
||||
case "nip44_encrypt":
|
||||
console.log("Calling nostr.nip44.encrypt()");
|
||||
result = await window.nostr.nip44.encrypt(
|
||||
request.params.public_key,
|
||||
request.params.content,
|
||||
);
|
||||
break;
|
||||
|
||||
case "nip44_decrypt":
|
||||
console.log("Calling nostr.nip44.decrypt()");
|
||||
result = await window.nostr.nip44.decrypt(
|
||||
request.params.public_key,
|
||||
request.params.content,
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown method: ${request.method}`);
|
||||
}
|
||||
|
||||
// Send response back to server
|
||||
const responsePayload = {
|
||||
id: request.id,
|
||||
result: result,
|
||||
error: null,
|
||||
};
|
||||
|
||||
console.log("Sending response:", responsePayload);
|
||||
|
||||
await fetch("/api/response", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(responsePayload),
|
||||
});
|
||||
|
||||
console.log("Response sent successfully");
|
||||
updateStatus("Request processed successfully", "connected");
|
||||
} catch (error) {
|
||||
console.error("Error handling request:", error);
|
||||
|
||||
// Send error response back to server
|
||||
const errorPayload = {
|
||||
id: request.id,
|
||||
result: null,
|
||||
error: error.message,
|
||||
};
|
||||
|
||||
console.log("Sending error response:", errorPayload);
|
||||
|
||||
await fetch("/api/response", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(errorPayload),
|
||||
});
|
||||
|
||||
updateStatus("Error: " + error.message, "error");
|
||||
}
|
||||
}
|
||||
|
||||
function updateStatus(message, className) {
|
||||
const statusEl = document.getElementById("status");
|
||||
statusEl.textContent = message;
|
||||
statusEl.className = className;
|
||||
}
|
||||
|
||||
// Start polling when page loads
|
||||
window.addEventListener("load", () => {
|
||||
console.log("NIP-07 Proxy loaded");
|
||||
|
||||
// Check if NIP-07 extension is available
|
||||
if (window.nostr) {
|
||||
console.log("NIP-07 extension detected");
|
||||
updateStatus("Connected to NIP-07 extension - Ready", "connected");
|
||||
} else {
|
||||
console.log("NIP-07 extension not found");
|
||||
updateStatus("NIP-07 extension not found", "error");
|
||||
}
|
||||
|
||||
// Start polling every 500 ms
|
||||
setInterval(pollForRequests, 500);
|
||||
});
|
||||
@@ -1,73 +0,0 @@
|
||||
use std::{fmt, io};
|
||||
|
||||
use hyper::http;
|
||||
use nostr::event;
|
||||
use oneshot::RecvError;
|
||||
|
||||
/// Error
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// I/O error
|
||||
Io(io::Error),
|
||||
/// HTTP error
|
||||
Http(http::Error),
|
||||
/// Json error
|
||||
Json(serde_json::Error),
|
||||
/// Event error
|
||||
Event(event::Error),
|
||||
/// Oneshot channel receive error
|
||||
OneShotRecv(RecvError),
|
||||
/// Generic error
|
||||
Generic(String),
|
||||
/// Timeout
|
||||
Timeout,
|
||||
/// The server is shutdown
|
||||
Shutdown,
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Io(e) => write!(f, "{e}"),
|
||||
Self::Http(e) => write!(f, "{e}"),
|
||||
Self::Json(e) => write!(f, "{e}"),
|
||||
Self::Event(e) => write!(f, "{e}"),
|
||||
Self::OneShotRecv(e) => write!(f, "{e}"),
|
||||
Self::Generic(e) => write!(f, "{e}"),
|
||||
Self::Timeout => write!(f, "timeout"),
|
||||
Self::Shutdown => write!(f, "server is shutdown"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(e: io::Error) -> Self {
|
||||
Self::Io(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<http::Error> for Error {
|
||||
fn from(e: http::Error) -> Self {
|
||||
Self::Http(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for Error {
|
||||
fn from(e: serde_json::Error) -> Self {
|
||||
Self::Json(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<event::Error> for Error {
|
||||
fn from(e: event::Error) -> Self {
|
||||
Self::Event(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RecvError> for Error {
|
||||
fn from(e: RecvError) -> Self {
|
||||
Self::OneShotRecv(e)
|
||||
}
|
||||
}
|
||||
@@ -1,678 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4, TcpListener};
|
||||
use std::pin::Pin;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::task::{Context, Poll};
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use atomic_destructor::{AtomicDestroyer, AtomicDestructor};
|
||||
use bytes::Bytes;
|
||||
use futures::FutureExt;
|
||||
use http_body_util::combinators::BoxBody;
|
||||
use http_body_util::{BodyExt, Full};
|
||||
use hyper::body::Incoming;
|
||||
use hyper::server::conn::http1;
|
||||
use hyper::service::service_fn;
|
||||
use hyper::{Method, Request, Response, StatusCode};
|
||||
use nostr::prelude::{BoxedFuture, SignerBackend};
|
||||
use nostr::{Event, NostrSigner, PublicKey, SignerError, UnsignedEvent};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize, Serializer};
|
||||
use serde_json::{json, Value};
|
||||
use smol::io::{AsyncRead, AsyncWrite};
|
||||
use smol::lock::Mutex;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::error::Error;
|
||||
|
||||
mod error;
|
||||
|
||||
const HTML: &str = include_str!("../index.html");
|
||||
const JS: &str = include_str!("../proxy.js");
|
||||
const CSS: &str = include_str!("../style.css");
|
||||
|
||||
/// Wrapper to make smol::Async<TcpStream> compatible with hyper
|
||||
struct HyperIo<T> {
|
||||
inner: T,
|
||||
}
|
||||
|
||||
impl<T> HyperIo<T> {
|
||||
fn new(inner: T) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsyncRead + Unpin> hyper::rt::Read for HyperIo<T> {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
mut buf: hyper::rt::ReadBufCursor<'_>,
|
||||
) -> Poll<Result<(), std::io::Error>> {
|
||||
let mut tbuf = vec![0; buf.remaining()];
|
||||
match Pin::new(&mut self.inner).poll_read(cx, &mut tbuf) {
|
||||
Poll::Ready(Ok(n)) => {
|
||||
buf.put_slice(&tbuf[..n]);
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsyncWrite + Unpin> hyper::rt::Write for HyperIo<T> {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<Result<usize, std::io::Error>> {
|
||||
Pin::new(&mut self.inner).poll_write(cx, buf)
|
||||
}
|
||||
|
||||
fn poll_flush(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Result<(), std::io::Error>> {
|
||||
Pin::new(&mut self.inner).poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Result<(), std::io::Error>> {
|
||||
Pin::new(&mut self.inner).poll_close(cx)
|
||||
}
|
||||
}
|
||||
|
||||
type PendingResponseMap = HashMap<Uuid, oneshot::Sender<Result<Value, String>>>;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Message {
|
||||
id: Uuid,
|
||||
error: Option<String>,
|
||||
result: Option<Value>,
|
||||
}
|
||||
|
||||
impl Message {
|
||||
fn into_result(self) -> Result<Value, String> {
|
||||
if let Some(error) = self.error {
|
||||
Err(error)
|
||||
} else {
|
||||
Ok(self.result.unwrap_or(Value::Null))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum RequestMethod {
|
||||
GetPublicKey,
|
||||
SignEvent,
|
||||
Nip04Encrypt,
|
||||
Nip04Decrypt,
|
||||
Nip44Encrypt,
|
||||
Nip44Decrypt,
|
||||
}
|
||||
|
||||
impl RequestMethod {
|
||||
fn as_str(&self) -> &str {
|
||||
match self {
|
||||
Self::GetPublicKey => "get_public_key",
|
||||
Self::SignEvent => "sign_event",
|
||||
Self::Nip04Encrypt => "nip04_encrypt",
|
||||
Self::Nip04Decrypt => "nip04_decrypt",
|
||||
Self::Nip44Encrypt => "nip44_encrypt",
|
||||
Self::Nip44Decrypt => "nip44_decrypt",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for RequestMethod {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct RequestData {
|
||||
id: Uuid,
|
||||
method: RequestMethod,
|
||||
params: Value,
|
||||
}
|
||||
|
||||
impl RequestData {
|
||||
#[inline]
|
||||
fn new(method: RequestMethod, params: Value) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
method,
|
||||
params,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Requests<'a> {
|
||||
requests: &'a [RequestData],
|
||||
}
|
||||
|
||||
impl<'a> Requests<'a> {
|
||||
#[inline]
|
||||
fn new(requests: &'a [RequestData]) -> Self {
|
||||
Self { requests }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn len(&self) -> usize {
|
||||
self.requests.len()
|
||||
}
|
||||
}
|
||||
|
||||
/// Params for NIP-04 and NIP-44 encryption/decryption
|
||||
#[derive(Serialize)]
|
||||
struct CryptoParams<'a> {
|
||||
public_key: &'a PublicKey,
|
||||
content: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> CryptoParams<'a> {
|
||||
#[inline]
|
||||
fn new(public_key: &'a PublicKey, content: &'a str) -> Self {
|
||||
Self {
|
||||
public_key,
|
||||
content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ProxyState {
|
||||
/// Requests waiting to be picked up by browser
|
||||
pub outgoing_requests: Mutex<Vec<RequestData>>,
|
||||
/// Map of request ID to response sender
|
||||
pub pending_responses: Mutex<PendingResponseMap>,
|
||||
/// Last time the client ask for the pending requests
|
||||
pub last_pending_request: Arc<AtomicU64>,
|
||||
/// Notification for shutdown
|
||||
pub shutdown_notify: smol::channel::Receiver<()>,
|
||||
pub shutdown_sender: smol::channel::Sender<()>,
|
||||
}
|
||||
|
||||
/// Configuration options for [`BrowserSignerProxy`].
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BrowserSignerProxyOptions {
|
||||
/// Request timeout for the signer extension. Default is 30 seconds.
|
||||
pub timeout: Duration,
|
||||
/// Proxy server IP address and port. Default is `127.0.0.1:7400`.
|
||||
pub addr: SocketAddr,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct InnerBrowserSignerProxy {
|
||||
/// Configuration options for the proxy
|
||||
options: BrowserSignerProxyOptions,
|
||||
/// Internal state of the proxy including request queues
|
||||
state: Arc<ProxyState>,
|
||||
/// Flag to indicate if the server is shutdown
|
||||
is_shutdown: Arc<AtomicBool>,
|
||||
/// Flat indicating if the server is started
|
||||
is_started: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl AtomicDestroyer for InnerBrowserSignerProxy {
|
||||
fn on_destroy(&self) {
|
||||
self.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
impl InnerBrowserSignerProxy {
|
||||
#[inline]
|
||||
fn is_shutdown(&self) -> bool {
|
||||
self.is_shutdown.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
fn shutdown(&self) {
|
||||
// Mark the server as shutdown
|
||||
self.is_shutdown.store(true, Ordering::SeqCst);
|
||||
|
||||
// Notify all waiters that the proxy is shutting down
|
||||
let _ = self.state.shutdown_sender.try_send(());
|
||||
}
|
||||
}
|
||||
|
||||
/// Nostr Browser Signer Proxy
|
||||
///
|
||||
/// Proxy to use Nostr Browser signer (NIP-07) in native applications.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BrowserSignerProxy {
|
||||
inner: AtomicDestructor<InnerBrowserSignerProxy>,
|
||||
}
|
||||
|
||||
impl Default for BrowserSignerProxyOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
timeout: Duration::from_secs(30),
|
||||
// 7 for NIP-07 and 400 because the NIP title is 40 bytes :)
|
||||
addr: SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 7400)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BrowserSignerProxyOptions {
|
||||
/// Sets the timeout duration.
|
||||
pub const fn timeout(mut self, timeout: Duration) -> Self {
|
||||
self.timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the IP address.
|
||||
pub const fn ip_addr(mut self, new_ip: IpAddr) -> Self {
|
||||
self.addr = SocketAddr::new(new_ip, self.addr.port());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the port number.
|
||||
pub const fn port(mut self, new_port: u16) -> Self {
|
||||
self.addr = SocketAddr::new(self.addr.ip(), new_port);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl BrowserSignerProxy {
|
||||
/// Construct a new browser signer proxy
|
||||
pub fn new(options: BrowserSignerProxyOptions) -> Self {
|
||||
let (shutdown_sender, shutdown_notify) = smol::channel::unbounded();
|
||||
let state = ProxyState {
|
||||
outgoing_requests: Mutex::new(Vec::new()),
|
||||
pending_responses: Mutex::new(HashMap::new()),
|
||||
last_pending_request: Arc::new(AtomicU64::new(0)),
|
||||
shutdown_notify,
|
||||
shutdown_sender,
|
||||
};
|
||||
|
||||
Self {
|
||||
inner: AtomicDestructor::new(InnerBrowserSignerProxy {
|
||||
options,
|
||||
state: Arc::new(state),
|
||||
is_shutdown: Arc::new(AtomicBool::new(false)),
|
||||
is_started: Arc::new(AtomicBool::new(false)),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Indicates whether the server is currently running.
|
||||
#[inline]
|
||||
pub fn is_started(&self) -> bool {
|
||||
self.inner.is_started.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
/// Checks if there is an open browser tap ready to respond to requests by
|
||||
/// verifying the time since the last pending request.
|
||||
#[inline]
|
||||
pub fn is_session_active(&self) -> bool {
|
||||
current_time() - self.inner.state.last_pending_request.load(Ordering::SeqCst) < 2
|
||||
}
|
||||
|
||||
/// Get the signer proxy webpage URL
|
||||
#[inline]
|
||||
pub fn url(&self) -> String {
|
||||
format!("http://{}", self.inner.options.addr)
|
||||
}
|
||||
|
||||
/// Start the proxy
|
||||
///
|
||||
/// If this is not called, will be automatically started on the first interaction with the signer.
|
||||
pub async fn start(&self) -> Result<(), Error> {
|
||||
// Ensure is not shutdown
|
||||
if self.inner.is_shutdown() {
|
||||
return Err(Error::Shutdown);
|
||||
}
|
||||
|
||||
// Mark the proxy as started and check if was already started
|
||||
let is_started: bool = self.inner.is_started.swap(true, Ordering::SeqCst);
|
||||
|
||||
// Immediately return if already started
|
||||
if is_started {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let listener = match smol::Async::<TcpListener>::bind(self.inner.options.addr) {
|
||||
Ok(listener) => listener,
|
||||
Err(e) => {
|
||||
// Undo the started flag if binding fails
|
||||
self.inner.is_started.store(false, Ordering::SeqCst);
|
||||
|
||||
// Propagate error
|
||||
return Err(Error::from(e));
|
||||
}
|
||||
};
|
||||
|
||||
let addr: SocketAddr = self.inner.options.addr;
|
||||
let state: Arc<ProxyState> = self.inner.state.clone();
|
||||
|
||||
smol::spawn(async move {
|
||||
log::info!("Starting proxy server on {addr}");
|
||||
|
||||
loop {
|
||||
futures::select! {
|
||||
accept_result = listener.accept().fuse() => {
|
||||
let (stream, _) = match accept_result {
|
||||
Ok(conn) => conn,
|
||||
Err(e) => {
|
||||
log::error!("Failed to accept connection: {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let io = HyperIo::new(stream);
|
||||
let state: Arc<ProxyState> = state.clone();
|
||||
let shutdown_notify = state.shutdown_notify.clone();
|
||||
|
||||
smol::spawn(async move {
|
||||
let service = service_fn(move |req| {
|
||||
handle_request(req, state.clone())
|
||||
});
|
||||
|
||||
futures::select! {
|
||||
res = http1::Builder::new().serve_connection(io, service).fuse() => {
|
||||
if let Err(e) = res {
|
||||
log::error!("Error serving connection: {e}");
|
||||
}
|
||||
}
|
||||
_ = shutdown_notify.recv().fuse() => {
|
||||
log::debug!("Closing connection, proxy server is shutting down.");
|
||||
}
|
||||
}
|
||||
}).detach();
|
||||
},
|
||||
_ = state.shutdown_notify.recv().fuse() => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("Shutting down proxy server.");
|
||||
}).detach();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
async fn store_pending_response(&self, id: Uuid, tx: oneshot::Sender<Result<Value, String>>) {
|
||||
let mut pending_responses = self.inner.state.pending_responses.lock().await;
|
||||
pending_responses.insert(id, tx);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
async fn store_outgoing_request(&self, request: RequestData) {
|
||||
let mut outgoing_requests = self.inner.state.outgoing_requests.lock().await;
|
||||
outgoing_requests.push(request);
|
||||
}
|
||||
|
||||
async fn request<T>(&self, method: RequestMethod, params: Value) -> Result<T, Error>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
// Start the proxy if not already started
|
||||
self.start().await?;
|
||||
|
||||
// Construct the request
|
||||
let request: RequestData = RequestData::new(method, params);
|
||||
|
||||
// Create a oneshot channel
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
// Store the response sender
|
||||
self.store_pending_response(request.id, tx).await;
|
||||
|
||||
// Add to outgoing requests queue
|
||||
self.store_outgoing_request(request).await;
|
||||
|
||||
// Wait for response
|
||||
let timeout_fut = smol::Timer::after(self.inner.options.timeout);
|
||||
let recv_fut = rx;
|
||||
|
||||
match futures::future::select(timeout_fut, recv_fut).await {
|
||||
futures::future::Either::Left(_) => Err(Error::Timeout),
|
||||
futures::future::Either::Right((recv_result, _)) => {
|
||||
match recv_result.map_err(|_| Error::Generic("Channel closed".to_string()))? {
|
||||
Ok(res) => Ok(serde_json::from_value(res)?),
|
||||
Err(error) => Err(Error::Generic(error)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
async fn _get_public_key(&self) -> Result<PublicKey, Error> {
|
||||
self.request(RequestMethod::GetPublicKey, json!({})).await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
async fn _sign_event(&self, event: UnsignedEvent) -> Result<Event, Error> {
|
||||
let event: Event = self
|
||||
.request(RequestMethod::SignEvent, serde_json::to_value(event)?)
|
||||
.await?;
|
||||
event.verify()?;
|
||||
Ok(event)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
async fn _nip04_encrypt(&self, public_key: &PublicKey, content: &str) -> Result<String, Error> {
|
||||
let params = CryptoParams::new(public_key, content);
|
||||
self.request(RequestMethod::Nip04Encrypt, serde_json::to_value(params)?)
|
||||
.await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
async fn _nip04_decrypt(&self, public_key: &PublicKey, content: &str) -> Result<String, Error> {
|
||||
let params = CryptoParams::new(public_key, content);
|
||||
self.request(RequestMethod::Nip04Decrypt, serde_json::to_value(params)?)
|
||||
.await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
async fn _nip44_encrypt(&self, public_key: &PublicKey, content: &str) -> Result<String, Error> {
|
||||
let params = CryptoParams::new(public_key, content);
|
||||
self.request(RequestMethod::Nip44Encrypt, serde_json::to_value(params)?)
|
||||
.await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
async fn _nip44_decrypt(&self, public_key: &PublicKey, content: &str) -> Result<String, Error> {
|
||||
let params = CryptoParams::new(public_key, content);
|
||||
self.request(RequestMethod::Nip44Decrypt, serde_json::to_value(params)?)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl NostrSigner for BrowserSignerProxy {
|
||||
fn backend(&self) -> SignerBackend {
|
||||
SignerBackend::BrowserExtension
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_public_key(&self) -> BoxedFuture<Result<PublicKey, SignerError>> {
|
||||
Box::pin(async move { self._get_public_key().await.map_err(SignerError::backend) })
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn sign_event(&self, unsigned: UnsignedEvent) -> BoxedFuture<Result<Event, SignerError>> {
|
||||
Box::pin(async move {
|
||||
self._sign_event(unsigned)
|
||||
.await
|
||||
.map_err(SignerError::backend)
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn nip04_encrypt<'a>(
|
||||
&'a self,
|
||||
public_key: &'a PublicKey,
|
||||
content: &'a str,
|
||||
) -> BoxedFuture<'a, Result<String, SignerError>> {
|
||||
Box::pin(async move {
|
||||
self._nip04_encrypt(public_key, content)
|
||||
.await
|
||||
.map_err(SignerError::backend)
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn nip04_decrypt<'a>(
|
||||
&'a self,
|
||||
public_key: &'a PublicKey,
|
||||
encrypted_content: &'a str,
|
||||
) -> BoxedFuture<'a, Result<String, SignerError>> {
|
||||
Box::pin(async move {
|
||||
self._nip04_decrypt(public_key, encrypted_content)
|
||||
.await
|
||||
.map_err(SignerError::backend)
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn nip44_encrypt<'a>(
|
||||
&'a self,
|
||||
public_key: &'a PublicKey,
|
||||
content: &'a str,
|
||||
) -> BoxedFuture<'a, Result<String, SignerError>> {
|
||||
Box::pin(async move {
|
||||
self._nip44_encrypt(public_key, content)
|
||||
.await
|
||||
.map_err(SignerError::backend)
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn nip44_decrypt<'a>(
|
||||
&'a self,
|
||||
public_key: &'a PublicKey,
|
||||
payload: &'a str,
|
||||
) -> BoxedFuture<'a, Result<String, SignerError>> {
|
||||
Box::pin(async move {
|
||||
self._nip44_decrypt(public_key, payload)
|
||||
.await
|
||||
.map_err(SignerError::backend)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_request(
|
||||
req: Request<Incoming>,
|
||||
state: Arc<ProxyState>,
|
||||
) -> Result<Response<BoxBody<Bytes, Error>>, Error> {
|
||||
match (req.method(), req.uri().path()) {
|
||||
// Serve the HTML proxy page
|
||||
(&Method::GET, "/") => Ok(Response::builder()
|
||||
.header("Content-Type", "text/html")
|
||||
.body(full(HTML))?),
|
||||
// Serve the CSS page style
|
||||
(&Method::GET, "/style.css") => Ok(Response::builder()
|
||||
.header("Content-Type", "text/css")
|
||||
.body(full(CSS))?),
|
||||
// Serve the JS proxy script
|
||||
(&Method::GET, "/proxy.js") => Ok(Response::builder()
|
||||
.header("Content-Type", "application/javascript")
|
||||
.body(full(JS))?),
|
||||
// Browser polls this endpoint to get pending requests
|
||||
(&Method::GET, "/api/pending") => {
|
||||
state
|
||||
.last_pending_request
|
||||
.store(current_time(), Ordering::SeqCst);
|
||||
|
||||
let mut outgoing = state.outgoing_requests.lock().await;
|
||||
|
||||
let requests: Requests<'_> = Requests::new(&outgoing);
|
||||
let json: String = serde_json::to_string(&requests)?;
|
||||
|
||||
log::debug!("Sending {} pending requests to browser", requests.len());
|
||||
|
||||
// Clear the outgoing requests after sending them
|
||||
outgoing.clear();
|
||||
|
||||
Ok(Response::builder()
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Access-Control-Allow-Origin", "*")
|
||||
.body(full(json))?)
|
||||
}
|
||||
// Get response
|
||||
(&Method::POST, "/api/response") => {
|
||||
// Correctly collect the body bytes from the stream
|
||||
let body_bytes: Bytes = match req.into_body().collect().await {
|
||||
Ok(collected) => collected.to_bytes(),
|
||||
Err(e) => {
|
||||
log::error!("Failed to read body: {e}");
|
||||
let response = Response::builder()
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.body(full("Failed to read body"))?;
|
||||
return Ok(response);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle responses from the browser extension
|
||||
let message: Message = match serde_json::from_slice(&body_bytes) {
|
||||
Ok(json) => json,
|
||||
Err(_) => {
|
||||
let response = Response::builder()
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.body(full("Invalid JSON"))?;
|
||||
return Ok(response);
|
||||
}
|
||||
};
|
||||
|
||||
log::debug!("Received response from browser: {message:?}");
|
||||
|
||||
let id: Uuid = message.id;
|
||||
let mut pending = state.pending_responses.lock().await;
|
||||
|
||||
match pending.remove(&id) {
|
||||
Some(sender) => {
|
||||
let _ = sender.send(message.into_result());
|
||||
}
|
||||
None => log::warn!("No pending request found for {id}"),
|
||||
}
|
||||
|
||||
let response = Response::builder()
|
||||
.header("Access-Control-Allow-Origin", "*")
|
||||
.body(full("OK"))?;
|
||||
Ok(response)
|
||||
}
|
||||
(&Method::OPTIONS, _) => {
|
||||
// Handle CORS preflight requests
|
||||
let response = Response::builder()
|
||||
.header("Access-Control-Allow-Origin", "*")
|
||||
.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
.header("Access-Control-Allow-Headers", "Content-Type")
|
||||
.body(full(""))?;
|
||||
Ok(response)
|
||||
}
|
||||
// 404 - not found
|
||||
_ => {
|
||||
let response = Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(full("Not Found"))?;
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn full<T: Into<Bytes>>(chunk: T) -> BoxBody<Bytes, Error> {
|
||||
Full::new(chunk.into())
|
||||
.map_err(|never| match never {})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
/// Gets the current time in seconds since the Unix epoch (1970-01-01). If the
|
||||
/// time is before the epoch, returns 0.
|
||||
#[inline]
|
||||
fn current_time() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
body {
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans,
|
||||
Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||
padding: 20px;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.connected {
|
||||
color: green;
|
||||
font-weight: bold;
|
||||
}
|
||||
.error {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
.status-box {
|
||||
background: #f9f9f9;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
border-left: 4px solid #ccc;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "app_state"
|
||||
name = "states"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
@@ -1,9 +1,6 @@
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Duration;
|
||||
|
||||
use nostr_lmdb::NostrLMDB;
|
||||
use nostr_sdk::prelude::*;
|
||||
use paths::nostr_file;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
@@ -12,7 +9,6 @@ pub mod paths;
|
||||
pub mod state;
|
||||
|
||||
static APP_STATE: OnceLock<AppState> = OnceLock::new();
|
||||
static NOSTR_CLIENT: OnceLock<Client> = OnceLock::new();
|
||||
static NIP65_RELAYS: OnceLock<Vec<(RelayUrl, Option<RelayMetadata>)>> = OnceLock::new();
|
||||
static NIP17_RELAYS: OnceLock<Vec<RelayUrl>> = OnceLock::new();
|
||||
|
||||
@@ -21,31 +17,7 @@ pub fn app_state() -> &'static AppState {
|
||||
APP_STATE.get_or_init(AppState::new)
|
||||
}
|
||||
|
||||
/// Initialize the nostr client.
|
||||
pub fn nostr_client() -> &'static Client {
|
||||
NOSTR_CLIENT.get_or_init(|| {
|
||||
// rustls uses the `aws_lc_rs` provider by default
|
||||
// This only errors if the default provider has already
|
||||
// been installed. We can ignore this `Result`.
|
||||
rustls::crypto::aws_lc_rs::default_provider()
|
||||
.install_default()
|
||||
.ok();
|
||||
|
||||
let lmdb = NostrLMDB::open(nostr_file()).expect("Database is NOT initialized");
|
||||
|
||||
let opts = ClientOptions::new()
|
||||
.gossip(true)
|
||||
.automatic_authentication(false)
|
||||
.verify_subscriptions(false)
|
||||
.sleep_when_idle(SleepWhenIdle::Enabled {
|
||||
timeout: Duration::from_secs(600),
|
||||
});
|
||||
|
||||
ClientBuilder::default().database(lmdb).opts(opts).build()
|
||||
})
|
||||
}
|
||||
|
||||
/// Default NIP65 Relays. Used for new account
|
||||
/// Default NIP-65 Relays. Used for new account
|
||||
pub fn default_nip65_relays() -> &'static Vec<(RelayUrl, Option<RelayMetadata>)> {
|
||||
NIP65_RELAYS.get_or_init(|| {
|
||||
vec![
|
||||
@@ -71,7 +43,7 @@ pub fn default_nip65_relays() -> &'static Vec<(RelayUrl, Option<RelayMetadata>)>
|
||||
})
|
||||
}
|
||||
|
||||
/// Default NIP17 Relays. Used for new account
|
||||
/// Default NIP-17 Relays. Used for new account
|
||||
pub fn default_nip17_relays() -> &'static Vec<RelayUrl> {
|
||||
NIP17_RELAYS.get_or_init(|| {
|
||||
vec![
|
||||
@@ -5,14 +5,16 @@ use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Error};
|
||||
use flume::{Receiver, Sender};
|
||||
use nostr_lmdb::NostrLMDB;
|
||||
use nostr_sdk::prelude::*;
|
||||
use smol::lock::RwLock;
|
||||
|
||||
use crate::constants::{
|
||||
BOOTSTRAP_RELAYS, METADATA_BATCH_LIMIT, METADATA_BATCH_TIMEOUT, SEARCH_RELAYS,
|
||||
};
|
||||
use crate::nostr_client;
|
||||
use crate::paths::support_dir;
|
||||
use crate::paths::config_dir;
|
||||
|
||||
const TIMEOUT: u64 = 5;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct AuthRequest {
|
||||
@@ -51,9 +53,6 @@ pub enum SignalKind {
|
||||
/// A signal to notify UI that the relay requires authentication
|
||||
Auth(AuthRequest),
|
||||
|
||||
/// A signal to notify UI that the browser proxy service is down
|
||||
ProxyDown,
|
||||
|
||||
/// A signal to notify UI that a new profile has been received
|
||||
NewProfile(Profile),
|
||||
|
||||
@@ -70,7 +69,7 @@ pub enum SignalKind {
|
||||
GiftWrapStatus(UnwrappingStatus),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Signal {
|
||||
rx: Receiver<SignalKind>,
|
||||
tx: Sender<SignalKind>,
|
||||
@@ -103,7 +102,7 @@ impl Signal {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Ingester {
|
||||
rx: Receiver<PublicKey>,
|
||||
tx: Sender<PublicKey>,
|
||||
@@ -165,32 +164,25 @@ impl EventTracker {
|
||||
}
|
||||
}
|
||||
|
||||
/// A simple storage to store all states that using across the application.
|
||||
#[derive(Debug)]
|
||||
pub struct AppState {
|
||||
/// A client to interact with Nostr
|
||||
client: Client,
|
||||
|
||||
/// Tracks activity related to Nostr events
|
||||
event_tracker: RwLock<EventTracker>,
|
||||
|
||||
/// Signal channel for communication between Nostr and GPUI
|
||||
signal: Signal,
|
||||
|
||||
/// Ingester channel for processing public keys
|
||||
ingester: Ingester,
|
||||
|
||||
/// The timestamp when the application was initialized.
|
||||
pub initialized_at: Timestamp,
|
||||
|
||||
/// Whether this is the first run of the application.
|
||||
pub is_first_run: AtomicBool,
|
||||
|
||||
/// Whether gift wrap processing is in progress.
|
||||
pub gift_wrap_processing: AtomicBool,
|
||||
|
||||
/// Subscription ID for listening to gift wrap events from relays.
|
||||
pub gift_wrap_sub_id: SubscriptionId,
|
||||
|
||||
/// Auto-close options for relay subscriptions
|
||||
pub auto_close_opts: Option<SubscribeAutoCloseOptions>,
|
||||
|
||||
/// Tracks activity related to Nostr events
|
||||
pub event_tracker: RwLock<EventTracker>,
|
||||
|
||||
/// Signal channel for communication between Nostr and GPUI
|
||||
pub signal: Signal,
|
||||
|
||||
/// Ingester channel for processing public keys
|
||||
pub ingester: Ingester,
|
||||
}
|
||||
|
||||
impl Default for AppState {
|
||||
@@ -201,28 +193,127 @@ impl Default for AppState {
|
||||
|
||||
impl AppState {
|
||||
pub fn new() -> Self {
|
||||
let first_run = Self::first_run();
|
||||
let initialized_at = Timestamp::now();
|
||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||
// rustls uses the `aws_lc_rs` provider by default
|
||||
// This only errors if the default provider has already
|
||||
// been installed. We can ignore this `Result`.
|
||||
rustls::crypto::aws_lc_rs::default_provider()
|
||||
.install_default()
|
||||
.ok();
|
||||
|
||||
let lmdb =
|
||||
NostrLMDB::open(config_dir().join("nostr")).expect("Database is NOT initialized");
|
||||
|
||||
let opts = ClientOptions::new()
|
||||
.gossip(true)
|
||||
.automatic_authentication(false)
|
||||
.verify_subscriptions(false)
|
||||
.sleep_when_idle(SleepWhenIdle::Enabled {
|
||||
timeout: Duration::from_secs(600),
|
||||
});
|
||||
|
||||
let client = ClientBuilder::default().database(lmdb).opts(opts).build();
|
||||
let event_tracker = RwLock::new(EventTracker::default());
|
||||
|
||||
let signal = Signal::default();
|
||||
let ingester = Ingester::default();
|
||||
|
||||
Self {
|
||||
initialized_at,
|
||||
client,
|
||||
event_tracker,
|
||||
signal,
|
||||
ingester,
|
||||
is_first_run: AtomicBool::new(first_run),
|
||||
gift_wrap_sub_id: SubscriptionId::new("inbox"),
|
||||
initialized_at: Timestamp::now(),
|
||||
gift_wrap_processing: AtomicBool::new(false),
|
||||
auto_close_opts: Some(opts),
|
||||
event_tracker: RwLock::new(EventTracker::default()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_notifications(&self) -> Result<(), Error> {
|
||||
let client = nostr_client();
|
||||
/// Returns a reference to the nostr client
|
||||
pub fn client(&'static self) -> &'static Client {
|
||||
&self.client
|
||||
}
|
||||
|
||||
/// Returns a reference to the event tracker
|
||||
pub fn tracker(&'static self) -> &'static RwLock<EventTracker> {
|
||||
&self.event_tracker
|
||||
}
|
||||
|
||||
/// Returns a reference to the signal channel
|
||||
pub fn signal(&'static self) -> &'static Signal {
|
||||
&self.signal
|
||||
}
|
||||
|
||||
/// Returns a reference to the ingester channel
|
||||
pub fn ingester(&'static self) -> &'static Ingester {
|
||||
&self.ingester
|
||||
}
|
||||
|
||||
/// Observes the signer and notifies the app when it's set
|
||||
pub async fn observe_signer(&'static self) {
|
||||
let client = self.client();
|
||||
let loop_duration = Duration::from_millis(800);
|
||||
|
||||
loop {
|
||||
if let Ok(signer) = client.signer().await {
|
||||
if let Ok(pk) = signer.get_public_key().await {
|
||||
// Notify the app that the signer has been set
|
||||
self.signal().send(SignalKind::SignerSet(pk)).await;
|
||||
|
||||
// Get user's gossip relays
|
||||
self.get_nip65(pk).await.ok();
|
||||
|
||||
// Exit the current loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
smol::Timer::after(loop_duration).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Observes the gift wrap status and notifies the app when it's set
|
||||
pub async fn observe_giftwrap(&'static self) {
|
||||
let client = self.client();
|
||||
let loop_duration = Duration::from_secs(20);
|
||||
let mut is_start_processing = false;
|
||||
let mut total_loops = 0;
|
||||
|
||||
loop {
|
||||
if client.has_signer().await {
|
||||
total_loops += 1;
|
||||
|
||||
if self.gift_wrap_processing.load(Ordering::Acquire) {
|
||||
is_start_processing = true;
|
||||
|
||||
// Reset gift wrap processing flag
|
||||
let _ = self.gift_wrap_processing.compare_exchange(
|
||||
true,
|
||||
false,
|
||||
Ordering::Release,
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
|
||||
let signal = SignalKind::GiftWrapStatus(UnwrappingStatus::Processing);
|
||||
self.signal().send(signal).await;
|
||||
} else {
|
||||
// Only run further if we are already processing
|
||||
// Wait until after 2 loops to prevent exiting early while events are still being processed
|
||||
if is_start_processing && total_loops >= 2 {
|
||||
let signal = SignalKind::GiftWrapStatus(UnwrappingStatus::Complete);
|
||||
self.signal().send(signal).await;
|
||||
|
||||
// Reset the counter
|
||||
is_start_processing = false;
|
||||
total_loops = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
smol::Timer::after(loop_duration).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles events from the nostr client
|
||||
pub async fn handle_notifications(&self) -> Result<(), Error> {
|
||||
// Get all bootstrapping relays
|
||||
let mut urls = vec![];
|
||||
urls.extend(BOOTSTRAP_RELAYS);
|
||||
@@ -230,15 +321,15 @@ impl AppState {
|
||||
|
||||
// Add relay to the relay pool
|
||||
for url in urls.into_iter() {
|
||||
client.add_relay(url).await?;
|
||||
self.client.add_relay(url).await?;
|
||||
}
|
||||
|
||||
// Establish connection to relays
|
||||
client.connect().await;
|
||||
self.client.connect().await;
|
||||
|
||||
let mut processed_events: HashSet<EventId> = HashSet::new();
|
||||
let mut challenges: HashSet<Cow<'_, str>> = HashSet::new();
|
||||
let mut notifications = client.notifications();
|
||||
let mut notifications = self.client.notifications();
|
||||
|
||||
while let Ok(notification) = notifications.recv().await {
|
||||
let RelayPoolNotification::Message { message, relay_url } = notification else {
|
||||
@@ -265,7 +356,7 @@ impl AppState {
|
||||
match event.kind {
|
||||
Kind::RelayList => {
|
||||
// Get events if relay list belongs to current user
|
||||
if let Ok(true) = Self::is_self_authored(&event).await {
|
||||
if let Ok(true) = self.is_self_authored(&event).await {
|
||||
let author = event.pubkey;
|
||||
|
||||
// Fetch user's metadata event
|
||||
@@ -286,7 +377,7 @@ impl AppState {
|
||||
}
|
||||
Kind::InboxRelays => {
|
||||
// Subscribe to gift wrap events if messaging relays belong to the current user
|
||||
if let Ok(true) = Self::is_self_authored(&event).await {
|
||||
if let Ok(true) = self.is_self_authored(&event).await {
|
||||
let urls: Vec<RelayUrl> =
|
||||
nip17::extract_relay_list(event.as_ref()).cloned().collect();
|
||||
|
||||
@@ -296,7 +387,7 @@ impl AppState {
|
||||
}
|
||||
}
|
||||
Kind::ContactList => {
|
||||
if let Ok(true) = Self::is_self_authored(&event).await {
|
||||
if let Ok(true) = self.is_self_authored(&event).await {
|
||||
let public_keys: HashSet<PublicKey> =
|
||||
event.tags.public_keys().copied().collect();
|
||||
|
||||
@@ -318,7 +409,7 @@ impl AppState {
|
||||
}
|
||||
}
|
||||
RelayMessage::EndOfStoredEvents(subscription_id) => {
|
||||
if *subscription_id == self.gift_wrap_sub_id {
|
||||
if subscription_id.as_ref() == &SubscriptionId::new("inbox") {
|
||||
self.signal
|
||||
.send(SignalKind::GiftWrapStatus(UnwrappingStatus::Processing))
|
||||
.await;
|
||||
@@ -353,6 +444,7 @@ impl AppState {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Batch metadata requests into a single subscription
|
||||
pub async fn handle_metadata_batching(&self) {
|
||||
let timeout = Duration::from_millis(METADATA_BATCH_TIMEOUT);
|
||||
let mut processed_pubkeys: HashSet<PublicKey> = HashSet::new();
|
||||
@@ -411,41 +503,46 @@ impl AppState {
|
||||
}
|
||||
}
|
||||
|
||||
async fn is_self_authored(event: &Event) -> Result<bool, Error> {
|
||||
let client = nostr_client();
|
||||
let signer = client.signer().await?;
|
||||
/// 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?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
Ok(public_key == event.pubkey)
|
||||
}
|
||||
|
||||
/// Subscribe for events that match the given kind for a given author
|
||||
async fn subscribe(&self, author: PublicKey, kind: Kind) -> Result<(), Error> {
|
||||
let client = nostr_client();
|
||||
pub async fn subscribe(&self, author: PublicKey, kind: Kind) -> Result<(), Error> {
|
||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||
let filter = Filter::new().author(author).kind(kind).limit(1);
|
||||
|
||||
// Subscribe to filters from the user's write relays
|
||||
client.subscribe(filter, Some(opts)).await?;
|
||||
self.client.subscribe(filter, Some(opts)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get metadata for a list of public keys
|
||||
async fn get_metadata_for_list(&self, public_keys: HashSet<PublicKey>) -> Result<(), Error> {
|
||||
if public_keys.is_empty() {
|
||||
return Err(anyhow!("You need at least one public key"));
|
||||
pub async fn get_metadata_for_list<I>(&self, public_keys: I) -> Result<(), Error>
|
||||
where
|
||||
I: IntoIterator<Item = PublicKey>,
|
||||
{
|
||||
let authors: Vec<PublicKey> = public_keys.into_iter().collect();
|
||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||
let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList];
|
||||
|
||||
// Return if the list is empty
|
||||
if authors.is_empty() {
|
||||
return Err(anyhow!("You need at least one public key".to_string(),));
|
||||
}
|
||||
|
||||
let client = nostr_client();
|
||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||
|
||||
let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList];
|
||||
let limit = public_keys.len() * kinds.len() + 20;
|
||||
let filter = Filter::new().authors(public_keys).kinds(kinds).limit(limit);
|
||||
let filter = Filter::new()
|
||||
.limit(authors.len() * kinds.len() + 20)
|
||||
.authors(authors)
|
||||
.kinds(kinds);
|
||||
|
||||
// Subscribe to filters to the bootstrap relays
|
||||
client
|
||||
self.client
|
||||
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
|
||||
.await?;
|
||||
|
||||
@@ -454,9 +551,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 client = nostr_client();
|
||||
let tx = self.signal.sender().clone();
|
||||
let timeout = Duration::from_secs(5);
|
||||
let timeout = Duration::from_secs(TIMEOUT);
|
||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||
|
||||
let filter = Filter::new()
|
||||
@@ -465,15 +560,18 @@ impl AppState {
|
||||
.limit(1);
|
||||
|
||||
// Subscribe to events from the bootstrapping relays
|
||||
client
|
||||
self.client
|
||||
.subscribe_to(BOOTSTRAP_RELAYS, 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 client.database().count(filter).await.unwrap_or(0) < 1 {
|
||||
if database.count(filter).await.unwrap_or(0) < 1 {
|
||||
tx.send_async(SignalKind::GossipRelaysNotFound).await.ok();
|
||||
}
|
||||
})
|
||||
@@ -487,12 +585,12 @@ impl AppState {
|
||||
&self,
|
||||
relays: &[(RelayUrl, Option<RelayMetadata>)],
|
||||
) -> Result<(), Error> {
|
||||
let client = nostr_client();
|
||||
let signer = client.signer().await?;
|
||||
let signer = self.client.signer().await?;
|
||||
|
||||
let tags: Vec<Tag> = relays
|
||||
.iter()
|
||||
.map(|(url, metadata)| Tag::relay_metadata(url.to_owned(), metadata.to_owned()))
|
||||
.cloned()
|
||||
.map(|(url, metadata)| Tag::relay_metadata(url, metadata))
|
||||
.collect();
|
||||
|
||||
let event = EventBuilder::new(Kind::RelayList, "")
|
||||
@@ -501,7 +599,7 @@ impl AppState {
|
||||
.await?;
|
||||
|
||||
// Send event to the public relays
|
||||
client.send_event_to(BOOTSTRAP_RELAYS, &event).await?;
|
||||
self.client.send_event_to(BOOTSTRAP_RELAYS, &event).await?;
|
||||
|
||||
// Get NIP-17 relays
|
||||
self.get_nip17(event.pubkey).await?;
|
||||
@@ -511,9 +609,7 @@ impl AppState {
|
||||
|
||||
/// Get and verify NIP-17 relays for a given public key
|
||||
pub async fn get_nip17(&self, public_key: PublicKey) -> Result<(), Error> {
|
||||
let client = nostr_client();
|
||||
let tx = self.signal.sender().clone();
|
||||
let timeout = Duration::from_secs(5);
|
||||
let timeout = Duration::from_secs(TIMEOUT);
|
||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||
|
||||
let filter = Filter::new()
|
||||
@@ -522,13 +618,16 @@ impl AppState {
|
||||
.limit(1);
|
||||
|
||||
// Subscribe to events from the bootstrapping relays
|
||||
client.subscribe(filter.clone(), Some(opts)).await?;
|
||||
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 client.database().count(filter).await.unwrap_or(0) < 1 {
|
||||
if database.count(filter).await.unwrap_or(0) < 1 {
|
||||
tx.send_async(SignalKind::MessagingRelaysNotFound)
|
||||
.await
|
||||
.ok();
|
||||
@@ -541,26 +640,28 @@ impl AppState {
|
||||
|
||||
/// Set NIP-17 relays for a current user
|
||||
pub async fn set_nip17(&self, relays: &[RelayUrl]) -> Result<(), Error> {
|
||||
let client = nostr_client();
|
||||
let signer = client.signer().await?;
|
||||
let signer = self.client.signer().await?;
|
||||
|
||||
let event = EventBuilder::new(Kind::InboxRelays, "")
|
||||
.tags(relays.iter().map(|relay| Tag::relay(relay.to_owned())))
|
||||
.tags(relays.iter().cloned().map(Tag::relay))
|
||||
.sign(&signer)
|
||||
.await?;
|
||||
|
||||
// Send event to the public relays
|
||||
client.send_event(&event).await?;
|
||||
self.client.send_event(&event).await?;
|
||||
|
||||
// Run inbox monitor
|
||||
// Get all gift wrap events after published event
|
||||
self.get_messages(event.pubkey, relays).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get all gift wrap events in the messaging relays for a given public key
|
||||
async fn get_messages(&self, public_key: PublicKey, urls: &[RelayUrl]) -> Result<(), Error> {
|
||||
let client = nostr_client();
|
||||
pub async fn get_messages(
|
||||
&self,
|
||||
public_key: PublicKey,
|
||||
urls: &[RelayUrl],
|
||||
) -> Result<(), Error> {
|
||||
let id = SubscriptionId::new("inbox");
|
||||
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
|
||||
|
||||
@@ -571,22 +672,22 @@ impl AppState {
|
||||
|
||||
// Ensure connection to relays
|
||||
for url in urls.iter() {
|
||||
client.add_relay(url).await?;
|
||||
client.connect_relay(url).await?;
|
||||
self.client.add_relay(url).await?;
|
||||
self.client.connect_relay(url).await?;
|
||||
}
|
||||
|
||||
// Subscribe to filters to user's messaging relays
|
||||
client.subscribe_with_id_to(urls, id, filter, None).await?;
|
||||
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: &Event) -> Result<(), Error> {
|
||||
let client = nostr_client();
|
||||
|
||||
// Save unwrapped event
|
||||
client.database().save_event(rumor).await?;
|
||||
self.client.database().save_event(rumor).await?;
|
||||
|
||||
// Create a reference event pointing to the unwrapped event
|
||||
let event = EventBuilder::new(Kind::ApplicationSpecificData, "")
|
||||
@@ -595,23 +696,22 @@ impl AppState {
|
||||
.await?;
|
||||
|
||||
// Save reference event
|
||||
client.database().save_event(&event).await?;
|
||||
self.client.database().save_event(&event).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retrieves a previously unwrapped event from local database
|
||||
async fn get_rumor(&self, id: EventId) -> Result<Event, Error> {
|
||||
let client = nostr_client();
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::ApplicationSpecificData)
|
||||
.identifier(id)
|
||||
.limit(1);
|
||||
|
||||
if let Some(event) = client.database().query(filter).await?.first_owned() {
|
||||
if let Some(event) = self.client.database().query(filter).await?.first_owned() {
|
||||
let target_id = event.tags.event_ids().collect::<Vec<_>>()[0];
|
||||
|
||||
if let Some(event) = client.database().event_by_id(target_id).await? {
|
||||
if let Some(event) = self.client.database().event_by_id(target_id).await? {
|
||||
Ok(event)
|
||||
} else {
|
||||
Err(anyhow!("Event not found."))
|
||||
@@ -623,13 +723,11 @@ impl AppState {
|
||||
|
||||
// Unwraps a gift-wrapped event and processes its contents.
|
||||
async fn extract_rumor(&self, gift_wrap: &Event) {
|
||||
let client = nostr_client();
|
||||
|
||||
let mut rumor: Option<Event> = None;
|
||||
|
||||
if let Ok(event) = self.get_rumor(gift_wrap.id).await {
|
||||
rumor = Some(event);
|
||||
} else if let Ok(unwrapped) = client.unwrap_gift_wrap(gift_wrap).await {
|
||||
} else if let Ok(unwrapped) = self.client.unwrap_gift_wrap(gift_wrap).await {
|
||||
// Sign the unwrapped event with a RANDOM KEYS
|
||||
if let Ok(event) = unwrapped.rumor.sign_with_keys(&Keys::generate()) {
|
||||
// Save this event to the database for future use.
|
||||
@@ -661,9 +759,4 @@ impl AppState {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn first_run() -> bool {
|
||||
let flag = support_dir().join(".first_run");
|
||||
!flag.exists() && std::fs::write(&flag, "").is_ok()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user