refactor: Client keys and Identity (#61)

* .

* .

* .

* .

* refactor client keys

* .

* .

* refactor

* .

* .

* .

* update new account
This commit is contained in:
reya
2025-06-17 07:16:16 +07:00
committed by GitHub
parent cc36adeafe
commit 440f17af18
31 changed files with 1460 additions and 586 deletions

View File

@@ -1,29 +1,19 @@
//! Global state management for the Nostr client application.
//!
//! This module provides a singleton global state that manages:
//! - Nostr client connections and event handling
//! - User identity and profile management
//! - Batched metadata fetching for performance
//! - Cross-component communication via channels
use std::collections::{BTreeMap, BTreeSet};
use std::mem;
use std::sync::OnceLock;
use std::time::Duration;
use std::{fs, mem};
use anyhow::{anyhow, Error};
use constants::{
ALL_MESSAGES_SUB_ID, APP_ID, APP_PUBKEY, BOOTSTRAP_RELAYS, METADATA_BATCH_LIMIT,
METADATA_BATCH_TIMEOUT, NEW_MESSAGE_SUB_ID, SEARCH_RELAYS,
};
use nostr_keyring::prelude::*;
use nostr_sdk::prelude::*;
use paths::nostr_file;
use smol::lock::RwLock;
use crate::constants::{
BATCH_CHANNEL_LIMIT, GLOBAL_CHANNEL_LIMIT, KEYRING_PATH, NIP17_RELAYS, NIP65_RELAYS,
};
use crate::constants::{BATCH_CHANNEL_LIMIT, GLOBAL_CHANNEL_LIMIT};
use crate::paths::support_dir;
pub mod constants;
pub mod paths;
@@ -50,11 +40,9 @@ pub enum NostrSignal {
pub struct Globals {
/// The Nostr SDK client
pub client: Client,
/// Cryptographic keys for signing Nostr events
pub client_signer: Keys,
/// Current user's profile information (pubkey and metadata)
pub identity: RwLock<Option<Profile>>,
/// Auto-close options for subscriptions to prevent memory leaks
/// Determines if this is the first time user run Coop
pub first_run: bool,
/// Auto-close options for subscriptions
pub auto_close: Option<SubscribeAutoCloseOptions>,
/// Channel sender for broadcasting global Nostr events to UI
pub global_sender: smol::channel::Sender<NostrSignal>,
@@ -78,18 +66,7 @@ pub fn shared_state() -> &'static Globals {
.install_default()
.ok();
let keyring = NostrKeyring::new(KEYRING_PATH);
// Get the client signer or generate a new one if it doesn't exist
let client_signer = if let Ok(keys) = keyring.get("client") {
keys
} else {
let keys = Keys::generate();
if let Err(e) = keyring.set("client", &keys) {
log::error!("Failed to save client keys: {}", e);
}
keys
};
let first_run = is_first_run().unwrap_or(true);
let opts = Options::new().gossip(true);
let lmdb = NostrLMDB::open(nostr_file()).expect("Database is NOT initialized");
@@ -101,12 +78,11 @@ pub fn shared_state() -> &'static Globals {
Globals {
client: ClientBuilder::default().database(lmdb).opts(opts).build(),
identity: RwLock::new(None),
persons: RwLock::new(BTreeMap::new()),
auto_close: Some(
SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE),
),
client_signer,
first_run,
global_sender,
global_receiver,
batch_sender,
@@ -120,6 +96,7 @@ impl Globals {
pub async fn start(&self) {
self.connect().await;
self.subscribe_for_app_updates().await;
self.preload_metadata().await;
nostr_sdk::async_utility::task::spawn(async move {
let mut batch: BTreeSet<PublicKey> = BTreeSet::new();
@@ -225,112 +202,21 @@ impl Globals {
}
}
/// Sets a new signer for the client and updates user identity
pub async fn set_signer<S>(&self, signer: S) -> Result<(), Error>
where
S: NostrSigner + 'static,
{
let public_key = signer.get_public_key().await?;
// Update signer
self.client.set_signer(signer).await;
// Fetch user's metadata
let metadata = shared_state()
.client
.fetch_metadata(public_key, Duration::from_secs(2))
.await?
.unwrap_or_default();
let profile = Profile::new(public_key, metadata);
let mut identity_guard = self.identity.write().await;
// Update the identity
*identity_guard = Some(profile);
// Subscribe for user's data
nostr_sdk::async_utility::task::spawn(async move {
shared_state().subscribe_for_user_data().await;
});
// Notify GPUi via the global channel
self.global_sender.send(NostrSignal::SignerUpdated).await?;
Ok(())
}
pub async fn unset_signer(&self) {
self.client.reset().await;
if let Ok(signer) = self.client.signer().await {
if let Ok(public_key) = signer.get_public_key().await {
let file = support_dir().join(format!(".{}", public_key.to_bech32().unwrap()));
fs::remove_file(&file).ok();
}
}
if let Err(e) = self.global_sender.send(NostrSignal::SignerUnset).await {
log::error!("Failed to send signal to global channel: {}", e);
}
}
/// Creates a new account with the given keys and metadata
pub async fn new_account(&self, keys: Keys, metadata: Metadata) {
let profile = Profile::new(keys.public_key(), metadata.clone());
// Update signer
self.client.set_signer(keys).await;
// Set metadata
self.client.set_metadata(&metadata).await.ok();
// Create relay list
let builder = EventBuilder::new(Kind::RelayList, "").tags(
NIP65_RELAYS.into_iter().filter_map(|url| {
if let Ok(url) = RelayUrl::parse(url) {
Some(Tag::relay_metadata(url, None))
} else {
None
}
}),
);
if let Err(e) = self.client.send_event_builder(builder).await {
log::error!("Failed to send relay list event: {}", e);
};
// Create messaging relay list
let builder = EventBuilder::new(Kind::InboxRelays, "").tags(
NIP17_RELAYS.into_iter().filter_map(|url| {
if let Ok(url) = RelayUrl::parse(url) {
Some(Tag::relay(url))
} else {
None
}
}),
);
if let Err(e) = self.client.send_event_builder(builder).await {
log::error!("Failed to send messaging relay list event: {}", e);
};
let mut guard = self.identity.write().await;
// Update the identity
*guard = Some(profile);
// Notify GPUi via the global channel
self.global_sender
.send(NostrSignal::SignerUpdated)
.await
.ok();
// Subscribe
self.subscribe_for_user_data().await;
}
/// Returns the current user's profile (blocking)
pub fn identity(&self) -> Option<Profile> {
self.identity.read_blocking().as_ref().cloned()
}
/// Returns the current user's profile (async)
pub async fn async_identity(&self) -> Option<Profile> {
self.identity.read().await.as_ref().cloned()
}
/// Gets a person's profile from cache or creates default (blocking)
pub fn person(&self, public_key: &PublicKey) -> Profile {
let metadata = if let Some(metadata) = self.persons.read_blocking().get(public_key) {
@@ -354,7 +240,7 @@ impl Globals {
}
/// Connects to bootstrap and configured relays
async fn connect(&self) {
pub(crate) async fn connect(&self) {
for relay in BOOTSTRAP_RELAYS.into_iter() {
if let Err(e) = self.client.add_relay(relay).await {
log::error!("Failed to add relay {}: {}", relay, e);
@@ -374,13 +260,7 @@ impl Globals {
}
/// Subscribes to user-specific data feeds (DMs, mentions, etc.)
async fn subscribe_for_user_data(&self) {
let Some(profile) = self.identity.read().await.clone() else {
return;
};
let public_key = profile.public_key();
pub async fn subscribe_for_user_data(&self, public_key: PublicKey) {
let metadata = Filter::new()
.kinds(vec![
Kind::Metadata,
@@ -435,7 +315,7 @@ impl Globals {
}
/// Subscribes to application update notifications
async fn subscribe_for_app_updates(&self) {
pub(crate) async fn subscribe_for_app_updates(&self) {
let coordinate = Coordinate {
kind: Kind::Custom(32267),
public_key: PublicKey::from_hex(APP_PUBKEY).expect("App Pubkey is invalid"),
@@ -458,8 +338,22 @@ impl Globals {
log::info!("Subscribing to app updates...");
}
pub(crate) async fn preload_metadata(&self) {
let filter = Filter::new().kind(Kind::Metadata).limit(100);
if let Ok(events) = self.client.database().query(filter).await {
for event in events.into_iter() {
self.insert_person(&event).await;
}
}
}
/// Stores an unwrapped event in local database with reference to original
async fn set_unwrapped(&self, root: EventId, event: &Event, keys: &Keys) -> Result<(), Error> {
pub(crate) async fn set_unwrapped(
&self,
root: EventId,
event: &Event,
keys: &Keys,
) -> Result<(), Error> {
// Must be use the random generated keys to sign this event
let event = EventBuilder::new(Kind::ApplicationSpecificData, event.as_json())
.tags(vec![Tag::identifier(root), Tag::event(root)])
@@ -473,9 +367,9 @@ impl Globals {
}
/// Retrieves a previously unwrapped event from local database
async fn get_unwrapped(&self, target: EventId) -> Result<Event, Error> {
pub(crate) async fn get_unwrapped(&self, target: EventId) -> Result<Event, Error> {
let filter = Filter::new()
.kind(Kind::Custom(30078))
.kind(Kind::ApplicationSpecificData)
.event(target)
.limit(1);
@@ -487,7 +381,7 @@ impl Globals {
}
/// Unwraps a gift-wrapped event and processes its contents
async fn unwrap_event(&self, subscription_id: &SubscriptionId, event: &Event) {
pub(crate) async fn unwrap_event(&self, subscription_id: &SubscriptionId, event: &Event) {
let new_messages_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID);
let random_keys = Keys::generate();
@@ -527,7 +421,7 @@ impl Globals {
}
/// Extracts public keys from contact list and queues metadata sync
async fn extract_pubkeys_and_sync(&self, event: &Event) {
pub(crate) async fn extract_pubkeys_and_sync(&self, event: &Event) {
if let Ok(signer) = self.client.signer().await {
if let Ok(public_key) = signer.get_public_key().await {
if public_key == event.pubkey {
@@ -539,7 +433,7 @@ impl Globals {
}
/// Fetches metadata for a batch of public keys
async fn sync_data_for_pubkeys(&self, public_keys: BTreeSet<PublicKey>) {
pub(crate) async fn sync_data_for_pubkeys(&self, public_keys: BTreeSet<PublicKey>) {
let kinds = vec![
Kind::Metadata,
Kind::ContactList,
@@ -561,7 +455,7 @@ impl Globals {
}
/// Inserts or updates a person's metadata from a Kind::Metadata event
async fn insert_person(&self, event: &Event) {
pub(crate) async fn insert_person(&self, event: &Event) {
let metadata = Metadata::from_json(&event.content).ok();
self.persons
@@ -577,7 +471,7 @@ impl Globals {
}
/// Notifies UI of application updates via global channel
async fn notify_update(&self, event: &Event) {
pub(crate) async fn notify_update(&self, event: &Event) {
let filter = Filter::new()
.ids(event.tags.event_ids().copied())
.kind(Kind::FileMetadata);
@@ -596,3 +490,14 @@ impl Globals {
}
}
}
fn is_first_run() -> Result<bool, anyhow::Error> {
let flag = support_dir().join(".coop_first_run");
if !flag.exists() {
fs::write(&flag, "")?;
Ok(true) // First run
} else {
Ok(false) // Not first run
}
}