//! Credential-safe native `TestClient` voice capability commands. use super::{FakeBackend, InventoryFuture as BackendFuture, LiveBackend, lock}; use libremetaverse::types::compat::{CancellationToken, Uri}; use libremetaverse_structured_data::{OSD, OSDFormat, OSDParser}; use std::collections::HashMap; use std::time::Duration; const MAX_RESPONSE_BYTES: usize = 1024 * 1024; const REQUEST_TIMEOUT: Duration = Duration::from_secs(30); #[derive(Clone, Copy)] pub(super) enum Command { Parcel, Account, } pub(super) fn commands() -> [super::CommandEntry; 2] { use super::{CommandCategory as Cat, CommandHandler}; [ ( "voiceparcel", "Obtain redacted parcel voice information. Usage: voiceparcel", Cat::Other, CommandHandler::Voice(Command::Parcel), ), ( "voiceaccount", "Provision voice account information with credentials redacted. Usage: voiceaccount", Cat::Other, CommandHandler::Voice(Command::Account), ), ] } pub(super) trait Backend { fn voice_query( &self, command: Command, cancellation: CancellationToken, ) -> BackendFuture<'_, Result>; } pub(super) enum VoiceResult { Parcel { region_name_present: bool, parcel_local_id: i32, channel_present: bool, }, Account { username_present: bool, password_present: bool, }, } pub(super) async fn execute( backend: &B, command: Command, args: &[String], cancellation: CancellationToken, ) -> String { if !args.is_empty() { return match command { Command::Parcel => "Usage: voiceparcel", Command::Account => "Usage: voiceaccount", } .into(); } match backend.voice_query(command, cancellation).await { Ok(VoiceResult::Parcel { region_name_present, parcel_local_id, channel_present, }) => format!( "Parcel voice info: region-name-present={region_name_present}, parcel-local-id={parcel_local_id}, channel=<{}>", if channel_present { "redacted" } else { "missing" } ), Ok(VoiceResult::Account { username_present, password_present, }) => format!( "Voice account provisioned: credentials=<{}>", if username_present && password_present { "redacted" } else { "incomplete" } ), Err(error) => error, } } impl Backend for FakeBackend { fn voice_query( &self, command: Command, cancellation: CancellationToken, ) -> BackendFuture<'_, Result> { Box::pin(async move { if cancellation.is_cancellation_requested() { return Err("Voice capability request cancelled".into()); } Ok(match command { Command::Parcel => { self.record("CALL voice-parcel-info".into()); VoiceResult::Parcel { region_name_present: true, parcel_local_id: 42, channel_present: true, } } Command::Account => { self.record("CALL voice-provision-account credentials=".into()); VoiceResult::Account { username_present: true, password_present: true, } } }) }) } } impl Backend for LiveBackend { fn voice_query( &self, command: Command, cancellation: CancellationToken, ) -> BackendFuture<'_, Result> { Box::pin(async move { let capability = match command { Command::Parcel => "ParcelVoiceInfoRequest", Command::Account => "ProvisionVoiceAccountRequest", }; let simulator = self .network .current_sim() .ok_or_else(|| "No current simulator is available".to_owned())?; let uri = simulator .native_capability_uri(capability) .map_err(|_| "Voice capability lookup failed".to_owned())? .ok_or_else(|| "Required voice capability is unavailable".to_owned())?; let http = lock(&self.client).http_caps_client(); let request = http.post_with_uri_osd_format_osd_cancellation_token_i_progress( Uri(uri.0), OSDFormat::Xml, OSD::Map(HashMap::new()), cancellation.clone(), None, ); let result = tokio::select! { () = cancellation.cancelled() => return Err("Voice capability request cancelled".into()), () = tokio::time::sleep(REQUEST_TIMEOUT) => return Err("Voice capability request timed out".into()), result = request => result, } .map_err(|_| "Voice capability request failed".to_owned())?; let (response, bytes) = result; if !response.is_success_status_code() || bytes.len() > MAX_RESPONSE_BYTES { return Err("Voice capability returned an error".into()); } let OSD::Map(map) = OSDParser::deserialize_with_bytes(bytes) .map_err(|_| "Voice capability returned malformed LLSD".to_owned())? else { return Err("Voice capability response was not an LLSD map".into()); }; Ok(match command { Command::Parcel => { let region_name_present = present_string(&map, "region_name"); let parcel_local_id = map .get("parcel_local_id") .and_then(|value| value.as_integer().ok()) .ok_or_else(|| "Parcel voice response omitted local ID".to_owned())?; let channel_present = match map.get("voice_credentials") { Some(OSD::Map(credentials)) => present_string(credentials, "channel_uri"), _ => false, }; if !region_name_present || !channel_present { return Err("Parcel voice response omitted required fields".into()); } VoiceResult::Parcel { region_name_present, parcel_local_id, channel_present, } } Command::Account => { let username_present = present_string(&map, "username"); let password_present = present_string(&map, "password"); if !username_present || !password_present { return Err("Voice account response omitted credentials".into()); } VoiceResult::Account { username_present, password_present, } } }) }) } } fn present_string(map: &HashMap, key: &str) -> bool { map.get(key) .and_then(|value| value.as_string().ok()) .is_some_and(|value| !value.is_empty()) }