feat: Implemented NIP-4e (#11)

* chore: refactor account registry

* wip: nip4e

* chore: rename account to device

* feat: nip44 encryption with master signer

* update

* refactor

* feat: unwrap with device keys

* chore: improve handler

* chore: fix rustls

* chore: refactor onboarding

* chore: fix compose

* chore: fix send message

* chore: fix forgot to request device

* fix send message

* chore: fix deadlock

* chore: small fixes

* chore: improve

* fix

* refactor

* refactor

* refactor

* fix

* add fetch request

* save keys

* fix

* update

* update

* update
This commit is contained in:
reya
2025-03-08 19:29:25 +07:00
committed by GitHub
parent 81664e3d4e
commit a53b2181ab
31 changed files with 1744 additions and 1065 deletions

12
crates/global/Cargo.toml Normal file
View File

@@ -0,0 +1,12 @@
[package]
name = "global"
version = "0.0.0"
edition = "2021"
publish = false
[dependencies]
nostr-sdk.workspace = true
dirs.workspace = true
smol.workspace = true
whoami = "1.5.2"

View File

@@ -0,0 +1,33 @@
pub const APP_NAME: &str = "Coop";
pub const APP_ID: &str = "su.reya.coop";
pub const KEYRING: &str = "Coop Safe Storage";
pub const CLIENT_KEYRING: &str = "Coop Client Keys";
pub const MASTER_KEYRING: &str = "Coop Master Keys";
pub const DEVICE_ANNOUNCEMENT_KIND: u16 = 10044;
pub const DEVICE_REQUEST_KIND: u16 = 4454;
pub const DEVICE_RESPONSE_KIND: u16 = 4455;
/// Bootstrap relays
pub const BOOTSTRAP_RELAYS: [&str; 3] = [
"wss://relay.damus.io",
"wss://relay.primal.net",
"wss://purplepag.es",
];
/// Subscriptions
pub const NEW_MESSAGE_SUB_ID: &str = "listen_new_giftwraps";
pub const ALL_MESSAGES_SUB_ID: &str = "listen_all_giftwraps";
/// Image Resizer Service
pub const IMAGE_SERVICE: &str = "https://wsrv.nl";
/// NIP96 Media Server
pub const NIP96_SERVER: &str = "https://nostrmedia.com";
/// Updater Public Key
pub const UPDATER_PUBKEY: &str = "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDkxM0EzQTQyRTBBMENENTYKUldSV3phRGdRam82a1dtU0JqYll4VnBaVUpSWUxCWlVQbnRkUnNERS96MzFMWDhqNW5zOXplMEwK";
/// Updater Server URL
pub const UPDATER_URL: &str =
"https://cdn.crabnebula.app/update/lume/coop/{{target}}-{{arch}}/{{current_version}}";

104
crates/global/src/lib.rs Normal file
View File

@@ -0,0 +1,104 @@
use constants::{ALL_MESSAGES_SUB_ID, APP_ID};
use dirs::config_dir;
use nostr_sdk::prelude::*;
use smol::lock::Mutex;
use std::{
fs,
sync::{Arc, OnceLock},
time::Duration,
};
pub mod constants;
/// Nostr Client
static CLIENT: OnceLock<Client> = OnceLock::new();
/// Current App Name
static APP_NAME: OnceLock<Arc<str>> = OnceLock::new();
/// NIP-4e: Device Keys, used for encryption
static DEVICE_KEYS: Mutex<Option<Arc<dyn NostrSigner>>> = Mutex::new(None);
/// NIP-4e: Device Name, used for display purposes
static DEVICE_NAME: Mutex<Option<Arc<String>>> = Mutex::new(None);
/// Nostr Client instance
pub fn get_client() -> &'static Client {
CLIENT.get_or_init(|| {
// Setup app data folder
let config_dir = config_dir().expect("Config directory not found");
let app_dir = config_dir.join(APP_ID);
// Create app directory if it doesn't exist
_ = fs::create_dir_all(&app_dir);
// Setup database
let lmdb = NostrLMDB::open(app_dir.join("nostr")).expect("Database is NOT initialized");
// Client options
let opts = Options::new()
// NIP-65
.gossip(true)
// Skip all very slow relays
.max_avg_latency(Duration::from_secs(2));
// Setup Nostr Client
ClientBuilder::default().database(lmdb).opts(opts).build()
})
}
/// Get app name
pub fn get_app_name() -> &'static str {
APP_NAME.get_or_init(|| {
Arc::from(format!(
"Coop on {} ({})",
whoami::distro(),
whoami::devicename()
))
})
}
/// Get device keys
pub async fn get_device_keys() -> Option<Arc<dyn NostrSigner>> {
let guard = DEVICE_KEYS.lock().await;
guard.clone()
}
/// Set device keys
pub async fn set_device_keys<T>(signer: T)
where
T: NostrSigner + 'static,
{
DEVICE_KEYS.lock().await.replace(Arc::new(signer));
// Re-subscribe to all messages
smol::spawn(async move {
let client = get_client();
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
if let Ok(signer) = client.signer().await {
let public_key = signer.get_public_key().await.unwrap();
// Create a filter for getting all gift wrapped events send to current user
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
let id = SubscriptionId::new(ALL_MESSAGES_SUB_ID);
_ = client.unsubscribe(&id);
_ = client.subscribe_with_id(id, filter, Some(opts)).await;
}
})
.await;
}
/// Set master's device name
pub async fn set_device_name(name: &str) {
let mut guard = DEVICE_NAME.lock().await;
if guard.is_none() {
guard.replace(Arc::new(name.to_owned()));
}
}
/// Get master's device name
pub fn get_device_name() -> Arc<String> {
let guard = DEVICE_NAME.lock_blocking();
guard.clone().unwrap_or(Arc::new("Main Device".into()))
}