nvpn 4.1.1

CLI and daemon for Nostr VPN private mesh networks
Documentation
use std::path::Path;

use anyhow::{Result, anyhow};
use nostr_vpn_core::config::{maybe_autoconfigure_node, normalize_nostr_pubkey};
use serde_json::json;

use super::{
    DaemonControlRequest, UpdateRosterArgs, clear_daemon_control_result, daemon_status,
    default_config_path, load_or_default_config, request_daemon_reload,
    wait_for_daemon_control_ack, wait_for_daemon_control_result,
};

pub(crate) fn maybe_reload_running_daemon(config_path: &Path) {
    let status = match daemon_status(config_path) {
        Ok(status) => status,
        Err(error) => {
            eprintln!("config: failed to inspect daemon status after save: {error}");
            return;
        }
    };
    if !status.running {
        return;
    }
    clear_daemon_control_result(config_path);
    if let Err(error) = request_daemon_reload(config_path) {
        eprintln!("config: failed to request daemon reload after save: {error}");
        return;
    }
    if let Err(error) = wait_for_daemon_control_ack(
        config_path,
        crate::daemon_control_ack_timeout(DaemonControlRequest::Reload),
    ) {
        eprintln!("config: daemon did not acknowledge reload after save: {error}");
        return;
    }
    if let Err(error) = wait_for_daemon_control_result(
        config_path,
        DaemonControlRequest::Reload,
        crate::daemon_control_result_timeout(DaemonControlRequest::Reload),
    ) {
        eprintln!("config: daemon reload after save failed: {error}");
    }
}

#[derive(Debug, Clone, Copy)]
pub(crate) enum RosterEditAction {
    AddDevice,
    RemoveDevice,
    AddAdmin,
    RemoveAdmin,
}

pub(crate) async fn update_active_network_roster(
    args: UpdateRosterArgs,
    action: RosterEditAction,
) -> Result<()> {
    let config_path = args.config.unwrap_or_else(default_config_path);
    let mut app = load_or_default_config(&config_path)?;
    if let Some(network_id) = args.network_id {
        app.set_active_network_id(&network_id)?;
    }
    let active_network_id = app
        .active_network_opt()
        .ok_or_else(|| anyhow!("create or join a network first"))?
        .id
        .clone();

    let mut changed = Vec::new();
    for device in &args.devices {
        let normalized = match action {
            RosterEditAction::AddDevice => app.add_device_to_network(&active_network_id, device)?,
            RosterEditAction::RemoveDevice => {
                let normalized = normalize_nostr_pubkey(device)?;
                app.remove_device_from_network(&active_network_id, device)?;
                normalized
            }
            RosterEditAction::AddAdmin => app.add_admin_to_network(&active_network_id, device)?,
            RosterEditAction::RemoveAdmin => {
                let normalized = normalize_nostr_pubkey(device)?;
                app.remove_admin_from_network(&active_network_id, device)?;
                normalized
            }
        };
        changed.push(normalized);
    }

    app.ensure_defaults();
    maybe_autoconfigure_node(&mut app);
    app.save(&config_path)?;
    maybe_reload_running_daemon(&config_path);

    let published = 0usize;

    if args.json {
        let active_network = app
            .active_network_opt()
            .ok_or_else(|| anyhow!("create or join a network first"))?;
        println!(
            "{}",
            serde_json::to_string_pretty(&json!({
                        "network_id": app.effective_network_id(),
                        "devices": active_network.devices,
                        "participants": active_network.devices,
                        "admins": active_network.admins,
                "changed": changed,
                "published_recipients": published,
                "published": args.publish,
            }))?
        );
    } else {
        println!("saved {}", config_path.display());
        println!("network_id={}", app.effective_network_id());
        println!("changed={}", changed.join(","));
        if args.publish {
            println!("published_recipients={published}");
        }
    }

    Ok(())
}