//! Capability-backed interest-list mode control. #![allow(clippy::missing_errors_doc)] #![allow(clippy::must_use_candidate)] #![allow(clippy::needless_pass_by_value)] #![allow(clippy::unnecessary_wraps)] // Fixed mapped constructor is fallible. use crate::{Error, GridClient, Simulator}; use libremetaverse_structured_data::{OSD, OSDMap, OSDParser}; use libremetaverse_types::compat::CancellationToken; use std::sync::{Arc, RwLock}; #[derive(Clone, Debug)] pub struct InterestListMessage { mode: String, interest_list_mode: crate::messages::linden::InterestListMode, } impl InterestListMessage { pub fn new() -> Result { Ok(Self { mode: "default".into(), interest_list_mode: crate::messages::linden::InterestListMode::Default, }) } pub fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> { let mode = map .get("mode") .map(|value| value.as_string()) .transpose()? .unwrap_or_else(|| "default".into()); self.set_mode(mode); Ok(()) } pub fn serialize(&self) -> Result { OSDMap::new_with_dictionary(std::collections::HashMap::from([( "mode".into(), OSD::String(self.mode.clone()), )])) } pub fn interest_list_mode(&self) -> crate::messages::linden::InterestListMode { self.interest_list_mode } pub fn set_interest_list_mode(&mut self, value: crate::messages::linden::InterestListMode) { self.interest_list_mode = value; self.mode = mode_name(value).into(); } pub fn mode(&self) -> String { self.mode.clone() } pub fn set_mode(&mut self, value: String) { self.interest_list_mode = if matches!( value.to_ascii_lowercase().as_str(), "360" | "panoramic360" | "panoramic_360" ) { crate::messages::linden::InterestListMode::Panoramic360 } else { crate::messages::linden::InterestListMode::Default }; self.mode = mode_name(self.interest_list_mode).into(); } } impl crate::interfaces::IMessage for InterestListMessage { fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> { InterestListMessage::deserialize(self, map) } fn serialize(&self) -> Result { InterestListMessage::serialize(self) } } fn mode_name(mode: crate::messages::linden::InterestListMode) -> &'static str { match mode { crate::messages::linden::InterestListMode::Default => "default", crate::messages::linden::InterestListMode::Panoramic360 => "360", } } pub(crate) struct InterestListManagerInner { client: Arc, current_mode: RwLock, } #[derive(Clone)] pub struct InterestListManager { pub(crate) inner: Arc, } impl InterestListManager { pub fn new(client: GridClient) -> Result { Self::native_new(Arc::new(client)) } pub(crate) fn native_new(client: Arc) -> Result { if let Some(inner) = client.cached_interest_list_manager_inner() { return Ok(Self { inner }); } let inner = Arc::new(InterestListManagerInner { client: Arc::clone(&client), current_mode: RwLock::new(crate::messages::linden::InterestListMode::Default), }); Ok(Self { inner: client.install_interest_list_manager_inner(inner), }) } pub fn dispose(&self) -> Result<(), Error> { Ok(()) } pub fn current_mode(&self) -> crate::messages::linden::InterestListMode { *self .inner .current_mode .read() .unwrap_or_else(std::sync::PoisonError::into_inner) } pub async fn reset(&self, cancellation_token: Option) -> Result<(), Error> { self.set_mode( crate::messages::linden::InterestListMode::Default, cancellation_token, ) .await } pub async fn set_mode( &self, mode: crate::messages::linden::InterestListMode, cancellation_token: Option, ) -> Result<(), Error> { let token = cancellation_token.unwrap_or_else(|| self.inner.client.cancellation_token()); token.throw_if_cancellation_requested()?; let simulations = self.inner.client.network().simulators.snapshot(); let mut accepted = false; for simulator in simulations { if self .set_mode_on_sim(simulator, mode, Some(token.clone())) .await? { accepted = true; } } if accepted || self.inner.client.network().simulators.is_empty() { *self .inner .current_mode .write() .unwrap_or_else(std::sync::PoisonError::into_inner) = mode; } Ok(()) } pub async fn reset_on_sim( &self, simulator: Simulator, cancellation_token: Option, ) -> Result { self.set_mode_on_sim( simulator, crate::messages::linden::InterestListMode::Default, cancellation_token, ) .await } pub async fn set_mode_on_sim( &self, simulator: Simulator, mode: crate::messages::linden::InterestListMode, cancellation_token: Option, ) -> Result { let token = cancellation_token.unwrap_or_else(|| self.inner.client.cancellation_token()); token.throw_if_cancellation_requested()?; let Some(caps) = simulator.native_caps() else { return Ok(false); }; let Some(uri) = caps.capability_uri("InterestList".into())? else { return Ok(false); }; let mut message = InterestListMessage::new()?; message.set_interest_list_mode(mode); let body = OSDParser::serialize_llsd_xml_bytes(OSD::Map(message.serialize()?.snapshot()))?; let (response, bytes) = self .inner .client .native_http_caps_client() .post_with_uri_string_bytes_cancellation_token_i_progress( uri, "application/llsd+xml".into(), body, token.clone(), None, ) .await?; token.throw_if_cancellation_requested()?; if bytes.len() > 1024 * 1024 { return Err(Error::Argument); } let ok = (200..300).contains(&response.status_code); if ok { *self .inner .current_mode .write() .unwrap_or_else(std::sync::PoisonError::into_inner) = mode; } Ok(ok) } } #[cfg(test)] mod tests { use super::*; use libremetaverse_types::compat::CancellationTokenSource; #[test] fn message_round_trip() { let mut value = InterestListMessage::new().unwrap(); value.set_interest_list_mode(crate::messages::linden::InterestListMode::Panoramic360); let mut copy = InterestListMessage::new().unwrap(); copy.deserialize(value.serialize().unwrap()).unwrap(); assert_eq!(copy.mode(), "360"); assert_eq!( copy.interest_list_mode(), crate::messages::linden::InterestListMode::Panoramic360 ); } #[tokio::test] async fn cancelled_mode_change_does_not_commit() { let client = GridClient::new().unwrap(); let manager = InterestListManager::new(client).unwrap(); let source = CancellationTokenSource::new(); source.cancel(); assert_eq!( manager .set_mode( crate::messages::linden::InterestListMode::Panoramic360, Some(source.token()) ) .await, Err(Error::Cancelled) ); assert_eq!( manager.current_mode(), crate::messages::linden::InterestListMode::Default ); } }