feat: add basic web of trust
This commit is contained in:
@@ -258,82 +258,105 @@ pub async fn login(
|
|||||||
// Connect to user's relay (NIP-65)
|
// Connect to user's relay (NIP-65)
|
||||||
init_nip65(client).await;
|
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 {
|
tauri::async_runtime::spawn(async move {
|
||||||
let state = handle.state::<Nostr>();
|
let state = handle.state::<Nostr>();
|
||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
let contact_list = state.contact_list.lock().await;
|
|
||||||
|
|
||||||
let signer = client.signer().await.unwrap();
|
let signer = client.signer().await.unwrap();
|
||||||
let public_key = signer.public_key().await.unwrap();
|
let public_key = signer.public_key().await.unwrap();
|
||||||
|
|
||||||
let notification_id = SubscriptionId::new(NOTIFICATION_SUB_ID);
|
let notification_id = SubscriptionId::new(NOTIFICATION_SUB_ID);
|
||||||
|
let notification = Filter::new().pubkey(public_key).kinds(vec![
|
||||||
if !contact_list.is_empty() {
|
Kind::TextNote,
|
||||||
let authors: Vec<PublicKey> = contact_list.iter().map(|f| f.public_key).collect();
|
Kind::Repost,
|
||||||
let sync = Filter::new()
|
Kind::Reaction,
|
||||||
.authors(authors.clone())
|
Kind::ZapReceipt,
|
||||||
.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);
|
|
||||||
|
|
||||||
// Sync notification with negentropy
|
// Sync notification with negentropy
|
||||||
if client
|
let _ = client
|
||||||
.reconcile(sync, NegentropyOptions::default())
|
.reconcile(
|
||||||
.await
|
notification.clone().limit(NOTIFICATION_NEG_LIMIT),
|
||||||
.is_ok()
|
NegentropyOptions::default(),
|
||||||
{
|
)
|
||||||
handle.emit("notification_synchronized", ()).unwrap();
|
.await;
|
||||||
}
|
|
||||||
|
|
||||||
let notification = Filter::new()
|
|
||||||
.pubkey(public_key)
|
|
||||||
.kinds(vec![
|
|
||||||
Kind::TextNote,
|
|
||||||
Kind::Repost,
|
|
||||||
Kind::Reaction,
|
|
||||||
Kind::ZapReceipt,
|
|
||||||
])
|
|
||||||
.since(Timestamp::now());
|
|
||||||
|
|
||||||
// Subscribing for new notification...
|
// Subscribing for new notification...
|
||||||
if let Err(e) = client
|
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
|
.await
|
||||||
{
|
{
|
||||||
println!("Error: {}", e)
|
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<PublicKey> = 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<PublicKey> = 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)
|
Ok(public_key)
|
||||||
|
|||||||
@@ -483,3 +483,13 @@ pub async fn verify_nip05(id: String, nip05: &str) -> Result<bool, String> {
|
|||||||
Err(e) => Err(e.to_string()),
|
Err(e) => Err(e.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub async fn is_trusted_user(id: String, state: State<'_, Nostr>) -> Result<bool, String> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use specta::Type;
|
use specta::Type;
|
||||||
use specta_typescript::Typescript;
|
use specta_typescript::Typescript;
|
||||||
use std::{
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
fs,
|
fs,
|
||||||
io::{self, BufRead},
|
io::{self, BufRead},
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
@@ -30,6 +31,7 @@ pub struct Nostr {
|
|||||||
client: Client,
|
client: Client,
|
||||||
contact_list: Mutex<Vec<Contact>>,
|
contact_list: Mutex<Vec<Contact>>,
|
||||||
settings: Mutex<Settings>,
|
settings: Mutex<Settings>,
|
||||||
|
circles: Mutex<HashMap<PublicKey, Vec<PublicKey>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Type)]
|
#[derive(Clone, Serialize, Deserialize, Type)]
|
||||||
@@ -38,6 +40,7 @@ pub struct Settings {
|
|||||||
image_resize_service: Option<String>,
|
image_resize_service: Option<String>,
|
||||||
use_relay_hint: bool,
|
use_relay_hint: bool,
|
||||||
content_warning: bool,
|
content_warning: bool,
|
||||||
|
trusted_only: bool,
|
||||||
display_avatar: bool,
|
display_avatar: bool,
|
||||||
display_zap_button: bool,
|
display_zap_button: bool,
|
||||||
display_repost_button: bool,
|
display_repost_button: bool,
|
||||||
@@ -52,6 +55,7 @@ impl Default for Settings {
|
|||||||
image_resize_service: Some("https://wsrv.nl".to_string()),
|
image_resize_service: Some("https://wsrv.nl".to_string()),
|
||||||
use_relay_hint: true,
|
use_relay_hint: true,
|
||||||
content_warning: true,
|
content_warning: true,
|
||||||
|
trusted_only: true,
|
||||||
display_avatar: true,
|
display_avatar: true,
|
||||||
display_zap_button: true,
|
display_zap_button: true,
|
||||||
display_repost_button: true,
|
display_repost_button: true,
|
||||||
@@ -121,6 +125,7 @@ fn main() {
|
|||||||
get_settings,
|
get_settings,
|
||||||
set_settings,
|
set_settings,
|
||||||
verify_nip05,
|
verify_nip05,
|
||||||
|
is_trusted_user,
|
||||||
get_event_meta,
|
get_event_meta,
|
||||||
get_event,
|
get_event,
|
||||||
get_event_from,
|
get_event_from,
|
||||||
@@ -265,6 +270,7 @@ fn main() {
|
|||||||
client,
|
client,
|
||||||
contact_list: Mutex::new(vec![]),
|
contact_list: Mutex::new(vec![]),
|
||||||
settings: Mutex::new(Settings::default()),
|
settings: Mutex::new(Settings::default()),
|
||||||
|
circles: Mutex::new(HashMap::new()),
|
||||||
});
|
});
|
||||||
|
|
||||||
Subscription::listen_any(app, move |event| {
|
Subscription::listen_any(app, move |event| {
|
||||||
|
|||||||
@@ -256,6 +256,14 @@ async verifyNip05(id: string, nip05: string) : Promise<Result<boolean, string>>
|
|||||||
else return { status: "error", error: e as any };
|
else return { status: "error", error: e as any };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async isTrustedUser(id: string) : Promise<Result<boolean, string>> {
|
||||||
|
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<Result<Meta, null>> {
|
async getEventMeta(content: string) : Promise<Result<Meta, null>> {
|
||||||
try {
|
try {
|
||||||
return { status: "ok", data: await TAURI_INVOKE("get_event_meta", { content }) };
|
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 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 Relays = { connected: string[]; read: string[] | null; write: string[] | null; both: string[] | null }
|
||||||
export type RichEvent = { raw: string; parsed: Meta | 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 SubKind = "Subscribe" | "Unsubscribe"
|
||||||
export type Subscription = { label: string; kind: SubKind; event_id: string | null }
|
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 }
|
export type Window = { label: string; title: string; url: string; width: number; height: number; maximizable: boolean; minimizable: boolean; hidden_title: boolean }
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { commands } from "@/commands.gen";
|
||||||
import { cn, replyTime } from "@/commons";
|
import { cn, replyTime } from "@/commons";
|
||||||
import { Note } from "@/components/note";
|
import { Note } from "@/components/note";
|
||||||
import { type LumeEvent, LumeWindow } from "@/system";
|
import { type LumeEvent, LumeWindow } from "@/system";
|
||||||
@@ -5,7 +6,7 @@ import { CaretDown } from "@phosphor-icons/react";
|
|||||||
import { Link, useSearch } from "@tanstack/react-router";
|
import { Link, useSearch } from "@tanstack/react-router";
|
||||||
import { Menu, MenuItem } from "@tauri-apps/api/menu";
|
import { Menu, MenuItem } from "@tauri-apps/api/menu";
|
||||||
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
||||||
import { memo, useCallback } from "react";
|
import { memo, useCallback, useEffect, useState } from "react";
|
||||||
import { User } from "./user";
|
import { User } from "./user";
|
||||||
|
|
||||||
export const ReplyNote = memo(function ReplyNote({
|
export const ReplyNote = memo(function ReplyNote({
|
||||||
@@ -16,6 +17,7 @@ export const ReplyNote = memo(function ReplyNote({
|
|||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
const search = useSearch({ strict: false });
|
const search = useSearch({ strict: false });
|
||||||
|
const [isTrusted, setIsTrusted] = useState<boolean>(null);
|
||||||
|
|
||||||
const showContextMenu = useCallback(async (e: React.MouseEvent) => {
|
const showContextMenu = useCallback(async (e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -41,6 +43,22 @@ export const ReplyNote = memo(function ReplyNote({
|
|||||||
await menu.popup().catch((e) => console.error(e));
|
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 <div>Not trusted</div>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Note.Provider event={event}>
|
<Note.Provider event={event}>
|
||||||
<User.Provider pubkey={event.pubkey}>
|
<User.Provider pubkey={event.pubkey}>
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export function Screen() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unlisten = listen("newsfeed_synchronized", async () => {
|
const unlisten = listen("synchronized", async () => {
|
||||||
await queryClient.invalidateQueries({ queryKey: [label, account] });
|
await queryClient.invalidateQueries({ queryKey: [label, account] });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user