feat: add support for basic web of trust #65

Merged
reya merged 9 commits from feat/wot into master 2026-10-20 13:43:39 +00:00
5 changed files with 81 additions and 22 deletions
Showing only changes of commit 10d74c5396 - Show all commits
+21 -2
View File
@@ -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<Room>, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let Some(me) = nostr.read(cx).current_user() else {
return;
};
let mut room_map: HashMap<u64, usize> = 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);
+28 -2
View File
@@ -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>) {
self.kind = RoomKind::Ongoing;
+6
View File
@@ -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,
+11
View File
@@ -83,12 +83,14 @@ impl Preferences {
impl Render for Preferences {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> 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")
+15 -18
View File
@@ -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.