tauri-plugin-auditaur 0.3.2

Development-first Tauri plugin for collecting Auditaur local telemetry.
use auditaur_collector::receiver::OTelBatch;
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine};
use serde::Serialize;
use std::io::Cursor;
use tauri::{AppHandle, Manager, Runtime, State, WebviewWindow};
use xcap::{
    image::{DynamicImage, ImageFormat, RgbaImage},
    Monitor as CaptureMonitor, Window as CaptureWindow,
};

use crate::{error::AuditaurError, state::AuditaurState};

pub const EXPORT_OTEL_BATCH_COMMAND: &str = "export_otel_batch";
pub const REGISTER_DRIVE_BRIDGE_COMMAND: &str = "register_drive_bridge";
pub const POLL_DRIVE_BRIDGE_REQUEST_COMMAND: &str = "poll_drive_bridge_request";
pub const COMPLETE_DRIVE_BRIDGE_REQUEST_COMMAND: &str = "complete_drive_bridge_request";
pub const CAPTURE_DRIVE_BRIDGE_SCREENSHOT_COMMAND: &str = "capture_drive_bridge_screenshot";

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DriveBridgeScreenshot {
    format: &'static str,
    png_base64: String,
    width: u32,
    height: u32,
    screenshot_backend: &'static str,
    window_label: String,
    window_title: Option<String>,
    native_window_id: Option<u32>,
    native_window_title: Option<String>,
    native_app_name: Option<String>,
}

#[tauri::command]
pub async fn export_otel_batch(
    state: State<'_, AuditaurState>,
    batch: OTelBatch,
) -> Result<(), AuditaurError> {
    state.export_batch(batch)
}

#[tauri::command]
pub async fn register_drive_bridge<R: Runtime>(
    app: AppHandle<R>,
    state: State<'_, AuditaurState>,
    window_label: Option<String>,
) -> Result<(), AuditaurError> {
    state.register_drive_bridge(window_label)?;
    if let Some((bridge_dir, alive)) = state.start_bridge_notifier_if_needed() {
        crate::start_drive_bridge_request_notifier(&app, bridge_dir, alive);
    }
    Ok(())
}

#[tauri::command]
pub async fn poll_drive_bridge_request(
    state: State<'_, AuditaurState>,
    window_label: Option<String>,
) -> Result<Option<auditaur_core::drive_bridge::DriveBridgeRequest>, AuditaurError> {
    state.poll_drive_bridge_request(window_label)
}

#[tauri::command]
pub async fn complete_drive_bridge_request(
    state: State<'_, AuditaurState>,
    response: auditaur_core::drive_bridge::DriveBridgeResponse,
) -> Result<(), AuditaurError> {
    state.complete_drive_bridge_request(response)
}

#[tauri::command]
pub async fn capture_drive_bridge_screenshot<R: Runtime>(
    app: AppHandle<R>,
    state: State<'_, AuditaurState>,
    window_label: Option<String>,
) -> Result<DriveBridgeScreenshot, AuditaurError> {
    state.ensure_drive_bridge_available()?;
    tauri::async_runtime::spawn_blocking(move || capture_window_screenshot(&app, window_label))
        .await
        .map_err(|error| AuditaurError::new(format!("native screenshot task failed: {error}")))?
}

fn capture_window_screenshot<R: Runtime>(
    app: &AppHandle<R>,
    window_label: Option<String>,
) -> Result<DriveBridgeScreenshot, AuditaurError> {
    let window = resolve_tauri_window(app, window_label.as_deref())?;
    let window_label = window.label().to_string();
    let window_titles = candidate_window_titles(app, &window, &window_label);
    let window_title = window_titles.first().cloned();
    let capture = match select_capture_window(&window_titles) {
        Ok(capture_window) => {
            let native_window_id = capture_window.id().ok();
            let native_window_title = capture_window.title().ok();
            let native_app_name = capture_window.app_name().ok();
            let image = capture_window.capture_image().map_err(|error| {
                AuditaurError::new(format!("native screenshot capture failed: {error}"))
            })?;
            NativeCapture {
                image,
                native_window_id,
                native_window_title,
                native_app_name,
            }
        }
        Err(window_error) => {
            let image = capture_tauri_window_region(&window).map_err(|region_error| {
                AuditaurError::new(format!(
                    "{window_error}; monitor-region fallback failed: {region_error}"
                ))
            })?;
            NativeCapture {
                image,
                native_window_id: None,
                native_window_title: window_title.clone(),
                native_app_name: configured_product_name(app),
            }
        }
    };
    let image = capture.image;
    let width = image.width();
    let height = image.height();
    let mut bytes = Cursor::new(Vec::new());
    DynamicImage::ImageRgba8(image)
        .write_to(&mut bytes, ImageFormat::Png)
        .map_err(|error| {
            AuditaurError::new(format!("native screenshot PNG encoding failed: {error}"))
        })?;

    Ok(DriveBridgeScreenshot {
        format: "png",
        png_base64: BASE64_STANDARD.encode(bytes.into_inner()),
        width,
        height,
        screenshot_backend: "tauri_native_window_xcap",
        window_label,
        window_title,
        native_window_id: capture.native_window_id,
        native_window_title: capture.native_window_title,
        native_app_name: capture.native_app_name,
    })
}

struct NativeCapture {
    image: RgbaImage,
    native_window_id: Option<u32>,
    native_window_title: Option<String>,
    native_app_name: Option<String>,
}

