chore: clean up codebase (#186)

* refactor app state

* clean up

* clean up

* .
This commit is contained in:
reya
2025-10-18 09:46:45 +07:00
committed by GitHub
parent 32a0401907
commit a1e0934fc3
37 changed files with 516 additions and 1716 deletions

View File

@@ -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

View File

@@ -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);

View File

@@ -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

View File

@@ -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()
}
}

View File

@@ -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

View File

@@ -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;

View File

@@ -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

View File

@@ -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 {

View File

@@ -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();

View File

@@ -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))

View File

@@ -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()

View File

@@ -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(())

View File

@@ -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

View File

@@ -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)];

View File

@@ -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 {

View File

@@ -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")),
),
),
),
)

View File

@@ -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);

View File

@@ -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(())
});

View File

@@ -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() {

View File

@@ -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?;

View File

@@ -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"

View File

@@ -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();
}

View File

@@ -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()

View File

@@ -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

View File

@@ -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?;

View File

@@ -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"

View File

@@ -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>

View File

@@ -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);
});

View File

@@ -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)
}
}

View File

@@ -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()
}

View File

@@ -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;
}

View File

@@ -1,5 +1,5 @@
[package]
name = "app_state"
name = "states"
version.workspace = true
edition.workspace = true
publish.workspace = true

View File

@@ -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![

View File

@@ -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()
}
}