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:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -410,16 +410,58 @@ pub async fn repost(raw: String, state: State<'_, Nostr>) -> Result<String, Stri
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn delete(id: String, state: State<'_, Nostr>) -> Result<String, String> {
|
||||
pub async fn is_reposted(id: String, state: State<'_, Nostr>) -> Result<bool, String> {
|
||||
let client = &state.client;
|
||||
let accounts = state.accounts.lock().unwrap().clone();
|
||||
|
||||
let event_id = EventId::parse(&id).map_err(|err| err.to_string())?;
|
||||
|
||||
match client.delete_event(event_id).await {
|
||||
Ok(event_id) => Ok(event_id.to_string()),
|
||||
let authors: Vec<PublicKey> = accounts
|
||||
.iter()
|
||||
.map(|acc| PublicKey::from_str(acc).unwrap())
|
||||
.collect();
|
||||
|
||||
let filter = Filter::new()
|
||||
.event(event_id)
|
||||
.kind(Kind::Repost)
|
||||
.authors(authors);
|
||||
|
||||
match client.database().query(vec![filter]).await {
|
||||
Ok(events) => Ok(!events.is_empty()),
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn request_delete(id: String, state: State<'_, Nostr>) -> Result<(), String> {
|
||||
let client = &state.client;
|
||||
let event_id = EventId::from_str(&id).map_err(|err| err.to_string())?;
|
||||
|
||||
match client.delete_event(event_id).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn is_deleted_event(id: String, state: State<'_, Nostr>) -> Result<bool, String> {
|
||||
let client = &state.client;
|
||||
let signer = client.signer().await.map_err(|err| err.to_string())?;
|
||||
let public_key = signer.public_key().await.map_err(|err| err.to_string())?;
|
||||
let event_id = EventId::from_str(&id).map_err(|err| err.to_string())?;
|
||||
let filter = Filter::new()
|
||||
.author(public_key)
|
||||
.event(event_id)
|
||||
.kind(Kind::EventDeletion);
|
||||
|
||||
match client.database().query(vec![filter]).await {
|
||||
Ok(events) => Ok(!events.is_empty()),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn event_to_bech32(id: String, state: State<'_, Nostr>) -> Result<String, String> {
|
||||
@@ -498,34 +540,3 @@ pub async fn search(query: String, state: State<'_, Nostr>) -> Result<Vec<RichEv
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn is_deleted_event(id: String, state: State<'_, Nostr>) -> Result<bool, String> {
|
||||
let client = &state.client;
|
||||
let signer = client.signer().await.map_err(|err| err.to_string())?;
|
||||
let public_key = signer.public_key().await.map_err(|err| err.to_string())?;
|
||||
let event_id = EventId::from_str(&id).map_err(|err| err.to_string())?;
|
||||
let filter = Filter::new()
|
||||
.author(public_key)
|
||||
.event(event_id)
|
||||
.kind(Kind::EventDeletion);
|
||||
|
||||
match client.database().query(vec![filter]).await {
|
||||
Ok(events) => Ok(!events.is_empty()),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn request_delete(id: String, state: State<'_, Nostr>) -> Result<(), String> {
|
||||
let client = &state.client;
|
||||
let event_id = EventId::from_str(&id).map_err(|err| err.to_string())?;
|
||||
let builder = EventBuilder::delete(vec![event_id]);
|
||||
|
||||
match client.send_event_builder(builder).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,10 @@ use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use std::{str::FromStr, time::Duration};
|
||||
use tauri::{Emitter, Manager, State};
|
||||
use tauri_specta::Event;
|
||||
|
||||
use crate::{
|
||||
common::{get_latest_event, process_event},
|
||||
NewSettings, Nostr, RichEvent, Settings,
|
||||
common::{get_all_accounts, get_latest_event, get_tags_content, process_event},
|
||||
Nostr, RichEvent, Settings,
|
||||
};
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Type)]
|
||||
@@ -104,14 +103,36 @@ pub async fn set_contact_list(
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn get_contact_list(state: State<'_, Nostr>) -> Result<Vec<String>, String> {
|
||||
let contact_list = state.contact_list.lock().unwrap().clone();
|
||||
let vec: Vec<String> = contact_list
|
||||
.into_iter()
|
||||
.map(|f| f.public_key.to_hex())
|
||||
.collect();
|
||||
pub async fn get_contact_list(id: String, state: State<'_, Nostr>) -> Result<Vec<String>, String> {
|
||||
let client = &state.client;
|
||||
let public_key = PublicKey::parse(&id).map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(vec)
|
||||
let filter = Filter::new()
|
||||
.author(public_key)
|
||||
.kind(Kind::ContactList)
|
||||
.limit(1);
|
||||
|
||||
let mut contact_list: Vec<String> = Vec::new();
|
||||
|
||||
match client.database().query(vec![filter]).await {
|
||||
Ok(events) => {
|
||||
if let Some(event) = events.into_iter().next() {
|
||||
for tag in event.tags.into_iter() {
|
||||
if let Some(TagStandard::PublicKey {
|
||||
public_key,
|
||||
uppercase: false,
|
||||
..
|
||||
}) = tag.to_standardized()
|
||||
{
|
||||
contact_list.push(public_key.to_hex())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(contact_list)
|
||||
}
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -149,13 +170,27 @@ pub async fn set_profile(profile: Profile, state: State<'_, Nostr>) -> Result<St
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn check_contact(id: String, state: State<'_, Nostr>) -> Result<bool, String> {
|
||||
let contact_list = &state.contact_list.lock().unwrap();
|
||||
let public_key = PublicKey::from_str(&id).map_err(|e| e.to_string())?;
|
||||
pub async fn is_contact(id: String, state: State<'_, Nostr>) -> Result<bool, String> {
|
||||
let client = &state.client;
|
||||
let public_key = PublicKey::parse(&id).map_err(|e| e.to_string())?;
|
||||
|
||||
match contact_list.iter().position(|x| x.public_key == public_key) {
|
||||
Some(_) => Ok(true),
|
||||
None => Ok(false),
|
||||
let filter = Filter::new()
|
||||
.author(public_key)
|
||||
.kind(Kind::ContactList)
|
||||
.limit(1);
|
||||
|
||||
match client.database().query(vec![filter]).await {
|
||||
Ok(events) => {
|
||||
if let Some(event) = events.into_iter().next() {
|
||||
let hex = public_key.to_hex();
|
||||
let pubkeys = get_tags_content(&event, TagKind::p());
|
||||
|
||||
Ok(pubkeys.iter().any(|i| i == &hex))
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,9 +302,18 @@ pub async fn get_group(id: String, state: State<'_, Nostr>) -> Result<String, St
|
||||
#[specta::specta]
|
||||
pub async fn get_all_groups(state: State<'_, Nostr>) -> Result<Vec<RichEvent>, String> {
|
||||
let client = &state.client;
|
||||
let signer = client.signer().await.map_err(|e| e.to_string())?;
|
||||
let public_key = signer.public_key().await.map_err(|e| e.to_string())?;
|
||||
let filter = Filter::new().kind(Kind::FollowSet).author(public_key);
|
||||
let accounts = get_all_accounts();
|
||||
let authors: Vec<PublicKey> = accounts
|
||||
.iter()
|
||||
.filter_map(|acc| {
|
||||
if let Ok(pk) = PublicKey::from_str(acc) {
|
||||
Some(pk)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let filter = Filter::new().kind(Kind::FollowSet).authors(authors);
|
||||
|
||||
match client.database().query(vec![filter]).await {
|
||||
Ok(events) => Ok(process_event(client, events).await),
|
||||
@@ -347,11 +391,20 @@ pub async fn get_interest(id: String, state: State<'_, Nostr>) -> Result<String,
|
||||
#[specta::specta]
|
||||
pub async fn get_all_interests(state: State<'_, Nostr>) -> Result<Vec<RichEvent>, String> {
|
||||
let client = &state.client;
|
||||
let signer = client.signer().await.map_err(|e| e.to_string())?;
|
||||
let public_key = signer.public_key().await.map_err(|e| e.to_string())?;
|
||||
let accounts = get_all_accounts();
|
||||
let authors: Vec<PublicKey> = accounts
|
||||
.iter()
|
||||
.filter_map(|acc| {
|
||||
if let Ok(pk) = PublicKey::from_str(acc) {
|
||||
Some(pk)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let filter = Filter::new()
|
||||
.kinds(vec![Kind::InterestSet, Kind::Interests])
|
||||
.author(public_key);
|
||||
.authors(authors);
|
||||
|
||||
match client.database().query(vec![filter]).await {
|
||||
Ok(events) => Ok(process_event(client, events).await),
|
||||
@@ -361,7 +414,7 @@ pub async fn get_all_interests(state: State<'_, Nostr>) -> Result<Vec<RichEvent>
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_mention_list(state: State<'_, Nostr>) -> Result<Vec<Mention>, String> {
|
||||
pub async fn get_all_profiles(state: State<'_, Nostr>) -> Result<Vec<Mention>, String> {
|
||||
let client = &state.client;
|
||||
let filter = Filter::new().kind(Kind::Metadata);
|
||||
|
||||
@@ -396,7 +449,9 @@ pub async fn set_wallet(uri: &str, state: State<'_, Nostr>) -> Result<bool, Stri
|
||||
|
||||
if let Ok(nwc_uri) = NostrWalletConnectURI::from_str(uri) {
|
||||
let nwc = NWC::new(nwc_uri);
|
||||
let keyring = Entry::new("Lume Secret", "Bitcoin Connect").map_err(|e| e.to_string())?;
|
||||
let keyring =
|
||||
Entry::new("Lume Secret Storage", "Bitcoin Connect").map_err(|e| e.to_string())?;
|
||||
|
||||
keyring.set_password(uri).map_err(|e| e.to_string())?;
|
||||
client.set_zapper(nwc).await;
|
||||
|
||||
@@ -408,29 +463,25 @@ pub async fn set_wallet(uri: &str, state: State<'_, Nostr>) -> Result<bool, Stri
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn load_wallet(state: State<'_, Nostr>) -> Result<String, String> {
|
||||
pub async fn load_wallet(state: State<'_, Nostr>) -> Result<(), String> {
|
||||
let client = &state.client;
|
||||
let keyring =
|
||||
Entry::new("Lume Secret Storage", "Bitcoin Connect").map_err(|e| e.to_string())?;
|
||||
|
||||
match keyring.get_password() {
|
||||
Ok(val) => {
|
||||
let uri = NostrWalletConnectURI::from_str(&val).unwrap();
|
||||
let nwc = NWC::new(uri);
|
||||
if client.zapper().await.is_err() {
|
||||
let keyring =
|
||||
Entry::new("Lume Secret Storage", "Bitcoin Connect").map_err(|e| e.to_string())?;
|
||||
|
||||
// Get current balance
|
||||
let balance = nwc.get_balance().await;
|
||||
match keyring.get_password() {
|
||||
Ok(val) => {
|
||||
let uri = NostrWalletConnectURI::from_str(&val).unwrap();
|
||||
let nwc = NWC::new(uri);
|
||||
|
||||
// Update zapper
|
||||
client.set_zapper(nwc).await;
|
||||
|
||||
match balance {
|
||||
Ok(val) => Ok(val.to_string()),
|
||||
Err(_) => Err("Get balance failed.".into()),
|
||||
client.set_zapper(nwc).await;
|
||||
}
|
||||
Err(_) => return Err("Wallet not found.".into()),
|
||||
}
|
||||
Err(_) => Err("NWC not found.".into()),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -452,52 +503,40 @@ pub async fn remove_wallet(state: State<'_, Nostr>) -> Result<(), String> {
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn zap_profile(
|
||||
id: &str,
|
||||
amount: &str,
|
||||
message: &str,
|
||||
id: String,
|
||||
amount: String,
|
||||
message: Option<String>,
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<bool, String> {
|
||||
) -> Result<(), String> {
|
||||
let client = &state.client;
|
||||
|
||||
let public_key: PublicKey = PublicKey::parse(id).map_err(|e| e.to_string())?;
|
||||
|
||||
let details = ZapDetails::new(ZapType::Private).message(message);
|
||||
let num = amount.parse::<u64>().map_err(|e| e.to_string())?;
|
||||
let details = message.map(|m| ZapDetails::new(ZapType::Public).message(m));
|
||||
|
||||
if client.zap(public_key, num, Some(details)).await.is_ok() {
|
||||
Ok(true)
|
||||
} else {
|
||||
Err("Zap profile failed".into())
|
||||
match client.zap(public_key, num, details).await {
|
||||
Ok(()) => Ok(()),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn zap_event(
|
||||
id: &str,
|
||||
amount: &str,
|
||||
message: &str,
|
||||
id: String,
|
||||
amount: String,
|
||||
message: Option<String>,
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<bool, String> {
|
||||
) -> Result<(), String> {
|
||||
let client = &state.client;
|
||||
let event_id = match Nip19::from_bech32(id) {
|
||||
Ok(val) => match val {
|
||||
Nip19::EventId(id) => id,
|
||||
Nip19::Event(event) => event.event_id,
|
||||
_ => return Err("Event ID is invalid.".into()),
|
||||
},
|
||||
Err(_) => match EventId::from_hex(id) {
|
||||
Ok(val) => val,
|
||||
Err(_) => return Err("Event ID is invalid.".into()),
|
||||
},
|
||||
};
|
||||
|
||||
let details = ZapDetails::new(ZapType::Private).message(message);
|
||||
let event_id = EventId::from_str(&id).map_err(|e| e.to_string())?;
|
||||
let num = amount.parse::<u64>().map_err(|e| e.to_string())?;
|
||||
let details = message.map(|m| ZapDetails::new(ZapType::Public).message(m));
|
||||
|
||||
if client.zap(event_id, num, Some(details)).await.is_ok() {
|
||||
Ok(true)
|
||||
} else {
|
||||
Err("Zap event failed".into())
|
||||
match client.zap(event_id, num, details).await {
|
||||
Ok(()) => Ok(()),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -544,27 +583,22 @@ pub async fn copy_friend(npub: &str, state: State<'_, Nostr>) -> Result<bool, St
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_notifications(state: State<'_, Nostr>) -> Result<Vec<String>, String> {
|
||||
pub async fn get_notifications(id: String, state: State<'_, Nostr>) -> Result<Vec<String>, String> {
|
||||
let client = &state.client;
|
||||
let public_key = PublicKey::from_str(&id).map_err(|e| e.to_string())?;
|
||||
|
||||
match client.signer().await {
|
||||
Ok(signer) => {
|
||||
let public_key = signer.public_key().await.unwrap();
|
||||
let filter = Filter::new()
|
||||
.pubkey(public_key)
|
||||
.kinds(vec![
|
||||
Kind::TextNote,
|
||||
Kind::Repost,
|
||||
Kind::Reaction,
|
||||
Kind::ZapReceipt,
|
||||
])
|
||||
.limit(200);
|
||||
let filter = Filter::new()
|
||||
.pubkey(public_key)
|
||||
.kinds(vec![
|
||||
Kind::TextNote,
|
||||
Kind::Repost,
|
||||
Kind::Reaction,
|
||||
Kind::ZapReceipt,
|
||||
])
|
||||
.limit(200);
|
||||
|
||||
match client.database().query(vec![filter]).await {
|
||||
Ok(events) => Ok(events.into_iter().map(|ev| ev.as_json()).collect()),
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
match client.database().query(vec![filter]).await {
|
||||
Ok(events) => Ok(events.into_iter().map(|ev| ev.as_json()).collect()),
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
@@ -577,29 +611,11 @@ pub fn get_user_settings(state: State<'_, Nostr>) -> Result<Settings, String> {
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn set_user_settings(
|
||||
settings: String,
|
||||
state: State<'_, Nostr>,
|
||||
handle: tauri::AppHandle,
|
||||
) -> Result<(), String> {
|
||||
let client = &state.client;
|
||||
let tags = vec![Tag::identifier("lume_user_setting")];
|
||||
let builder = EventBuilder::new(Kind::ApplicationSpecificData, &settings, tags);
|
||||
pub async fn set_user_settings(settings: String, state: State<'_, Nostr>) -> Result<(), String> {
|
||||
let parsed: Settings = serde_json::from_str(&settings).map_err(|e| e.to_string())?;
|
||||
state.settings.lock().unwrap().clone_from(&parsed);
|
||||
|
||||
match client.send_event_builder(builder).await {
|
||||
Ok(_) => {
|
||||
let parsed: Settings = serde_json::from_str(&settings).map_err(|e| e.to_string())?;
|
||||
|
||||
// Update state
|
||||
state.settings.lock().unwrap().clone_from(&parsed);
|
||||
|
||||
// Emit new changes to frontend
|
||||
NewSettings(parsed).emit(&handle).unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -613,12 +629,3 @@ pub async fn verify_nip05(id: String, nip05: &str) -> Result<bool, String> {
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn is_trusted_user(id: String, state: State<'_, Nostr>) -> Result<bool, String> {
|
||||
let trusted_list = &state.trusted_list.lock().unwrap();
|
||||
let public_key = PublicKey::from_str(&id).map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(trusted_list.contains(&public_key))
|
||||
}
|
||||
|
||||
@@ -2,4 +2,5 @@ pub mod account;
|
||||
pub mod event;
|
||||
pub mod metadata;
|
||||
pub mod relay;
|
||||
pub mod sync;
|
||||
pub mod window;
|
||||
|
||||
@@ -5,6 +5,7 @@ use specta::Type;
|
||||
use std::{
|
||||
fs::OpenOptions,
|
||||
io::{self, BufRead, Write},
|
||||
str::FromStr,
|
||||
};
|
||||
use tauri::{path::BaseDirectory, Manager, State};
|
||||
|
||||
@@ -18,8 +19,9 @@ pub struct Relays {
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_relays(state: State<'_, Nostr>) -> Result<Relays, String> {
|
||||
pub async fn get_relays(id: String, state: State<'_, Nostr>) -> Result<Relays, String> {
|
||||
let client = &state.client;
|
||||
let public_key = PublicKey::from_str(&id).map_err(|e| e.to_string())?;
|
||||
|
||||
let connected_relays = client
|
||||
.relays()
|
||||
@@ -28,9 +30,6 @@ pub async fn get_relays(state: State<'_, Nostr>) -> Result<Relays, String> {
|
||||
.map(|url| url.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let signer = client.signer().await.map_err(|e| e.to_string())?;
|
||||
let public_key = signer.public_key().await.map_err(|e| e.to_string())?;
|
||||
|
||||
let filter = Filter::new()
|
||||
.author(public_key)
|
||||
.kind(Kind::RelayList)
|
||||
@@ -98,13 +97,14 @@ pub async fn get_relays(state: State<'_, Nostr>) -> Result<Relays, String> {
|
||||
pub async fn connect_relay(relay: &str, state: State<'_, Nostr>) -> Result<bool, String> {
|
||||
let client = &state.client;
|
||||
let status = client.add_relay(relay).await.map_err(|e| e.to_string())?;
|
||||
|
||||
if status {
|
||||
println!("Connecting to relay: {}", relay);
|
||||
client
|
||||
.connect_relay(relay)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
@@ -112,14 +112,12 @@ pub async fn connect_relay(relay: &str, state: State<'_, Nostr>) -> Result<bool,
|
||||
#[specta::specta]
|
||||
pub async fn remove_relay(relay: &str, state: State<'_, Nostr>) -> Result<bool, String> {
|
||||
let client = &state.client;
|
||||
|
||||
client
|
||||
.remove_relay(relay)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
client
|
||||
.disconnect_relay(relay)
|
||||
.force_remove_relay(relay)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
|
||||
204
src-tauri/src/commands/sync.rs
Normal file
204
src-tauri/src/commands/sync.rs
Normal file
@@ -0,0 +1,204 @@
|
||||
use nostr_sdk::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use std::str::FromStr;
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tauri_specta::Event as TauriEvent;
|
||||
|
||||
use crate::{common::get_tags_content, Nostr};
|
||||
|
||||
#[derive(Clone, Serialize, Type, TauriEvent)]
|
||||
pub struct NegentropyEvent {
|
||||
kind: NegentropyKind,
|
||||
total_event: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Type)]
|
||||
pub enum NegentropyKind {
|
||||
Profile,
|
||||
Metadata,
|
||||
Events,
|
||||
EventIds,
|
||||
Global,
|
||||
Notification,
|
||||
Others,
|
||||
}
|
||||
|
||||
pub fn run_fast_sync(accounts: Vec<String>, app_handle: AppHandle) {
|
||||
if accounts.is_empty() {
|
||||
return;
|
||||
};
|
||||
|
||||
let public_keys: Vec<PublicKey> = accounts
|
||||
.iter()
|
||||
.filter_map(|acc| {
|
||||
if let Ok(pk) = PublicKey::from_str(acc) {
|
||||
Some(pk)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let state = app_handle.state::<Nostr>();
|
||||
let client = &state.client;
|
||||
let bootstrap_relays = state.bootstrap_relays.lock().unwrap().clone();
|
||||
|
||||
// NEG: Sync profile
|
||||
//
|
||||
let profile = Filter::new()
|
||||
.authors(public_keys.clone())
|
||||
.kind(Kind::Metadata)
|
||||
.limit(4);
|
||||
|
||||
if let Ok(report) = client
|
||||
.reconcile_with(&bootstrap_relays, profile, NegentropyOptions::default())
|
||||
.await
|
||||
{
|
||||
NegentropyEvent {
|
||||
kind: NegentropyKind::Profile,
|
||||
total_event: report.received.len() as i32,
|
||||
}
|
||||
.emit(&app_handle)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// NEG: Sync contact list
|
||||
//
|
||||
let contact_list = Filter::new()
|
||||
.authors(public_keys.clone())
|
||||
.kind(Kind::ContactList)
|
||||
.limit(4);
|
||||
|
||||
if let Ok(report) = client
|
||||
.reconcile_with(
|
||||
&bootstrap_relays,
|
||||
contact_list.clone(),
|
||||
NegentropyOptions::default(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
NegentropyEvent {
|
||||
kind: NegentropyKind::Metadata,
|
||||
total_event: report.received.len() as i32,
|
||||
}
|
||||
.emit(&app_handle)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// NEG: Sync events from contact list
|
||||
//
|
||||
if let Ok(events) = client.database().query(vec![contact_list]).await {
|
||||
let pubkeys: Vec<PublicKey> = events
|
||||
.iter()
|
||||
.flat_map(|ev| {
|
||||
let tags = get_tags_content(ev, TagKind::p());
|
||||
tags.into_iter().filter_map(|p| {
|
||||
if let Ok(pk) = PublicKey::from_hex(p) {
|
||||
Some(pk)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
for chunk in pubkeys.chunks(500) {
|
||||
if chunk.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
let authors = chunk.to_owned();
|
||||
|
||||
// NEG: Sync event
|
||||
//
|
||||
let events = Filter::new()
|
||||
.authors(authors.clone())
|
||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||
.limit(1000);
|
||||
|
||||
if let Ok(report) = client
|
||||
.reconcile_with(&bootstrap_relays, events, NegentropyOptions::default())
|
||||
.await
|
||||
{
|
||||
NegentropyEvent {
|
||||
kind: NegentropyKind::Events,
|
||||
total_event: report.received.len() as i32,
|
||||
}
|
||||
.emit(&app_handle)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// NEG: Sync metadata
|
||||
//
|
||||
let metadata = Filter::new()
|
||||
.authors(authors)
|
||||
.kind(Kind::Metadata)
|
||||
.limit(1000);
|
||||
|
||||
if let Ok(report) = client
|
||||
.reconcile_with(&bootstrap_relays, metadata, NegentropyOptions::default())
|
||||
.await
|
||||
{
|
||||
NegentropyEvent {
|
||||
kind: NegentropyKind::Metadata,
|
||||
total_event: report.received.len() as i32,
|
||||
}
|
||||
.emit(&app_handle)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NEG: Sync other metadata
|
||||
//
|
||||
let others = Filter::new().authors(public_keys.clone()).kinds(vec![
|
||||
Kind::Interests,
|
||||
Kind::InterestSet,
|
||||
Kind::FollowSet,
|
||||
Kind::EventDeletion,
|
||||
Kind::Custom(30315),
|
||||
]);
|
||||
|
||||
if let Ok(report) = client
|
||||
.reconcile_with(&bootstrap_relays, others, NegentropyOptions::default())
|
||||
.await
|
||||
{
|
||||
NegentropyEvent {
|
||||
kind: NegentropyKind::Others,
|
||||
total_event: report.received.len() as i32,
|
||||
}
|
||||
.emit(&app_handle)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// NEG: Sync notification
|
||||
//
|
||||
let notification = Filter::new()
|
||||
.pubkeys(public_keys)
|
||||
.kinds(vec![
|
||||
Kind::Reaction,
|
||||
Kind::TextNote,
|
||||
Kind::Repost,
|
||||
Kind::ZapReceipt,
|
||||
])
|
||||
.limit(10000);
|
||||
|
||||
if let Ok(report) = client
|
||||
.reconcile_with(
|
||||
&bootstrap_relays,
|
||||
notification,
|
||||
NegentropyOptions::default(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
NegentropyEvent {
|
||||
kind: NegentropyKind::Notification,
|
||||
total_event: report.received.len() as i32,
|
||||
}
|
||||
.emit(&app_handle)
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -22,6 +22,7 @@ pub struct Window {
|
||||
maximizable: bool,
|
||||
minimizable: bool,
|
||||
hidden_title: bool,
|
||||
closable: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Type)]
|
||||
@@ -109,7 +110,7 @@ pub fn reload_column(label: String, app_handle: tauri::AppHandle) -> Result<bool
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), String> {
|
||||
pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<String, String> {
|
||||
if let Some(current_window) = app_handle.get_window(&window.label) {
|
||||
if current_window.is_visible().unwrap_or_default() {
|
||||
let _ = current_window.set_focus();
|
||||
@@ -117,6 +118,8 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S
|
||||
let _ = current_window.show();
|
||||
let _ = current_window.set_focus();
|
||||
};
|
||||
|
||||
Ok(current_window.label().to_string())
|
||||
} else {
|
||||
let new_window = WebviewWindowBuilder::new(
|
||||
&app_handle,
|
||||
@@ -131,6 +134,7 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S
|
||||
.minimizable(window.minimizable)
|
||||
.maximizable(window.maximizable)
|
||||
.transparent(true)
|
||||
.closable(window.closable)
|
||||
.effects(WindowEffectsConfig {
|
||||
state: None,
|
||||
effects: vec![Effect::UnderWindowBackground],
|
||||
@@ -142,24 +146,26 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S
|
||||
|
||||
// Restore native border
|
||||
new_window.add_border(None);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(new_window.label().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[specta::specta]
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), String> {
|
||||
if let Some(window) = app_handle.get_window(&window.label) {
|
||||
if window.is_visible().unwrap_or_default() {
|
||||
let _ = window.set_focus();
|
||||
pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<String, String> {
|
||||
if let Some(current_window) = app_handle.get_window(&window.label) {
|
||||
if current_window.is_visible().unwrap_or_default() {
|
||||
let _ = current_window.set_focus();
|
||||
} else {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
let _ = current_window.show();
|
||||
let _ = current_window.set_focus();
|
||||
};
|
||||
|
||||
Ok(current_window.label().to_string())
|
||||
} else {
|
||||
let window = WebviewWindowBuilder::new(
|
||||
let new_window = WebviewWindowBuilder::new(
|
||||
&app_handle,
|
||||
&window.label,
|
||||
WebviewUrl::App(PathBuf::from(window.url)),
|
||||
@@ -171,6 +177,7 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S
|
||||
.maximizable(window.maximizable)
|
||||
.transparent(true)
|
||||
.decorations(false)
|
||||
.closable(window.closable)
|
||||
.effects(WindowEffectsConfig {
|
||||
state: None,
|
||||
effects: vec![Effect::Mica],
|
||||
@@ -181,7 +188,9 @@ pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), S
|
||||
.unwrap();
|
||||
|
||||
// Set decoration
|
||||
window.create_overlay_titlebar().unwrap();
|
||||
new_window.create_overlay_titlebar().unwrap();
|
||||
|
||||
Ok(new_window.label().to_string())
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use futures::future::join_all;
|
||||
use keyring_search::{Limit, List, Search};
|
||||
use linkify::LinkFinder;
|
||||
use nostr_sdk::prelude::*;
|
||||
use reqwest::Client as ReqClient;
|
||||
use serde::Serialize;
|
||||
use specta::Type;
|
||||
use std::{collections::HashSet, str::FromStr, time::Duration};
|
||||
use std::{collections::HashSet, str::FromStr};
|
||||
|
||||
use crate::RichEvent;
|
||||
|
||||
@@ -18,6 +19,8 @@ pub struct Meta {
|
||||
}
|
||||
|
||||
const IMAGES: [&str; 7] = ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"];
|
||||
// const VIDEOS: [&str; 6] = ["mp4", "avi", "mov", "mkv", "wmv", "webm"];
|
||||
|
||||
const NOSTR_EVENTS: [&str; 10] = [
|
||||
"@nevent1",
|
||||
"@note1",
|
||||
@@ -30,6 +33,7 @@ const NOSTR_EVENTS: [&str; 10] = [
|
||||
"Nostr:note1",
|
||||
"Nostr:nevent1",
|
||||
];
|
||||
|
||||
const NOSTR_MENTIONS: [&str; 10] = [
|
||||
"@npub1",
|
||||
"nostr:npub1",
|
||||
@@ -47,6 +51,15 @@ pub fn get_latest_event(events: &Events) -> Option<&Event> {
|
||||
events.iter().next()
|
||||
}
|
||||
|
||||
pub fn get_tags_content(event: &Event, kind: TagKind) -> Vec<String> {
|
||||
event
|
||||
.tags
|
||||
.iter()
|
||||
.filter(|t| t.kind() == kind)
|
||||
.filter_map(|t| t.content().map(|content| content.to_string()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn create_tags(content: &str) -> Vec<Tag> {
|
||||
let mut tags: Vec<Tag> = vec![];
|
||||
let mut tag_set: HashSet<String> = HashSet::new();
|
||||
@@ -65,7 +78,7 @@ pub fn create_tags(content: &str) -> Vec<Tag> {
|
||||
let hashtags = words
|
||||
.iter()
|
||||
.filter(|&&word| word.starts_with('#'))
|
||||
.map(|&s| s.to_string())
|
||||
.map(|&s| s.to_string().replace("#", "").to_lowercase())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for mention in mentions {
|
||||
@@ -128,6 +141,19 @@ pub fn create_tags(content: &str) -> Vec<Tag> {
|
||||
tags
|
||||
}
|
||||
|
||||
pub fn get_all_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") && !v.ends_with("Lume"))
|
||||
.map(String::from)
|
||||
.collect();
|
||||
|
||||
accounts.into_iter().collect()
|
||||
}
|
||||
|
||||
pub async fn process_event(client: &Client, events: Events) -> Vec<RichEvent> {
|
||||
// Remove event thread if event is TextNote
|
||||
let events: Vec<Event> = events
|
||||
@@ -201,38 +227,6 @@ pub async fn process_event(client: &Client, events: Events) -> Vec<RichEvent> {
|
||||
join_all(futures).await
|
||||
}
|
||||
|
||||
pub async fn init_nip65(client: &Client, public_key: &str) {
|
||||
let author = PublicKey::from_str(public_key).unwrap();
|
||||
let filter = Filter::new().author(author).kind(Kind::RelayList).limit(1);
|
||||
|
||||
// client.add_relay("ws://127.0.0.1:1984").await.unwrap();
|
||||
// client.connect_relay("ws://127.0.0.1:1984").await.unwrap();
|
||||
|
||||
if let Ok(events) = client
|
||||
.fetch_events(vec![filter], Some(Duration::from_secs(5)))
|
||||
.await
|
||||
{
|
||||
if let Some(event) = events.first() {
|
||||
let relay_list = nip65::extract_relay_list(event);
|
||||
for (url, metadata) in relay_list {
|
||||
let opts = match metadata {
|
||||
Some(RelayMetadata::Read) => RelayOptions::new().read(true).write(false),
|
||||
Some(_) => RelayOptions::new().write(true).read(false),
|
||||
None => RelayOptions::default(),
|
||||
};
|
||||
if let Err(e) = client.pool().add_relay(&url.to_string(), opts).await {
|
||||
eprintln!("Failed to add relay {}: {:?}", url, e);
|
||||
}
|
||||
if let Err(e) = client.connect_relay(url.to_string()).await {
|
||||
eprintln!("Failed to connect to relay {}: {:?}", url, e);
|
||||
} else {
|
||||
println!("Connecting to relay: {} - {:?}", url, metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn parse_event(content: &str) -> Meta {
|
||||
let mut finder = LinkFinder::new();
|
||||
finder.url_must_have_scheme(false);
|
||||
|
||||
@@ -5,14 +5,20 @@
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use border::WebviewWindowExt as BorderWebviewWindowExt;
|
||||
use commands::{account::*, event::*, metadata::*, relay::*, window::*};
|
||||
use common::parse_event;
|
||||
use commands::{
|
||||
account::*,
|
||||
event::*,
|
||||
metadata::*,
|
||||
relay::*,
|
||||
sync::{run_fast_sync, NegentropyEvent},
|
||||
window::*,
|
||||
};
|
||||
use common::{get_all_accounts, get_tags_content, parse_event};
|
||||
use nostr_sdk::prelude::{Profile as DatabaseProfile, *};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use specta_typescript::Typescript;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fs,
|
||||
io::{self, BufRead},
|
||||
str::FromStr,
|
||||
@@ -30,8 +36,9 @@ pub mod common;
|
||||
pub struct Nostr {
|
||||
client: Client,
|
||||
settings: Mutex<Settings>,
|
||||
contact_list: Mutex<Vec<Contact>>,
|
||||
trusted_list: Mutex<HashSet<PublicKey>>,
|
||||
accounts: Mutex<Vec<String>>,
|
||||
subscriptions: Mutex<Vec<SubscriptionId>>,
|
||||
bootstrap_relays: Mutex<Vec<Url>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Type)]
|
||||
@@ -76,14 +83,14 @@ struct Subscription {
|
||||
label: String,
|
||||
kind: SubKind,
|
||||
event_id: Option<String>,
|
||||
contacts: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Type, Clone, TauriEvent)]
|
||||
struct NewSettings(Settings);
|
||||
|
||||
pub const DEFAULT_DIFFICULTY: u8 = 21;
|
||||
pub const FETCH_LIMIT: usize = 100;
|
||||
pub const NOTIFICATION_NEG_LIMIT: usize = 64;
|
||||
pub const FETCH_LIMIT: usize = 50;
|
||||
pub const NOTIFICATION_SUB_ID: &str = "lume_notification";
|
||||
|
||||
fn main() {
|
||||
@@ -99,22 +106,21 @@ fn main() {
|
||||
get_bootstrap_relays,
|
||||
save_bootstrap_relays,
|
||||
get_accounts,
|
||||
create_account,
|
||||
watch_account,
|
||||
import_account,
|
||||
connect_account,
|
||||
get_private_key,
|
||||
delete_account,
|
||||
reset_password,
|
||||
is_account_sync,
|
||||
create_sync_file,
|
||||
login,
|
||||
has_signer,
|
||||
set_signer,
|
||||
get_profile,
|
||||
set_profile,
|
||||
get_contact_list,
|
||||
set_contact_list,
|
||||
check_contact,
|
||||
is_contact,
|
||||
toggle_contact,
|
||||
get_mention_list,
|
||||
get_all_profiles,
|
||||
set_group,
|
||||
get_group,
|
||||
get_all_groups,
|
||||
@@ -131,7 +137,6 @@ fn main() {
|
||||
get_user_settings,
|
||||
set_user_settings,
|
||||
verify_nip05,
|
||||
is_trusted_user,
|
||||
get_event_meta,
|
||||
get_event,
|
||||
get_event_from,
|
||||
@@ -142,12 +147,13 @@ fn main() {
|
||||
get_all_events_by_hashtags,
|
||||
get_local_events,
|
||||
get_global_events,
|
||||
is_deleted_event,
|
||||
request_delete,
|
||||
search,
|
||||
publish,
|
||||
reply,
|
||||
repost,
|
||||
is_reposted,
|
||||
request_delete,
|
||||
is_deleted_event,
|
||||
event_to_bech32,
|
||||
user_to_bech32,
|
||||
create_column,
|
||||
@@ -158,7 +164,7 @@ fn main() {
|
||||
reopen_lume,
|
||||
quit
|
||||
])
|
||||
.events(collect_events![Subscription, NewSettings]);
|
||||
.events(collect_events![Subscription, NewSettings, NegentropyEvent]);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
builder
|
||||
@@ -179,6 +185,7 @@ fn main() {
|
||||
let handle = app.handle();
|
||||
let handle_clone = handle.clone();
|
||||
let handle_clone_child = handle_clone.clone();
|
||||
let handle_clone_sync = handle_clone_child.clone();
|
||||
let main_window = app.get_webview_window("main").unwrap();
|
||||
|
||||
let config_dir = handle
|
||||
@@ -216,7 +223,7 @@ fn main() {
|
||||
}
|
||||
});
|
||||
|
||||
let client = tauri::async_runtime::block_on(async move {
|
||||
let (client, bootstrap_relays) = tauri::async_runtime::block_on(async move {
|
||||
// Setup database
|
||||
let database = NostrLMDB::open(config_dir.join("nostr-lmdb"))
|
||||
.expect("Error: cannot create database.");
|
||||
@@ -224,10 +231,10 @@ fn main() {
|
||||
// Config
|
||||
let opts = Options::new()
|
||||
.gossip(true)
|
||||
.max_avg_latency(Duration::from_millis(500))
|
||||
.max_avg_latency(Duration::from_millis(800))
|
||||
.automatic_authentication(false)
|
||||
.connection_timeout(Some(Duration::from_secs(20)))
|
||||
.send_timeout(Some(Duration::from_secs(10)))
|
||||
.send_timeout(Some(Duration::from_secs(20)))
|
||||
.timeout(Duration::from_secs(20));
|
||||
|
||||
// Setup nostr client
|
||||
@@ -279,17 +286,27 @@ fn main() {
|
||||
// Connect
|
||||
client.connect_with_timeout(Duration::from_secs(10)).await;
|
||||
|
||||
client
|
||||
// Get all bootstrap relays
|
||||
let bootstrap_relays: Vec<Url> =
|
||||
client.pool().all_relays().await.into_keys().collect();
|
||||
|
||||
(client, bootstrap_relays)
|
||||
});
|
||||
|
||||
let accounts = get_all_accounts();
|
||||
// Run fast sync for all accounts
|
||||
run_fast_sync(accounts.clone(), handle_clone_sync);
|
||||
|
||||
// Create global state
|
||||
app.manage(Nostr {
|
||||
client,
|
||||
accounts: Mutex::new(accounts),
|
||||
settings: Mutex::new(Settings::default()),
|
||||
contact_list: Mutex::new(Vec::new()),
|
||||
trusted_list: Mutex::new(HashSet::new()),
|
||||
subscriptions: Mutex::new(Vec::new()),
|
||||
bootstrap_relays: Mutex::new(bootstrap_relays),
|
||||
});
|
||||
|
||||
// Handle subscription
|
||||
Subscription::listen_any(app, move |event| {
|
||||
let handle = handle_clone_child.to_owned();
|
||||
let payload = event.payload;
|
||||
@@ -302,42 +319,84 @@ fn main() {
|
||||
SubKind::Subscribe => {
|
||||
let subscription_id = SubscriptionId::new(payload.label);
|
||||
|
||||
match payload.event_id {
|
||||
Some(id) => {
|
||||
if !client
|
||||
.pool()
|
||||
.subscriptions()
|
||||
.await
|
||||
.contains_key(&subscription_id)
|
||||
{
|
||||
// Update state
|
||||
state
|
||||
.subscriptions
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(subscription_id.clone());
|
||||
|
||||
println!(
|
||||
"Total subscriptions: {}",
|
||||
state.subscriptions.lock().unwrap().len()
|
||||
);
|
||||
|
||||
if let Some(id) = payload.event_id {
|
||||
let event_id = EventId::from_str(&id).unwrap();
|
||||
let filter =
|
||||
Filter::new().event(event_id).since(Timestamp::now());
|
||||
|
||||
if let Err(e) = client
|
||||
.subscribe_with_id(subscription_id, vec![filter], None)
|
||||
.subscribe_with_id(
|
||||
subscription_id.clone(),
|
||||
vec![filter],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
println!("Subscription error: {}", e)
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let contact_list = state.contact_list.lock().unwrap().clone();
|
||||
if !contact_list.is_empty() {
|
||||
let authors: Vec<PublicKey> =
|
||||
contact_list.iter().map(|f| f.public_key).collect();
|
||||
|
||||
let filter = Filter::new()
|
||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||
.authors(authors)
|
||||
.since(Timestamp::now());
|
||||
if let Some(ids) = payload.contacts {
|
||||
let authors: Vec<PublicKey> = ids
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
if let Ok(pk) = PublicKey::from_str(item) {
|
||||
Some(pk)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if let Err(e) = client
|
||||
.subscribe_with_id(subscription_id, vec![filter], None)
|
||||
.await
|
||||
{
|
||||
println!("Subscription error: {}", e)
|
||||
}
|
||||
if let Err(e) = client
|
||||
.subscribe_with_id(
|
||||
subscription_id,
|
||||
vec![Filter::new()
|
||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||
.authors(authors)
|
||||
.since(Timestamp::now())],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
println!("Subscription error: {}", e)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
SubKind::Unsubscribe => {
|
||||
let subscription_id = SubscriptionId::new(payload.label);
|
||||
let mut sub_state = state.subscriptions.lock().unwrap().clone();
|
||||
|
||||
if let Some(pos) = sub_state.iter().position(|x| *x == subscription_id)
|
||||
{
|
||||
sub_state.remove(pos);
|
||||
state.subscriptions.lock().unwrap().clone_from(&sub_state)
|
||||
}
|
||||
|
||||
println!(
|
||||
"Total subscriptions: {}",
|
||||
state.subscriptions.lock().unwrap().len()
|
||||
);
|
||||
|
||||
client.unsubscribe(subscription_id).await
|
||||
}
|
||||
}
|
||||
@@ -363,6 +422,30 @@ fn main() {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let state = handle_clone.state::<Nostr>();
|
||||
let client = &state.client;
|
||||
let accounts = state.accounts.lock().unwrap().clone();
|
||||
|
||||
let public_keys: Vec<PublicKey> = accounts
|
||||
.iter()
|
||||
.filter_map(|acc| {
|
||||
if let Ok(pk) = PublicKey::from_str(acc) {
|
||||
Some(pk)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Subscribe for new notification
|
||||
if let Ok(e) = client
|
||||
.subscribe_with_id(
|
||||
SubscriptionId::new(NOTIFICATION_SUB_ID),
|
||||
vec![Filter::new().pubkeys(public_keys).since(Timestamp::now())],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
println!("Subscribed for notification on {} relays", e.success.len())
|
||||
}
|
||||
|
||||
let allow_notification = match handle_clone.notification().request_permission() {
|
||||
Ok(_) => {
|
||||
@@ -377,6 +460,7 @@ fn main() {
|
||||
|
||||
let notification_id = SubscriptionId::new(NOTIFICATION_SUB_ID);
|
||||
let mut notifications = client.pool().notifications();
|
||||
let mut new_events: Vec<EventId> = Vec::new();
|
||||
|
||||
while let Ok(notification) = notifications.recv().await {
|
||||
match notification {
|
||||
@@ -394,6 +478,17 @@ fn main() {
|
||||
println!("Error: {}", e);
|
||||
}
|
||||
|
||||
// Workaround for https://github.com/rust-nostr/nostr/issues/509
|
||||
// TODO: remove
|
||||
let _ = client
|
||||
.fetch_events(
|
||||
vec![Filter::new()
|
||||
.kind(Kind::TextNote)
|
||||
.limit(0)],
|
||||
Some(Duration::from_secs(5)),
|
||||
)
|
||||
.await;
|
||||
|
||||
if allow_notification {
|
||||
if let Err(e) = &handle_clone
|
||||
.notification()
|
||||
@@ -426,8 +521,12 @@ fn main() {
|
||||
event,
|
||||
} = message
|
||||
{
|
||||
let tags = get_tags_content(&event, TagKind::p());
|
||||
|
||||
// Handle events from notification subscription
|
||||
if subscription_id == notification_id {
|
||||
if subscription_id == notification_id
|
||||
&& tags.iter().any(|item| accounts.iter().any(|i| i == item))
|
||||
{
|
||||
// Send native notification
|
||||
if allow_notification {
|
||||
let author = client
|
||||
@@ -437,27 +536,46 @@ fn main() {
|
||||
.unwrap_or_else(|_| {
|
||||
DatabaseProfile::new(event.pubkey, Metadata::new())
|
||||
});
|
||||
let metadata = author.metadata();
|
||||
|
||||
send_event_notification(&event, metadata, &handle_clone);
|
||||
send_event_notification(
|
||||
&event,
|
||||
author.metadata(),
|
||||
&handle_clone,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let label = subscription_id.to_string();
|
||||
let raw = event.as_json();
|
||||
let parsed = if event.kind == Kind::TextNote {
|
||||
Some(parse_event(&event.content).await)
|
||||
} else {
|
||||
None
|
||||
let payload = RichEvent {
|
||||
raw: event.as_json(),
|
||||
parsed: if event.kind == Kind::TextNote {
|
||||
Some(parse_event(&event.content).await)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
};
|
||||
|
||||
handle_clone
|
||||
.emit_to(
|
||||
EventTarget::labeled(label),
|
||||
EventTarget::labeled(subscription_id.to_string()),
|
||||
"event",
|
||||
RichEvent { raw, parsed },
|
||||
payload,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
if state
|
||||
.subscriptions
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|i| i == &subscription_id)
|
||||
{
|
||||
new_events.push(event.id);
|
||||
|
||||
if new_events.len() > 5 {
|
||||
handle_clone.emit("synchronized", ()).unwrap();
|
||||
new_events.clear();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
RelayPoolNotification::Shutdown => break,
|
||||
@@ -468,47 +586,6 @@ fn main() {
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.on_window_event(|window, event| {
|
||||
if let tauri::WindowEvent::Focused(focused) = event {
|
||||
if !focused {
|
||||
let handle = window.app_handle().to_owned();
|
||||
let config_dir = handle.path().app_config_dir().unwrap();
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let state = handle.state::<Nostr>();
|
||||
let client = &state.client;
|
||||
|
||||
if let Ok(signer) = client.signer().await {
|
||||
let public_key = signer.public_key().await.unwrap();
|
||||
let bech32 = public_key.to_bech32().unwrap();
|
||||
|
||||
if fs::metadata(config_dir.join(bech32)).is_ok() {
|
||||
if let Ok(contact_list) =
|
||||
client.get_contact_list(Some(Duration::from_secs(5))).await
|
||||
{
|
||||
let authors: Vec<PublicKey> =
|
||||
contact_list.iter().map(|f| f.public_key).collect();
|
||||
|
||||
if client
|
||||
.reconcile(
|
||||
Filter::new()
|
||||
.authors(authors)
|
||||
.kinds(vec![Kind::TextNote, Kind::Repost])
|
||||
.limit(1000),
|
||||
NegentropyOptions::default(),
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
handle.emit("synchronized", ()).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.plugin(prevent_default())
|
||||
.plugin(tauri_plugin_decorum::init())
|
||||
.plugin(tauri_plugin_store::Builder::default().build())
|
||||
|
||||
Reference in New Issue
Block a user