fn capture_tauri_window_region<R: Runtime>(
    window: &WebviewWindow<R>,
) -> Result<RgbaImage, AuditaurError> {
    let position = window.outer_position().map_err(|error| {
        AuditaurError::new(format!("could not read Tauri window position: {error}"))
    })?;
    let size = window.outer_size().map_err(|error| {
        AuditaurError::new(format!("could not read Tauri window size: {error}"))
    })?;
    let center_x = position.x.saturating_add((size.width / 2) as i32);
    let center_y = position.y.saturating_add((size.height / 2) as i32);
    let monitor = CaptureMonitor::from_point(center_x, center_y).map_err(|error| {
        AuditaurError::new(format!("could not resolve window monitor: {error}"))
    })?;
    let monitor_x = monitor
        .x()
        .map_err(|error| AuditaurError::new(format!("could not read monitor x: {error}")))?;
    let monitor_y = monitor
        .y()
        .map_err(|error| AuditaurError::new(format!("could not read monitor y: {error}")))?;
    let monitor_width = monitor
        .width()
        .map_err(|error| AuditaurError::new(format!("could not read monitor width: {error}")))?;
    let monitor_height = monitor
        .height()
        .map_err(|error| AuditaurError::new(format!("could not read monitor height: {error}")))?;
    let x = position.x.saturating_sub(monitor_x).max(0) as u32;
    let y = position.y.saturating_sub(monitor_y).max(0) as u32;
    let width = size.width.min(monitor_width.saturating_sub(x));
    let height = size.height.min(monitor_height.saturating_sub(y));
    if width == 0 || height == 0 {
        return Err(AuditaurError::new(format!(
            "window bounds ({}, {}, {}x{}) do not intersect monitor bounds ({}, {}, {}x{})",
            position.x,
            position.y,
            size.width,
            size.height,
            monitor_x,
            monitor_y,
            monitor_width,
            monitor_height
        )));
    }
    monitor
        .capture_region(x, y, width, height)
        .map_err(|error| {
            AuditaurError::new(format!("native monitor region capture failed: {error}"))
        })
}

fn candidate_window_titles<R: Runtime>(
    app: &AppHandle<R>,
    window: &WebviewWindow<R>,
    window_label: &str,
) -> Vec<String> {
    let mut titles = Vec::new();
    push_title(&mut titles, window.title().ok());
    push_title(&mut titles, configured_window_title(app, window_label));
    push_title(&mut titles, configured_product_name(app));
    titles
}

fn push_title(titles: &mut Vec<String>, title: Option<String>) {
    let Some(title) = title else {
        return;
    };
    let title = title.trim();
    if title.is_empty() || titles.iter().any(|existing| existing == title) {
        return;
    }
    titles.push(title.to_string());
}

fn configured_window_title<R: Runtime>(app: &AppHandle<R>, window_label: &str) -> Option<String> {
    app.config()
        .app
        .windows
        .iter()
        .find(|window| window.label == window_label)
        .map(|window| window.title.clone())
        .filter(|title| !title.trim().is_empty())
}

fn configured_product_name<R: Runtime>(app: &AppHandle<R>) -> Option<String> {
    app.config()
        .product_name
        .clone()
        .filter(|title| !title.trim().is_empty())
}

fn resolve_tauri_window<R: Runtime>(
    app: &AppHandle<R>,
    window_label: Option<&str>,
) -> Result<WebviewWindow<R>, AuditaurError> {
    if let Some(window_label) = window_label {
        return app.get_webview_window(window_label).ok_or_else(|| {
            AuditaurError::new(format!("no Tauri WebView window matched `{window_label}`"))
        });
    }

    let mut windows: Vec<_> = app.webview_windows().into_values().collect();
    match windows.len() {
        0 => Err(AuditaurError::new(
            "native screenshot capture failed: no Tauri WebView windows are available",
        )),
        1 => Ok(windows.remove(0)),
        _ => Err(AuditaurError::new(
            "native screenshot capture requires a windowLabel when multiple Tauri WebView windows exist",
        )),
    }
}

fn select_capture_window(titles: &[String]) -> Result<CaptureWindow, AuditaurError> {
    let pid = std::process::id();
    let windows = CaptureWindow::all().map_err(|error| {
        AuditaurError::new(format!("native window enumeration failed: {error}"))
    })?;
    let mut same_process: Vec<_> = windows
        .iter()
        .filter(|window| window.pid().ok() == Some(pid))
        .cloned()
        .collect();

    for title in titles {
        let mut titled: Vec<_> = same_process
            .iter()
            .filter(|window| window.title().ok().as_deref() == Some(title))
            .cloned()
            .collect();
        if let Some(focused) = take_focused(&mut titled) {
            return Ok(focused);
        }
        if let Some(window) = titled.into_iter().next() {
            return Ok(window);
        }
    }

    if let Some(focused) = take_focused(&mut same_process) {
        return Ok(focused);
    }
    match same_process.len() {
        0 => Err(AuditaurError::new(
            "native screenshot capture failed: no native window matched the current app process",
        )),
        1 => Ok(same_process.remove(0)),
        count => Err(AuditaurError::new(format!(
            "native screenshot capture found {count} native windows for this process; set driveBridge.windowLabel and a unique Tauri window title"
        ))),
    }
}

fn take_focused(windows: &mut Vec<CaptureWindow>) -> Option<CaptureWindow> {
    let index = windows
        .iter()
        .position(|window| window.is_focused().ok() == Some(true))?;
    Some(windows.remove(index))
}