iris-chat 0.1.66

Iris Chat command line client and shared encrypted chat core
Documentation
use super::*;

pub(in crate::core) fn accept_notification_peer_in_store(conn: &rusqlite::Connection, peer: &str) {
    conn.execute_batch("INSERT OR IGNORE INTO preferences(id, send_typing_indicators, send_read_receipts, desktop_notifications_enabled, startup_at_login_enabled, nostr_relay_urls_json, image_proxy_enabled, image_proxy_url, image_proxy_key_hex, image_proxy_salt_hex, mobile_push_server_url)
        VALUES (1,1,1,1,0,'[]',1,'','','','');").unwrap();
    conn.execute("UPDATE preferences SET accepted_owner_pubkeys_json=json_insert(accepted_owner_pubkeys_json, '$[#]', ?1) WHERE id=1", [peer]).unwrap();
}

pub(super) fn request_network_peer(core: &mut AppCore, owner: &Keys, peer: PublicKey) {
    let friend = Keys::generate();
    let mut graph = nostr_social_graph::SocialGraph::new(&owner.public_key().to_hex());
    request_graph_event(&mut graph, owner, &[friend.public_key()], 3, 1);
    request_graph_event(&mut graph, &friend, &[peer], 3, 1);
    core.user_discovery.owner_pubkey_hex = Some(owner.public_key().to_hex());
    core.user_discovery.social_graph = Some(graph.to_binary().unwrap());
    core.app_store
        .replace_user_discovery(&core.user_discovery)
        .unwrap();
    core.refresh_social_graph();
    core.persist_best_effort_inner();
}

fn request_graph_event(
    graph: &mut nostr_social_graph::SocialGraph,
    author: &Keys,
    peers: &[PublicKey],
    kind: u32,
    at: u64,
) {
    graph.handle_event(
        &nostr_social_graph::NostrEvent {
            pubkey: author.public_key().to_hex(),
            kind,
            created_at: at,
            tags: peers
                .iter()
                .map(|peer| vec!["p".into(), peer.to_hex()])
                .collect(),
            content: String::new(),
            id: format!("{at:064x}"),
            sig: String::new(),
        },
        true,
        1.0,
    );
}

fn deliver_request(core: &mut AppCore, sender: &Keys, id: &str, body: &str) {
    core.apply_runtime_text_message(
        sender.public_key(),
        None,
        body.into(),
        unix_now().get(),
        None,
        Some(id.into()),
        None,
    );
}

#[test]
fn request_policy_full_graph_following_overmuted_and_switches() {
    use message_requests::RequestAccess::*;
    let owner = Keys::generate();
    let mut core = logged_in_test_core("request-policy-tiers", &owner, &Keys::generate());
    let peers: Vec<_> = (0..6).map(|_| Keys::generate()).collect();
    let mut graph = nostr_social_graph::SocialGraph::new(&owner.public_key().to_hex());
    request_graph_event(&mut graph, &owner, &[peers[0].public_key()], 3, 1);
    for n in 0..4 {
        request_graph_event(&mut graph, &peers[n], &[peers[n + 1].public_key()], 3, 1);
    }
    core.social_graph = Some(graph);
    core.preferences.accept_unknown_direct_messages = false;
    core.preferences.accept_social_graph_direct_messages = false;
    let followed = peers[0].public_key().to_hex();
    let distant = peers[4].public_key().to_hex();
    let stranger = peers[5].public_key().to_hex();
    assert_eq!(core.direct_request_access(&followed), Accepted);
    assert_eq!(core.direct_request_access(&distant), Denied);
    assert_eq!(core.direct_request_access(&stranger), Denied);
    core.preferences.accept_social_graph_direct_messages = true;
    assert_eq!(
        core.social_graph
            .as_ref()
            .unwrap()
            .get_follow_distance(&distant),
        5
    );
    assert_eq!(core.direct_request_access(&distant), Network);
    core.preferences.accept_unknown_direct_messages = true;
    assert_eq!(core.direct_request_access(&stranger), Unconnected);
    request_graph_event(
        core.social_graph.as_mut().unwrap(),
        &peers[0],
        &[peers[4].public_key()],
        10_000,
        2,
    );
    request_graph_event(
        core.social_graph.as_mut().unwrap(),
        &peers[1],
        &[peers[4].public_key()],
        10_000,
        2,
    );
    assert!(core
        .social_graph
        .as_ref()
        .unwrap()
        .is_overmuted(&distant, 1.0));
    assert_eq!(core.direct_request_access(&distant), Denied);
    core.preferences.accept_overmuted_direct_messages = true;
    assert_eq!(core.direct_request_access(&distant), Network);
    deliver_request(&mut core, &peers[0], "followed-first", "hello");
    assert!(!core.thread_is_message_request(&followed));
    core.social_graph = None;
    core.preferences.accept_unknown_direct_messages = false;
    assert_eq!(
        core.direct_request_access(&followed),
        Accepted,
        "unfollowing preserves accepted chats"
    );
    core.set_user_blocked(&followed, true);
    assert_eq!(core.direct_request_access(&followed), Denied);
}

#[test]
fn request_policy_pending_threads_do_not_bypass_disabled_requests() {
    let owner = Keys::generate();
    let peer = Keys::generate();
    let mut core = logged_in_test_core("request-policy-pending", &owner, &Keys::generate());
    deliver_request(&mut core, &peer, "first", "hello");
    let chat = peer.public_key().to_hex();
    assert!(core.thread_is_message_request(&chat));
    core.preferences.accept_unknown_direct_messages = false;
    deliver_request(&mut core, &peer, "second", "should be ignored");
    assert_eq!(core.threads[&chat].messages.len(), 1);
    core.accept_message_request(&chat);
    deliver_request(&mut core, &peer, "third", "accepted");
    assert_eq!(core.threads[&chat].messages.len(), 2);
}

#[test]
fn request_policy_newest_fifty_survive_restart_without_bumping_or_evicting_accepted() {
    let owner = Keys::generate();
    let device = Keys::generate();
    let (mut core, _, dir) =
        logged_in_test_core_with_updates("request-policy-fifo", &owner, &device);
    let peers: Vec<_> = (0..52).map(|_| Keys::generate()).collect();
    for (n, peer) in peers.iter().take(50).enumerate() {
        deliver_request(&mut core, peer, &format!("first-{n}"), "hello");
    }
    let oldest = peers[0].public_key().to_hex();
    let accepted = peers[1].public_key().to_hex();
    core.accept_message_request(&accepted);
    deliver_request(&mut core, &peers[50], "fifty-first", "hello");
    deliver_request(
        &mut core,
        &peers[0],
        "oldest-again",
        "more messages do not bump me",
    );
    let order = core.app_store.request_arrivals().unwrap();
    core.persist_best_effort_inner();
    drop(core);
    let mut restored =
        restart_block_history_core(&owner, &device, dir.path().to_string_lossy().into_owned());
    restored.restore_device_sync_record_projection().unwrap();
    assert_eq!(restored.app_store.request_arrivals().unwrap(), order);
    deliver_request(&mut restored, &peers[51], "fifty-second", "hello");
    assert!(!restored.threads.contains_key(&oldest));
    assert!(restored.threads.contains_key(&accepted));
    assert!(restored
        .threads
        .contains_key(&peers[51].public_key().to_hex()));
    let pending = restored
        .threads
        .keys()
        .filter(|peer| {
            restored.direct_request_access(peer) == message_requests::RequestAccess::Unconnected
        })
        .count();
    assert_eq!(pending, 50);
    let cutoff = restored.chat_deletions[&oldest];
    restored.apply_runtime_text_message(
        peers[0].public_key(),
        None,
        "replay".into(),
        cutoff,
        None,
        Some("replayed-old".into()),
        None,
    );
    assert!(!restored.threads.contains_key(&oldest));
}

#[test]
fn request_policy_bounds_messages_and_bytes_per_pending_sender() {
    let owner = Keys::generate();
    let peer = Keys::generate();
    let large = Keys::generate();
    let mut core = logged_in_test_core("request-policy-bounds", &owner, &Keys::generate());
    for n in 0..30 {
        deliver_request(&mut core, &peer, &format!("message-{n}"), "hello");
    }
    assert_eq!(core.threads[&peer.public_key().to_hex()].messages.len(), 20);
    for n in 0..5 {
        deliver_request(
            &mut core,
            &large,
            &format!("large-{n}"),
            &"x".repeat(30 * 1024),
        );
    }
    assert_eq!(core.threads[&large.public_key().to_hex()].messages.len(), 2);
}

#[test]
fn request_policy_preferences_persist_and_push_stays_quiet_until_accepted() {
    let owner = Keys::generate();
    let device = Keys::generate();
    let peer = Keys::generate();
    let chat = peer.public_key().to_hex();
    let (mut core, _, dir) =
        logged_in_test_core_with_updates("request-policy-prefs", &owner, &device);
    core.handle_action(AppAction::SetAcceptSocialGraphDirectMessages { enabled: false });
    core.handle_action(AppAction::SetAcceptOvermutedDirectMessages { enabled: true });
    deliver_request(&mut core, &peer, "hello", "quiet request");
    core.rebuild_persist_and_emit_state();
    let notifications = crate::core::notifications::decide_notifications(
        &[],
        &core.state.chat_list,
        &core.preferences,
        false,
        None,
    );
    assert!(notifications.is_empty());
    let conn = core.app_store.shared();
    assert!(!crate::core::mobile_push::request_policy::allows(
        &conn.lock().unwrap(),
        &owner.public_key().to_hex(),
        &chat
    ));
    core.accept_message_request(&chat);
    assert!(crate::core::mobile_push::request_policy::allows(
        &conn.lock().unwrap(),
        &owner.public_key().to_hex(),
        &chat
    ));
    core.persist_best_effort_inner();
    drop(core);
    let restored =
        restart_block_history_core(&owner, &device, dir.path().to_string_lossy().into_owned());
    assert!(!restored.preferences.accept_social_graph_direct_messages);
    assert!(restored.preferences.accept_overmuted_direct_messages);
}

#[test]
fn request_policy_encrypted_first_contact_and_subscription_wire_stay_private() {
    let owner = Keys::generate();
    let device = Keys::generate();
    let sender = Keys::generate();
    for allowed in [false, true] {
        let mut core = logged_in_test_core("request-policy-wire", &owner, &device);
        core.preferences.accept_unknown_direct_messages = allowed;
        let (event, _) = appcore_direct_message_event_with_author_keys_for_test(
            core.protocol_engine.as_mut().unwrap(),
            &sender,
            "secret request text",
            unix_now().get(),
        );
        let wire = serde_json::to_string(&event).unwrap();
        for private in [
            owner.public_key().to_hex(),
            sender.public_key().to_hex(),
            "secret request text".into(),
        ] {
            assert!(!wire.contains(&private));
        }
        core.pending_relay_publishes.clear();
        core.handle_relay_event(event.clone());
        core.handle_relay_event(event);
        let chat = sender.public_key().to_hex();
        assert_eq!(
            core.threads
                .get(&chat)
                .map_or(0, |thread| thread.messages.len()),
            usize::from(allowed)
        );
        assert!(
            pending_events_with_kind(&core, MESSAGE_EVENT_KIND).is_empty(),
            "requests must not send receipts"
        );
        let authors = core.subscribable_message_author_hexes();
        assert_eq!(authors.is_empty(), !allowed);
        let push = core.build_mobile_push_sync_snapshot();
        let request = build_mobile_push_create_subscription_request(
            owner.secret_key().to_secret_hex(),
            "ios".into(),
            "test-token".into(),
            Some("to.iris.chat".into()),
            push.message_author_pubkeys,
            Vec::new(),
            vec![Keys::generate().public_key().to_hex()],
            false,
            None,
            Vec::new(),
        )
        .expect("serialized notification subscription")
        .body_json
        .unwrap();
        assert!(!request.contains(&owner.public_key().to_hex()));
        assert!(!request.contains(&sender.public_key().to_hex()));
    }
}