From 9152c3e12287f09d106e7816a71c8b671f799394 Mon Sep 17 00:00:00 2001 From: reya Date: Mon, 23 Sep 2024 13:24:33 +0700 Subject: [PATCH] feat: add basic web of trust --- src-tauri/src/commands/account.rs | 141 +++++++++++-------- src-tauri/src/commands/metadata.rs | 10 ++ src-tauri/src/main.rs | 6 + src/commands.gen.ts | 10 +- src/components/reply.tsx | 20 ++- src/routes/columns/_layout/newsfeed.lazy.tsx | 2 +- 6 files changed, 127 insertions(+), 62 deletions(-) diff --git a/src-tauri/src/commands/account.rs b/src-tauri/src/commands/account.rs index 2ed54b06..f678c426 100644 --- a/src-tauri/src/commands/account.rs +++ b/src-tauri/src/commands/account.rs @@ -258,82 +258,105 @@ pub async fn login( // Connect to user's relay (NIP-65) init_nip65(client).await; - // Get user's contact list - if let Ok(contacts) = client.get_contact_list(Some(Duration::from_secs(5))).await { - let mut contacts_state = state.contact_list.lock().await; - *contacts_state = contacts; - }; - - // Get user's settings - if let Ok(settings) = get_user_settings(client).await { - let mut settings_state = state.settings.lock().await; - *settings_state = settings; - }; - tauri::async_runtime::spawn(async move { let state = handle.state::(); let client = &state.client; - let contact_list = state.contact_list.lock().await; let signer = client.signer().await.unwrap(); let public_key = signer.public_key().await.unwrap(); let notification_id = SubscriptionId::new(NOTIFICATION_SUB_ID); - - if !contact_list.is_empty() { - let authors: Vec = contact_list.iter().map(|f| f.public_key).collect(); - let sync = Filter::new() - .authors(authors.clone()) - .kinds(vec![Kind::TextNote, Kind::Repost]) - .limit(NEWSFEED_NEG_LIMIT); - - if client - .reconcile(sync, NegentropyOptions::default()) - .await - .is_ok() - { - handle.emit("newsfeed_synchronized", ()).unwrap(); - } - }; - - drop(contact_list); - - let sync = Filter::new() - .pubkey(public_key) - .kinds(vec![ - Kind::TextNote, - Kind::Repost, - Kind::Reaction, - Kind::ZapReceipt, - ]) - .limit(NOTIFICATION_NEG_LIMIT); + let notification = Filter::new().pubkey(public_key).kinds(vec![ + Kind::TextNote, + Kind::Repost, + Kind::Reaction, + Kind::ZapReceipt, + ]); // Sync notification with negentropy - if client - .reconcile(sync, NegentropyOptions::default()) - .await - .is_ok() - { - handle.emit("notification_synchronized", ()).unwrap(); - } - - let notification = Filter::new() - .pubkey(public_key) - .kinds(vec![ - Kind::TextNote, - Kind::Repost, - Kind::Reaction, - Kind::ZapReceipt, - ]) - .since(Timestamp::now()); + let _ = client + .reconcile( + notification.clone().limit(NOTIFICATION_NEG_LIMIT), + NegentropyOptions::default(), + ) + .await; // Subscribing for new notification... if let Err(e) = client - .subscribe_with_id(notification_id, vec![notification], None) + .subscribe_with_id( + notification_id, + vec![notification.since(Timestamp::now())], + None, + ) .await { println!("Error: {}", e) } + + // Get user's settings + if let Ok(settings) = get_user_settings(client).await { + state.settings.lock().await.clone_from(&settings); + }; + + // Get user's contact list + if let Ok(contacts) = client.get_contact_list(Some(Duration::from_secs(5))).await { + state.contact_list.lock().await.clone_from(&contacts); + + if !contacts.is_empty() { + let pubkeys: Vec = contacts.iter().map(|f| f.public_key).collect(); + + let newsfeed = Filter::new() + .authors(pubkeys.clone()) + .kinds(vec![Kind::TextNote, Kind::Repost]) + .limit(NEWSFEED_NEG_LIMIT); + + if client + .reconcile(newsfeed, NegentropyOptions::default()) + .await + .is_ok() + { + handle.emit("synchronized", ()).unwrap(); + } + + let filter = Filter::new() + .authors(pubkeys.clone()) + .kind(Kind::ContactList) + .limit(4000); + + if client + .reconcile(filter, NegentropyOptions::default()) + .await + .is_ok() + { + for pubkey in pubkeys.into_iter() { + let mut list: Vec = Vec::new(); + let f = Filter::new() + .author(pubkey) + .kind(Kind::ContactList) + .limit(1); + + if let Ok(events) = client.database().query(vec![f]).await { + 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() + { + list.push(public_key) + } + } + + if !list.is_empty() { + state.circles.lock().await.insert(pubkey, list); + }; + } + } + } + } + }; + }; }); Ok(public_key) diff --git a/src-tauri/src/commands/metadata.rs b/src-tauri/src/commands/metadata.rs index 582fb79f..3bc81594 100644 --- a/src-tauri/src/commands/metadata.rs +++ b/src-tauri/src/commands/metadata.rs @@ -483,3 +483,13 @@ pub async fn verify_nip05(id: String, nip05: &str) -> Result { Err(e) => Err(e.to_string()), } } + +#[tauri::command] +#[specta::specta] +pub async fn is_trusted_user(id: String, state: State<'_, Nostr>) -> Result { + let circles = &state.circles.lock().await; + let public_key = PublicKey::from_str(&id).map_err(|e| e.to_string())?; + let trusted = circles.values().any(|v| v.contains(&public_key)); + + Ok(trusted) +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index f01fa5d2..880bfe21 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -12,6 +12,7 @@ use serde::{Deserialize, Serialize}; use specta::Type; use specta_typescript::Typescript; use std::{ + collections::HashMap, fs, io::{self, BufRead}, str::FromStr, @@ -30,6 +31,7 @@ pub struct Nostr { client: Client, contact_list: Mutex>, settings: Mutex, + circles: Mutex>>, } #[derive(Clone, Serialize, Deserialize, Type)] @@ -38,6 +40,7 @@ pub struct Settings { image_resize_service: Option, use_relay_hint: bool, content_warning: bool, + trusted_only: bool, display_avatar: bool, display_zap_button: bool, display_repost_button: bool, @@ -52,6 +55,7 @@ impl Default for Settings { image_resize_service: Some("https://wsrv.nl".to_string()), use_relay_hint: true, content_warning: true, + trusted_only: true, display_avatar: true, display_zap_button: true, display_repost_button: true, @@ -121,6 +125,7 @@ fn main() { get_settings, set_settings, verify_nip05, + is_trusted_user, get_event_meta, get_event, get_event_from, @@ -265,6 +270,7 @@ fn main() { client, contact_list: Mutex::new(vec![]), settings: Mutex::new(Settings::default()), + circles: Mutex::new(HashMap::new()), }); Subscription::listen_any(app, move |event| { diff --git a/src/commands.gen.ts b/src/commands.gen.ts index e1ab23e5..f75eae8b 100644 --- a/src/commands.gen.ts +++ b/src/commands.gen.ts @@ -256,6 +256,14 @@ async verifyNip05(id: string, nip05: string) : Promise> else return { status: "error", error: e as any }; } }, +async isTrustedUser(id: string) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("is_trusted_user", { id }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, async getEventMeta(content: string) : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("get_event_meta", { content }) }; @@ -463,7 +471,7 @@ export type NewSettings = Settings export type Profile = { name: string; display_name: string; about: string | null; picture: string; banner: string | null; nip05: string | null; lud16: string | null; website: string | null } export type Relays = { connected: string[]; read: string[] | null; write: string[] | null; both: string[] | null } export type RichEvent = { raw: string; parsed: Meta | null } -export type Settings = { proxy: string | null; image_resize_service: string | null; use_relay_hint: boolean; content_warning: boolean; display_avatar: boolean; display_zap_button: boolean; display_repost_button: boolean; display_media: boolean; transparent: boolean } +export type Settings = { proxy: string | null; image_resize_service: string | null; use_relay_hint: boolean; content_warning: boolean; trusted_only: boolean; display_avatar: boolean; display_zap_button: boolean; display_repost_button: boolean; display_media: boolean; transparent: boolean } export type SubKind = "Subscribe" | "Unsubscribe" export type Subscription = { label: string; kind: SubKind; event_id: string | null } export type Window = { label: string; title: string; url: string; width: number; height: number; maximizable: boolean; minimizable: boolean; hidden_title: boolean } diff --git a/src/components/reply.tsx b/src/components/reply.tsx index 317bb5c1..87be27f6 100644 --- a/src/components/reply.tsx +++ b/src/components/reply.tsx @@ -1,3 +1,4 @@ +import { commands } from "@/commands.gen"; import { cn, replyTime } from "@/commons"; import { Note } from "@/components/note"; import { type LumeEvent, LumeWindow } from "@/system"; @@ -5,7 +6,7 @@ import { CaretDown } from "@phosphor-icons/react"; import { Link, useSearch } from "@tanstack/react-router"; import { Menu, MenuItem } from "@tauri-apps/api/menu"; import { writeText } from "@tauri-apps/plugin-clipboard-manager"; -import { memo, useCallback } from "react"; +import { memo, useCallback, useEffect, useState } from "react"; import { User } from "./user"; export const ReplyNote = memo(function ReplyNote({ @@ -16,6 +17,7 @@ export const ReplyNote = memo(function ReplyNote({ className?: string; }) { const search = useSearch({ strict: false }); + const [isTrusted, setIsTrusted] = useState(null); const showContextMenu = useCallback(async (e: React.MouseEvent) => { e.preventDefault(); @@ -41,6 +43,22 @@ export const ReplyNote = memo(function ReplyNote({ await menu.popup().catch((e) => console.error(e)); }, []); + useEffect(() => { + async function check() { + const res = await commands.isTrustedUser(event.pubkey); + + if (res.status === "ok") { + setIsTrusted(res.data); + } + } + + check(); + }, []); + + if (isTrusted !== null && isTrusted === false) { + return
Not trusted
; + } + return ( diff --git a/src/routes/columns/_layout/newsfeed.lazy.tsx b/src/routes/columns/_layout/newsfeed.lazy.tsx index 03824756..e360bec3 100644 --- a/src/routes/columns/_layout/newsfeed.lazy.tsx +++ b/src/routes/columns/_layout/newsfeed.lazy.tsx @@ -82,7 +82,7 @@ export function Screen() { ); useEffect(() => { - const unlisten = listen("newsfeed_synchronized", async () => { + const unlisten = listen("synchronized", async () => { await queryClient.invalidateQueries({ queryKey: [label, account] }); });