iris-chat-protocol 0.1.2

Reusable Iris Chat double-ratchet protocol engine
Documentation
use super::*;

include!("protocol_engine/types.rs");
include!("protocol_engine/engine_core.rs");
include!("protocol_engine/engine_sends.rs");
include!("protocol_engine/engine_incoming_retry.rs");
include!("protocol_engine/engine_resolution.rs");
include!("protocol_engine/engine_sender_key_repair.rs");
include!("protocol_engine/engine_queue_filters.rs");
include!("protocol_engine/free_functions.rs");

#[cfg(test)]
mod tests {
    use super::*;

    fn test_engine(owner: &Keys, device: &Keys) -> ProtocolEngine {
        let storage = Arc::new(InMemoryStorage::new()) as Arc<dyn StorageAdapter>;
        ProtocolEngine::load_or_create_for_local_device(storage, owner.public_key(), device)
            .expect("protocol engine")
    }

    fn pending_outbound(
        message_id: &str,
        chat_id: &str,
        recipient_owner_hex: String,
        send_remote: bool,
        local_sibling_payload: Option<Vec<u8>>,
        probe_local_sibling_roster: bool,
        reason: ProtocolPendingReason,
    ) -> ProtocolPendingOutbound {
        ProtocolPendingOutbound {
            message_id: message_id.to_string(),
            chat_id: chat_id.to_string(),
            recipient_owner_hex,
            send_remote,
            remote_payload: b"remote".to_vec(),
            local_sibling_payload,
            inner_event_id: Some(message_id.to_string()),
            delivered_remote_device_hexes: Vec::new(),
            delivered_local_device_hexes: Vec::new(),
            probe_local_sibling_roster,
            created_at_secs: 1,
            next_retry_at_secs: 1,
            reason,
        }
    }

    #[test]
    fn local_sibling_roster_probe_does_not_block_delivery() {
        let owner = Keys::generate();
        let device = Keys::generate();
        let mut engine = test_engine(&owner, &device);
        engine.pending_outbound.push(pending_outbound(
            "message-id",
            "chat-id",
            owner.public_key().to_hex(),
            false,
            Some(b"local".to_vec()),
            true,
            ProtocolPendingReason::PublishRetry,
        ));

        assert!(!engine.has_delivery_blocking_message_work("message-id"));
    }

    #[test]
    fn known_local_sibling_target_blocks_delivery() {
        let owner = Keys::generate();
        let device = Keys::generate();
        let sibling = Keys::generate();
        let mut engine = test_engine(&owner, &device);
        engine
            .session_manager
            .replace_local_roster(DeviceRoster::new(
                NdrUnixSeconds(1),
                vec![
                    AuthorizedDevice::new(ndr_device(device.public_key()), NdrUnixSeconds(1)),
                    AuthorizedDevice::new(ndr_device(sibling.public_key()), NdrUnixSeconds(1)),
                ],
            ));
        engine.pending_outbound.push(pending_outbound(
            "message-id",
            "chat-id",
            owner.public_key().to_hex(),
            false,
            Some(b"local".to_vec()),
            false,
            ProtocolPendingReason::PublishRetry,
        ));

        assert!(engine.has_delivery_blocking_message_work("message-id"));
    }

    #[test]
    fn missing_remote_roster_blocks_delivery() {
        let owner = Keys::generate();
        let device = Keys::generate();
        let peer = Keys::generate();
        let mut engine = test_engine(&owner, &device);
        engine.pending_outbound.push(pending_outbound(
            "message-id",
            &peer.public_key().to_hex(),
            peer.public_key().to_hex(),
            true,
            None,
            false,
            ProtocolPendingReason::MissingRoster,
        ));

        assert!(engine.has_delivery_blocking_message_work("message-id"));
    }

    #[test]
    fn protocol_discovery_effects_fetch_appkeys_and_invites_for_owner() {
        let owner = Keys::generate();
        let device = Keys::generate();
        let peer = Keys::generate();
        let engine = test_engine(&owner, &device);

        let effects = engine.protocol_discovery_effects_for_owners(
            [peer.public_key()],
            UnixSeconds(1_777_159_500),
            "test_discovery",
        );

        let filters = effects
            .into_iter()
            .flat_map(|effect| match effect {
                ProtocolEffect::FetchProtocolState { filters, .. } => filters,
                ProtocolEffect::Publish(_) => Vec::new(),
            })
            .collect::<Vec<_>>();

        assert_eq!(filters.len(), 2);
        assert!(has_filter_with_kind_author(
            &filters,
            APP_KEYS_EVENT_KIND,
            peer.public_key()
        ));
        assert!(has_filter_with_kind_author(
            &filters,
            INVITE_EVENT_KIND,
            peer.public_key()
        ));
    }

    fn has_filter_with_kind_author(filters: &[Filter], kind: u32, author: PublicKey) -> bool {
        let author_hex = author.to_hex();
        filters
            .iter()
            .map(|filter| serde_json::to_value(filter).expect("filter json"))
            .any(|filter| {
                let has_kind = filter
                    .get("kinds")
                    .and_then(|kinds| kinds.as_array())
                    .is_some_and(|kinds| {
                        kinds
                            .iter()
                            .any(|value| value.as_u64() == Some(kind as u64))
                    });
                let has_author = filter
                    .get("authors")
                    .and_then(|authors| authors.as_array())
                    .is_some_and(|authors| {
                        authors
                            .iter()
                            .any(|value| value.as_str() == Some(author_hex.as_str()))
                    });
                has_kind && has_author
            })
    }
}