iris-chat 0.1.50

Iris Chat command line client and shared encrypted chat core
Documentation
//! Opt-in interoperability driver using the same FFI actions and updates as
//! native shells. Explicit test identities only; echoes media after accepting a call.
use anyhow::{bail, Context, Result};
use iris_chat_core::{AppAction, AppReconciler, AppUpdate, CallAudioCodec, FfiApp};
use serde_json::json;
use std::io::{self, BufRead, Write};
use std::time::{Duration, Instant};

mod iris_call_fixture_account;
mod iris_call_fixture_mode;

struct Updates(flume::Sender<AppUpdate>);
impl AppReconciler for Updates {
    fn reconcile(&self, update: AppUpdate) {
        let _ = self.0.try_send(update);
    }
}
fn emit(value: serde_json::Value) -> Result<()> {
    println!("{value}");
    io::stdout().flush().context("flush fixture output")
}
fn main() -> Result<()> {
    let mut args = std::env::args().skip(1);
    let dir = args
        .next()
        .context("usage: iris-call-fixture <fresh-data-dir>")?;
    let isolated_control = std::env::var("IRIS_CALL_ISOLATED_CONTROL").as_deref() == Ok("1");
    let mode = iris_call_fixture_mode::Mode::parse(&args.collect::<Vec<_>>(), isolated_control)?;
    if mode.resume_device.is_none()
        && std::path::Path::new(&dir)
            .read_dir()
            .is_ok_and(|mut entries| entries.next().is_some())
    {
        bail!("Call fixture requires a fresh data directory");
    }
    if mode.normal_persistent {
        iris_call_fixture_mode::private_directory(std::path::Path::new(&dir))?;
    }
    let receipt = std::path::Path::new(&dir).join("fixture-account-bundle.json");
    let restore = mode
        .resume_device
        .as_deref()
        .map(|peer| match mode.resume_owner.as_deref() {
            Some(owner) => iris_call_fixture_account::restore_exact(&receipt, owner, peer),
            None => iris_call_fixture_account::restore(&receipt, peer),
        })
        .transpose()?;
    let app = FfiApp::new(dir, String::new(), String::new());
    let (tx, updates) = flume::bounded(96);
    app.listen_for_updates(Box::new(Updates(tx)));
    if isolated_control {
        app.dispatch(AppAction::SetNearbyEnabled { enabled: false });
        app.dispatch(AppAction::SetNearbyLanEnabled { enabled: false });
        app.dispatch(AppAction::SetNearbyBluetoothEnabled { enabled: false });
    }
    app.dispatch(restore.unwrap_or_else(|| {
        AppAction::CreateAccount {
            name: if mode.normal_persistent {
                "Saved receiver test"
            } else {
                "Call test"
            }
            .into(),
        }
    }));
    if isolated_control {
        // Persisted settings are explicitly reconciled with this live fixture.
        app.dispatch(AppAction::SetNostrRelays {
            relay_urls: vec![std::env::var("IRIS_DEMO_RELAYS")?],
        });
    }
    if !isolated_control && !mode.normal_persistent {
        app.dispatch(AppAction::SetNearbyLanEnabled { enabled: true });
    }
    if let Ok(url) = std::env::var("IRIS_CALL_PUSH_SERVER_URL") {
        app.dispatch(AppAction::SetMobilePushServerUrl { url });
    }
    app.dispatch(AppAction::CreatePublicInvite);
    let deadline = Instant::now() + Duration::from_secs(30);
    let mut identity_saved = !mode.persist_identity || mode.resume_device.is_some();
    loop {
        if !identity_saved {
            for update in updates.try_iter() {
                if let AppUpdate::PersistAccountBundle {
                    owner_nsec,
                    owner_pubkey_hex,
                    device_nsec,
                    ..
                } = update
                {
                    iris_call_fixture_account::save(
                        &receipt,
                        owner_nsec.as_deref(),
                        &owner_pubkey_hex,
                        &device_nsec,
                    )?;
                    identity_saved = true;
                    break;
                }
            }
        }
        let state = app.state();
        if let (Some(account), Some(invite), true) =
            (state.account, state.public_invite, identity_saved)
        {
            if mode.normal_persistent {
                anyhow::ensure!(
                    state.preferences.nostr_relay_urls == vec![std::env::var("IRIS_DEMO_RELAYS")?],
                    "Saved fixture message server differs from the preserved setting"
                );
            }
            emit(
                json!({"event":"ready","owner":account.public_key_hex,"device":account.device_public_key_hex,"device_npub":account.device_npub,"invite":invite.url}),
            )?;
            break;
        }
        if Instant::now() > deadline {
            bail!("Account and invite did not become ready: {:?}", state.toast);
        }
        std::thread::sleep(Duration::from_millis(20));
    }
    let (commands_tx, commands) = flume::bounded(16);
    std::thread::spawn(move || {
        for line in io::stdin().lock().lines() {
            let Ok(line) = line else {
                break;
            };
            if commands_tx.send(line).is_err() {
                break;
            }
        }
    });
    let mut auto_answer = true;
    let mut answer_voice = false;
    let mut last_call = String::new();
    let mut last_key_request = (String::new(), 0u32);
    let mut audio = 0u64;
    let mut video = 0u64;
    let mut lan_owners = Vec::<String>::new();
    let mut audio_nonzero = 0u64;
    let audio_codec = CallAudioCodec::new()?;
    let mut audio_playout_at = Instant::now();
    #[cfg(feature = "desktop-media")]
    let desktop_video = if std::env::var_os("IRIS_CALL_DESKTOP_CODEC").is_some() {
        let (send, recv) = flume::bounded::<(String, u32, bool, u64, Vec<u8>, u32, u32)>(8);
        let (out_send, out_recv) = flume::bounded(16);
        let mut codec =
            iris_chat_core::DesktopVideoTranscoder::new().map_err(anyhow::Error::msg)?;
        std::thread::spawn(move || {
            let mut active = String::new();
            while let Ok((id, seq, key, timestamp, data, target, generation)) = recv.recv() {
                if active != id {
                    let Ok(fresh) = iris_chat_core::DesktopVideoTranscoder::new() else {
                        break;
                    };
                    codec = fresh;
                    active = id.clone();
                }
                for event in codec.receive(seq, key, data, target, generation) {
                    if out_send.send((id.clone(), timestamp, event)).is_err() {
                        return;
                    }
                }
            }
        });
        Some((send, out_recv))
    } else {
        None
    };
    #[cfg(not(feature = "desktop-media"))]
    if std::env::var_os("IRIS_CALL_DESKTOP_CODEC").is_some() {
        bail!("Build the call fixture with desktop-media for desktop codec interoperability");
    }

    loop {
        match commands.recv_timeout(Duration::from_millis(10)) {
            Ok(line) => {
                let parts = line.split_whitespace().collect::<Vec<_>>();
                match parts.as_slice() {
                    ["accept", owner] => {
                        app.dispatch(AppAction::CreateChat {
                            peer_input: (*owner).into(),
                        });
                        app.dispatch(AppAction::SetMessageRequestAccepted {
                            chat_id: (*owner).into(),
                        });
                        emit(json!({"event":"accepted"}))?;
                    }
                    ["call", owner, kind] => app.dispatch(AppAction::StartCall {
                        chat_id: (*owner).into(),
                        video: *kind == "video",
                    }),
                    ["message", owner, words @ ..] if !words.is_empty() => {
                        app.dispatch(AppAction::SendMessage {
                            chat_id: (*owner).into(),
                            text: words.join(" "),
                        });
                        emit(json!({"event":"message-sent"}))?;
                    }
                    ["answer", kind] => {
                        auto_answer = true;
                        answer_voice = *kind == "voice";
                    }
                    ["manual"] => auto_answer = false,
                    ["peer", owner] => {
                        let ready = app.peer_profile_debug((*owner).into()).is_some_and(|peer| {
                            peer.roster_device_count > 0 && peer.active_session_count > 0
                        });
                        emit(json!({"event":"peer","ready":ready}))?;
                    }
                    ["status"] => {
                        let state = app.state();
                        let call=state.call.map(|s|json!({"id":s.call_id,"phase":s.phase,"video":s.video_capable,"muted":s.remote_muted}));
                        emit(
                            json!({"event":"status","call":call,"audio_frames":audio,"video_frames":video,"nonzero_audio_frames":audio_nonzero,
                                "lan_owners":lan_owners}),
                        )?;
                    }
                    ["health"] if isolated_control => {
                        let raw: serde_json::Value =
                            serde_json::from_str(&app.export_support_bundle_json())?;
                        if raw["ffi_queue"]["core_support_bundle_timed_out"].as_bool()
                            != Some(false)
                        {
                            bail!("Control support query unavailable");
                        }
                        let expected = std::env::var("IRIS_DEMO_RELAYS")?;
                        let transport = &raw["relay_transport"];
                        emit(json!({"event":"health",
                            "expected_relay_matches":raw["relay_urls"] == json!([expected]),
                            "connected_relay_count":transport["connected_relay_count"].as_u64().context("Missing relay count")?,
                            "pending_relay_publish_count":transport["pending_relay_publish_count"].as_u64().context("Missing outbox count")?,
                            "fips_transport":raw.get("fips_transport").context("Missing FIPS diagnostics")?,
                        }))?;
                    }
                    ["end"] => {
                        if let Some(c) = app.state().call {
                            app.dispatch(AppAction::EndCall { call_id: c.call_id });
                        }
                    }
                    ["stop"] => break,
                    _ => emit(json!({"event":"error","reason":"Unknown fixture command"}))?,
                }
            }
            Err(flume::RecvTimeoutError::Disconnected) => break,
            Err(flume::RecvTimeoutError::Timeout) => {}
        }
        for update in updates.try_iter().take(128) {
            if let AppUpdate::NearbyPeersChanged {
                snapshot,
                lan_peer_ids,
                ..
            } = &update
            {
                lan_owners = snapshot
                    .peers
                    .iter()
                    .filter(|peer| lan_peer_ids.contains(&peer.id))
                    .filter_map(|peer| peer.owner_pubkey_hex.clone())
                    .collect();
            }
            if let AppUpdate::CallMedia {
                call_id,
                kind,
                sequence,
                timestamp_us,
                key_frame,
                data,
            } = update
            {
                if kind == 1 {
                    audio += 1;
                    audio_codec.queue(sequence, data.clone());
                } else if kind == 2 {
                    video += 1;
                }
                #[cfg(feature = "desktop-media")]
                if kind == 2 {
                    if let (Some((send, _)), Some(call)) = (&desktop_video, app.state().call) {
                        if send
                            .try_send((
                                call_id.clone(),
                                sequence,
                                key_frame,
                                timestamp_us,
                                data,
                                call.target_bitrate_bps,
                                call.key_frame_generation,
                            ))
                            .is_err()
                        {
                            app.dispatch(AppAction::RequestCallKeyFrame { call_id });
                        }
                        continue;
                    }
                }
                app.dispatch(AppAction::SendCallMedia {
                    call_id,
                    kind,
                    timestamp_us,
                    key_frame,
                    data,
                });
            }
        }
        #[cfg(feature = "desktop-media")]
        if let Some((_, recv)) = &desktop_video {
            for (call_id, timestamp_us, event) in recv.try_iter().take(16) {
                match event {
                    iris_chat_core::DesktopCallEvent::Encoded {
                        kind,
                        key_frame,
                        data,
                        ..
                    } => app.dispatch(AppAction::SendCallMedia {
                        call_id,
                        kind,
                        timestamp_us,
                        key_frame,
                        data,
                    }),
                    iris_chat_core::DesktopCallEvent::RequestKeyFrame => {
                        app.dispatch(AppAction::RequestCallKeyFrame { call_id })
                    }
                    _ => {}
                }
            }
        }
        if audio_playout_at.elapsed() >= Duration::from_millis(20) {
            audio_playout_at = Instant::now();
            if audio_codec.playout().iter().any(|v| v.unsigned_abs() > 10) {
                audio_nonzero += 1;
            }
        }
        if let Some(call) = app.state().call {
            if call.key_frame_generation > 0
                && last_key_request != (call.call_id.clone(), call.key_frame_generation)
            {
                app.dispatch(AppAction::RequestCallKeyFrame {
                    call_id: call.call_id.clone(),
                });
                last_key_request = (call.call_id.clone(), call.key_frame_generation);
            }
            let state = format!("{}:{}", call.call_id, call.phase);
            if last_call != state {
                emit(
                    json!({"event":"call","id":call.call_id,"phase":call.phase,"video":call.video_capable}),
                )?;
                last_call = state;
                if call.phase == "connected" {
                    app.dispatch(AppAction::SetCallMediaConnected {
                        call_id: call.call_id.clone(),
                        connected: true,
                    });
                }
                if auto_answer && call.phase == "incoming" {
                    app.dispatch(if answer_voice {
                        AppAction::AnswerCallWithVoice {
                            call_id: call.call_id,
                        }
                    } else {
                        AppAction::AnswerCall {
                            call_id: call.call_id,
                        }
                    });
                }
            }
        }
    }
    app.shutdown();
    emit(json!({"event":"stopped"}))
}