feat: Multi Accounts (#237)

* wip: new sync

* wip: restructure routes

* update

* feat: improve sync

* feat: repost with multi-account

* feat: improve sync

* feat: publish with multi account

* fix: settings screen

* feat: add zap for multi accounts
This commit is contained in:
雨宮蓮
2024-10-22 16:00:06 +07:00
committed by GitHub
parent ba9c81a10a
commit cc7de41bfd
89 changed files with 2695 additions and 2911 deletions
+54 -258
View File
@@ -1,17 +1,11 @@
use keyring::Entry;
use keyring_search::{Limit, List, Search};
use nostr_sdk::prelude::*;
use serde::{Deserialize, Serialize};
use specta::Type;
use std::{
collections::HashSet,
fs::{self, File},
str::FromStr,
time::Duration,
};
use tauri::{Emitter, Manager, State};
use std::{str::FromStr, time::Duration};
use tauri::{Emitter, State};
use crate::{Nostr, NOTIFICATION_SUB_ID};
use crate::{common::get_all_accounts, Nostr};
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
struct Account {
@@ -22,67 +16,31 @@ struct Account {
#[tauri::command]
#[specta::specta]
pub fn get_accounts() -> Vec<String> {
let search = Search::new().expect("Unexpected.");
let results = search.by_service("Lume Secret Storage");
let list = List::list_credentials(&results, Limit::All);
let accounts: HashSet<String> = list
.split_whitespace()
.filter(|v| v.starts_with("npub1"))
.map(String::from)
.collect();
accounts.into_iter().collect()
get_all_accounts()
}
#[tauri::command]
#[specta::specta]
pub async fn create_account(
name: String,
about: String,
picture: String,
pub async fn watch_account(key: String, state: State<'_, Nostr>) -> Result<String, String> {
let public_key = PublicKey::from_str(&key).map_err(|e| e.to_string())?;
let bech32 = public_key.to_bech32().map_err(|e| e.to_string())?;
let keyring = Entry::new("Lume Secret Storage", &bech32).map_err(|e| e.to_string())?;
keyring.set_password("").map_err(|e| e.to_string())?;
// Update state
state.accounts.lock().unwrap().push(bech32.clone());
Ok(bech32)
}
#[tauri::command]
#[specta::specta]
pub async fn import_account(
key: String,
password: String,
state: State<'_, Nostr>,
) -> Result<String, String> {
let client = &state.client;
let keys = Keys::generate();
let npub = keys.public_key().to_bech32().map_err(|e| e.to_string())?;
let secret_key = keys.secret_key();
let enc = EncryptedSecretKey::new(secret_key, password, 16, KeySecurity::Medium)
.map_err(|err| err.to_string())?;
let enc_bech32 = enc.to_bech32().map_err(|err| err.to_string())?;
// Save account
let keyring = Entry::new("Lume Secret Storage", &npub).map_err(|e| e.to_string())?;
let account = Account {
password: enc_bech32,
nostr_connect: None,
};
let j = serde_json::to_string(&account).map_err(|e| e.to_string())?;
let _ = keyring.set_password(&j);
let signer = NostrSigner::Keys(keys);
// Update signer
client.set_signer(Some(signer)).await;
let mut metadata = Metadata::new()
.display_name(name.clone())
.name(name.to_lowercase())
.about(about);
if let Ok(url) = Url::parse(&picture) {
metadata = metadata.picture(url)
}
match client.set_metadata(&metadata).await {
Ok(_) => Ok(npub),
Err(e) => Err(e.to_string()),
}
}
#[tauri::command]
#[specta::specta]
pub async fn import_account(key: String, password: String) -> Result<String, String> {
let (npub, enc_bech32) = match key.starts_with("ncryptsec") {
true => {
let enc = EncryptedSecretKey::from_bech32(key).map_err(|err| err.to_string())?;
@@ -117,6 +75,9 @@ pub async fn import_account(key: String, password: String) -> Result<String, Str
let pwd = serde_json::to_string(&account).map_err(|e| e.to_string())?;
keyring.set_password(&pwd).map_err(|e| e.to_string())?;
// Update state
state.accounts.lock().unwrap().push(npub.clone());
Ok(npub)
}
@@ -157,6 +118,9 @@ pub async fn connect_account(uri: String, state: State<'_, Nostr>) -> Result<Str
// Update signer
let _ = client.set_signer(Some(signer.into())).await;
// Update state
state.accounts.lock().unwrap().push(remote_npub.clone());
Ok(remote_npub)
}
Err(err) => Err(err.to_string()),
@@ -208,34 +172,32 @@ pub fn delete_account(id: String) -> Result<(), String> {
#[tauri::command]
#[specta::specta]
pub fn is_account_sync(id: String, handle: tauri::AppHandle) -> bool {
let config_dir = handle
.path()
.app_config_dir()
.expect("Error: app config directory not found.");
pub async fn has_signer(id: String, state: State<'_, Nostr>) -> Result<bool, String> {
let client = &state.client;
let public_key = PublicKey::from_str(&id).map_err(|e| e.to_string())?;
fs::metadata(config_dir.join(id)).is_ok()
match client.signer().await {
Ok(signer) => {
// Emit reload in front-end
// handle.emit("signer", ()).unwrap();
let signer_key = signer.public_key().await.map_err(|e| e.to_string())?;
let is_match = signer_key == public_key;
Ok(is_match)
}
Err(_) => Ok(false),
}
}
#[tauri::command]
#[specta::specta]
pub fn create_sync_file(id: String, handle: tauri::AppHandle) -> bool {
let config_dir = handle
.path()
.app_config_dir()
.expect("Error: app config directory not found.");
File::create(config_dir.join(id)).is_ok()
}
#[tauri::command]
#[specta::specta]
pub async fn login(
pub async fn set_signer(
account: String,
password: String,
state: State<'_, Nostr>,
handle: tauri::AppHandle,
) -> Result<String, String> {
) -> Result<(), String> {
let client = &state.client;
let keyring = Entry::new("Lume Secret Storage", &account).map_err(|e| e.to_string())?;
@@ -247,7 +209,7 @@ pub async fn login(
Err(e) => return Err(e.to_string()),
};
let public_key = match account.nostr_connect {
match account.nostr_connect {
None => {
let ncryptsec =
EncryptedSecretKey::from_bech32(account.password).map_err(|e| e.to_string())?;
@@ -255,196 +217,30 @@ pub async fn login(
.to_secret_key(password)
.map_err(|_| "Wrong password.")?;
let keys = Keys::new(secret_key);
let public_key = keys.public_key().to_bech32().unwrap();
let signer = NostrSigner::Keys(keys);
// Update signer
client.set_signer(Some(signer)).await;
// Emit to front-end
handle.emit("signer-updated", ()).unwrap();
public_key
Ok(())
}
Some(bunker) => {
let uri = NostrConnectURI::parse(bunker).map_err(|e| e.to_string())?;
let public_key = uri.signer_public_key().unwrap().to_bech32().unwrap();
let app_keys = Keys::from_str(&account.password).map_err(|e| e.to_string())?;
match Nip46Signer::new(uri, app_keys, Duration::from_secs(120), None) {
Ok(signer) => {
// Update signer
client.set_signer(Some(signer.into())).await;
public_key
// Emit to front-end
handle.emit("signer-updated", ()).unwrap();
Ok(())
}
Err(e) => return Err(e.to_string()),
Err(e) => Err(e.to_string()),
}
}
};
// NIP-65: Connect to user's relay list
// init_nip65(client, &public_key).await;
// NIP-03: Get user's contact list
let contact_list = {
if let Ok(contacts) = client.get_contact_list(Some(Duration::from_secs(5))).await {
state.contact_list.lock().unwrap().clone_from(&contacts);
contacts
} else {
Vec::new()
}
};
let public_key_clone = public_key.clone();
// Run seperate thread for sync
tauri::async_runtime::spawn(async move {
let state = handle.state::<Nostr>();
let client = &state.client;
let author = PublicKey::from_str(&public_key).unwrap();
// Subscribe for new notification
if let Ok(e) = client
.subscribe_with_id(
SubscriptionId::new(NOTIFICATION_SUB_ID),
vec![Filter::new().pubkey(author).since(Timestamp::now())],
None,
)
.await
{
println!("Subscribed: {}", e.success.len())
}
// Get events from contact list
if !contact_list.is_empty() {
let authors: Vec<PublicKey> = contact_list.iter().map(|f| f.public_key).collect();
// Syncing all metadata events from contact list
if let Ok(report) = client
.reconcile(
Filter::new()
.authors(authors.clone())
.kinds(vec![Kind::Metadata, Kind::ContactList])
.limit(authors.len() * 10),
NegentropyOptions::default(),
)
.await
{
println!("Received: {}", report.received.len());
}
// Syncing all events from contact list
if let Ok(report) = client
.reconcile(
Filter::new()
.authors(authors.clone())
.kinds(vec![Kind::TextNote, Kind::Repost, Kind::EventDeletion])
.limit(authors.len() * 40),
NegentropyOptions::default(),
)
.await
{
println!("Received: {}", report.received.len());
}
// Create the trusted public key list from contact list
// TODO: create a cached file
if let Ok(events) = client
.database()
.query(vec![Filter::new().kind(Kind::ContactList)])
.await
{
let keys: Vec<&str> = events
.iter()
.flat_map(|event| {
event
.tags
.iter()
.filter(|t| t.kind() == TagKind::p())
.filter_map(|t| t.content())
.collect::<Vec<&str>>()
})
.collect();
let trusted_list: HashSet<PublicKey> = keys
.into_iter()
.filter_map(|item| {
if let Ok(pk) = PublicKey::from_str(item) {
Some(pk)
} else {
None
}
})
.collect();
// Update app's state
state.trusted_list.lock().unwrap().clone_from(&trusted_list);
let trusted_users: Vec<PublicKey> = trusted_list.into_iter().collect();
println!("Total trusted users: {}", trusted_users.len());
if let Ok(report) = client
.reconcile(
Filter::new()
.authors(trusted_users)
.kinds(vec![
Kind::Metadata,
Kind::TextNote,
Kind::Repost,
Kind::EventDeletion,
])
.limit(5000),
NegentropyOptions::default(),
)
.await
{
println!("Received: {}", report.received.len())
}
};
};
// Syncing all user's events
if let Ok(report) = client
.reconcile(
Filter::new().author(author).kinds(vec![
Kind::TextNote,
Kind::Repost,
Kind::FollowSet,
Kind::InterestSet,
Kind::Interests,
Kind::EventDeletion,
Kind::MuteList,
Kind::BookmarkSet,
Kind::BlockedRelays,
Kind::EmojiSet,
Kind::RelaySet,
Kind::RelayList,
Kind::ApplicationSpecificData,
]),
NegentropyOptions::default(),
)
.await
{
println!("Received: {}", report.received.len())
}
// Syncing all tagged events for current user
if let Ok(report) = client
.reconcile(
Filter::new().pubkey(author).kinds(vec![
Kind::TextNote,
Kind::Repost,
Kind::Reaction,
Kind::ZapReceipt,
]),
NegentropyOptions::default(),
)
.await
{
println!("Received: {}", report.received.len())
};
handle
.emit("neg_synchronized", ())
.expect("Something wrong!");
});
Ok(public_key_clone)
}
}