From 10d74c53966ec5349a851481a5045fd058a18e7d Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Tue, 20 Oct 2026 10:01:13 +0700 Subject: [PATCH] auto-trust web of trust chat requests behind a setting --- crates/chat/src/lib.rs | 23 +++++++++++++++-- crates/chat/src/room.rs | 30 +++++++++++++++++++-- crates/settings/src/lib.rs | 6 +++++ crates/workspace/src/dialogs/settings.rs | 11 ++++++++ docs/web-of-trust.md | 33 +++++++++++------------- 5 files changed, 81 insertions(+), 22 deletions(-) diff --git a/crates/chat/src/lib.rs b/crates/chat/src/lib.rs index ef1668c8..c64db7fb 100644 --- a/crates/chat/src/lib.rs +++ b/crates/chat/src/lib.rs @@ -577,7 +577,12 @@ impl ChatRegistry { return; }; - let room: Room = room.into().organize(&public_key); + let mut room: Room = room.into().organize(&public_key); + + if room.auto_trusted(public_key, cx) { + room.kind = RoomKind::Ongoing; + } + let room_id = room.id; let entity = cx.new(|_| room); entity.update(cx, |room, cx| room.ensure_directory(cx)); @@ -652,6 +657,11 @@ impl ChatRegistry { /// Extend the registry with new rooms. fn extend_rooms(&mut self, rooms: HashSet, cx: &mut Context) { + let nostr = NostrRegistry::global(cx); + let Some(me) = nostr.read(cx).current_user() else { + return; + }; + let mut room_map: HashMap = self .rooms .iter() @@ -660,6 +670,13 @@ impl ChatRegistry { .collect(); for new_room in rooms.into_iter() { + // Auto-trust also applies to rooms loaded from the database + let new_room = if new_room.kind == RoomKind::Request && new_room.auto_trusted(me, cx) { + new_room.kind(RoomKind::Ongoing) + } else { + new_room + }; + // Check if we already have a room with this ID if let Some(&index) = room_map.get(&new_room.id) { self.rooms[index].update(cx, |this, cx| { @@ -789,7 +806,9 @@ impl ChatRegistry { match self.room_index.get(&message.room).cloned() { Some(room) => { room.update(cx, |this, cx| { - if this.kind == RoomKind::Request && message.rumor.pubkey == public_key { + if this.kind == RoomKind::Request + && (message.rumor.pubkey == public_key || this.auto_trusted(public_key, cx)) + { this.set_ongoing(cx); } this.push_message(message, cx); diff --git a/crates/chat/src/room.rs b/crates/chat/src/room.rs index c119f823..18b58be2 100644 --- a/crates/chat/src/room.rs +++ b/crates/chat/src/room.rs @@ -8,8 +8,8 @@ use instant::Duration; use itertools::Itertools; use nip4e::DeviceRegistry; use nostr_sdk::prelude::*; -use person::{Directory, Person}; -use settings::{RoomConfig, SignerKind}; +use person::{Directory, Person, WebOfTrust}; +use settings::{AppSettings, RoomConfig, SignerKind}; use state::{NostrRegistry, TIMEOUT, UniversalSigner}; use crate::{FileAttachment, KIND_FILE_MESSAGE, NewMessage}; @@ -183,6 +183,32 @@ impl Room { self } + /// Whether the auto-trust setting promotes this room out of the requests: + /// everyone other than the current user is inside the web of trust + pub fn auto_trusted(&self, me: PublicKey, cx: &App) -> bool { + if !AppSettings::get_wot_auto_trust(cx) { + return false; + } + + let wot = WebOfTrust::global(cx).read(cx); + + let mut others = false; + + for key in &self.members { + if *key == me { + continue; + } + + others = true; + + if !wot.is_in_network(key) { + return false; + } + } + + others + } + /// Sets this room is ongoing conversation pub(crate) fn set_ongoing(&mut self, cx: &mut Context) { self.kind = RoomKind::Ongoing; diff --git a/crates/settings/src/lib.rs b/crates/settings/src/lib.rs index 5b5b34aa..6c1f0af8 100644 --- a/crates/settings/src/lib.rs +++ b/crates/settings/src/lib.rs @@ -58,6 +58,7 @@ setting_accessors! { pub appearance_mode: AppearanceMode, pub hide_avatar: bool, pub screening: bool, + pub wot_auto_trust: bool, pub nip4e: bool, pub hide_group_notice: bool, pub file_server: Url, @@ -132,6 +133,10 @@ pub struct Settings { /// Enable screening for unknown chat requests pub screening: bool, + /// Promote chat requests from people followed by the user's contacts + #[serde(default)] + pub wot_auto_trust: bool, + /// Enable decoupling encryption key pub nip4e: bool, @@ -168,6 +173,7 @@ impl Default for Settings { appearance_mode: AppearanceMode::default(), hide_avatar: false, screening: true, + wot_auto_trust: false, nip4e: false, hide_group_notice: false, panel_background: true, diff --git a/crates/workspace/src/dialogs/settings.rs b/crates/workspace/src/dialogs/settings.rs index 7bea2318..57b8d9c0 100644 --- a/crates/workspace/src/dialogs/settings.rs +++ b/crates/workspace/src/dialogs/settings.rs @@ -83,12 +83,14 @@ impl Preferences { impl Render for Preferences { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { const SCREENING: &str = "Show an screening dialog to verify the unknown sender."; + const WOT: &str = "Treat people followed by your contacts as known senders."; const AVATAR: &str = "Hide all avatar pictures to improve performance."; const MODE: &str = "Follow the OS, or always use light or dark."; const NIP4E: &str = "Use a dedicated key to encrypt and decrypt messages."; const BACKGROUND: &str = "Show an image behind chat and group panels."; let screening = AppSettings::get_screening(cx); + let wot_auto_trust = AppSettings::get_wot_auto_trust(cx); let hide_avatar = AppSettings::get_hide_avatar(cx); let nip4e = AppSettings::get_nip4e(cx); let appearance_mode = AppSettings::get_appearance_mode(cx); @@ -116,6 +118,15 @@ impl Render for Preferences { AppSettings::update_screening(!screening, cx); }), ) + .child( + Switch::new("wot-auto-trust") + .label("Web of Trust") + .description(WOT) + .checked(wot_auto_trust) + .on_click(move |_, _window, cx| { + AppSettings::update_wot_auto_trust(!wot_auto_trust, cx); + }), + ) .child( Switch::new("avatar") .label("Hide user avatar") diff --git a/docs/web-of-trust.md b/docs/web-of-trust.md index 827b10c2..4cedff0b 100644 --- a/docs/web-of-trust.md +++ b/docs/web-of-trust.md @@ -156,26 +156,23 @@ Where request rooms are rendered, show the mutual count for the sender: ### Phase 3 — opt-in auto-trust (behind a setting) +Status: **implemented** (Phase 2 skipped for now; it will follow a different +flow). + The payoff: strangers vouched for by the user's contacts can skip screening. -- Add `pub wot_auto_trust: bool` to `setting_accessors!` in - `crates/settings/src/lib.rs` (default `false`) and a toggle in the existing - preferences screen (`crates/workspace/src/dialogs/settings.rs`). -- When enabled, apply it in one place — `ChatRegistry::new_message` in - `crates/chat/src/lib.rs`, next to the existing promotion rule — so in-app and - any future consumer can't drift (Amethyst's lesson from `TrustVerdicts`): - - ```rust - if this.kind == RoomKind::Request - && (message.rumor.pubkey == public_key - || (AppSettings::get_wot_auto_trust(cx) && wot.is_in_network(&message.rumor.pubkey))) - { - this.set_ongoing(cx); - } - ``` - - Group requests stay conservative: every member must be in the network (or - already a contact) to promote, mirroring how `is_contact` works today. +- `pub wot_auto_trust: bool` in `setting_accessors!` in + `crates/settings/src/lib.rs` (default `false`) and a "Web of trust" toggle in + the preferences screen (`crates/workspace/src/dialogs/settings.rs`). +- The rule lives in one predicate — `Room::auto_trusted(me, cx)` in + `crates/chat/src/room.rs`: the setting is on and every member other than the + current user is inside the web of trust (`is_in_network`, so followed + senders always pass and strangers need at least one mutual contact). + It's applied at every point a room's kind is decided so the states can't + drift: `ChatRegistry::add_room` (incoming first message), + `ChatRegistry::new_message` (live promotion next to the existing + I-sent-it rule), and `ChatRegistry::extend_rooms` (rooms loaded from the + database, so the split survives a restart). Deliberately not planned: sorting contacts (the new-chat dialog lists only follows already), notification feeds (coop has none), muting/spam scoring.