735 lines
25 KiB
Rust
735 lines
25 KiB
Rust
//! Offline marketplace folder classification and capability-backed listing state.
|
|
|
|
#![allow(clippy::missing_errors_doc)]
|
|
#![allow(clippy::must_use_candidate)]
|
|
#![allow(clippy::needless_pass_by_value)]
|
|
|
|
use crate::agent_manager::EventRegistry;
|
|
use crate::client_core::ClientWeakHandle;
|
|
use crate::marketplace::{
|
|
MarketplaceFolderRole, MarketplaceListingStatus, MarketplaceValidationFlags,
|
|
};
|
|
use crate::{Error, GridClient, Inventory, InventoryFolder};
|
|
use libremetaverse_structured_data::{OSD, OSDParser};
|
|
use libremetaverse_types::compat::{
|
|
CancellationToken, EventHandler, ExternalError, HttpResponse, Subscription, Uri,
|
|
};
|
|
use libremetaverse_types::{FolderType, UUID};
|
|
use serde_json::{Value, json};
|
|
use std::collections::HashMap;
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
use std::sync::{Arc, RwLock};
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
const JSON_CONTENT_TYPE: &str = "application/json";
|
|
|
|
fn read<T>(value: &RwLock<T>) -> std::sync::RwLockReadGuard<'_, T> {
|
|
value
|
|
.read()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
}
|
|
|
|
fn write<T>(value: &RwLock<T>) -> std::sync::RwLockWriteGuard<'_, T> {
|
|
value
|
|
.write()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
}
|
|
|
|
fn folder_at(inventory: &Inventory, id: UUID) -> Option<InventoryFolder> {
|
|
inventory
|
|
.get_node_or_default(id)
|
|
.ok()
|
|
.flatten()
|
|
.and_then(|node| node.data())
|
|
.and_then(|value| value.as_any().downcast_ref::<InventoryFolder>().cloned())
|
|
}
|
|
|
|
fn child_folders(inventory: &Inventory, parent: UUID) -> Vec<InventoryFolder> {
|
|
inventory
|
|
.get_contents_with_uuid(parent)
|
|
.unwrap_or_default()
|
|
.into_iter()
|
|
.filter_map(|value| value.as_any().downcast_ref::<InventoryFolder>().cloned())
|
|
.collect()
|
|
}
|
|
|
|
/// Pure inventory-tree decisions used by marketplace workflows.
|
|
pub struct MarketplaceFolderClassifier;
|
|
|
|
impl MarketplaceFolderClassifier {
|
|
pub fn get_listings_root(inventory: &Inventory) -> Result<UUID, Error> {
|
|
Ok(inventory
|
|
.find_folder_for_type(FolderType::MarketplaceListings)
|
|
.unwrap_or_else(UUID::zero))
|
|
}
|
|
|
|
pub fn get_role(
|
|
folder_id: UUID,
|
|
inventory: &Inventory,
|
|
) -> Result<MarketplaceFolderRole, Error> {
|
|
let Some(folder) = folder_at(inventory, folder_id) else {
|
|
return Ok(MarketplaceFolderRole::None);
|
|
};
|
|
if folder.preferred_type() == FolderType::MarketplaceListings {
|
|
return Ok(MarketplaceFolderRole::ListingsRoot);
|
|
}
|
|
let Some(parent) = folder_at(inventory, folder.base.parent_uuid()) else {
|
|
return Ok(MarketplaceFolderRole::None);
|
|
};
|
|
if parent.preferred_type() == FolderType::MarketplaceListings {
|
|
return Ok(MarketplaceFolderRole::Listing);
|
|
}
|
|
let parent_role = Self::get_role(parent.base.uuid(), inventory)?;
|
|
if folder.preferred_type() == FolderType::MarketplaceVersion
|
|
&& parent_role == MarketplaceFolderRole::Listing
|
|
{
|
|
return Ok(MarketplaceFolderRole::Version);
|
|
}
|
|
if folder.preferred_type() == FolderType::MarketplaceStock
|
|
&& parent_role == MarketplaceFolderRole::Version
|
|
{
|
|
return Ok(MarketplaceFolderRole::Stock);
|
|
}
|
|
if parent_role == MarketplaceFolderRole::Version {
|
|
return Ok(MarketplaceFolderRole::Content);
|
|
}
|
|
Ok(MarketplaceFolderRole::None)
|
|
}
|
|
|
|
pub fn is_marketplace_folder(folder_id: UUID, inventory: &Inventory) -> Result<bool, Error> {
|
|
Ok(Self::get_role(folder_id, inventory)? != MarketplaceFolderRole::None)
|
|
}
|
|
|
|
pub fn get_listing_folder(folder_id: UUID, inventory: &Inventory) -> Result<UUID, Error> {
|
|
let mut current = folder_id;
|
|
for _ in 0..512 {
|
|
match Self::get_role(current, inventory)? {
|
|
MarketplaceFolderRole::Listing => return Ok(current),
|
|
MarketplaceFolderRole::ListingsRoot | MarketplaceFolderRole::None => {
|
|
return Ok(UUID::zero());
|
|
}
|
|
_ => {
|
|
let Some(folder) = folder_at(inventory, current) else {
|
|
return Ok(UUID::zero());
|
|
};
|
|
current = folder.base.parent_uuid();
|
|
}
|
|
}
|
|
}
|
|
Ok(UUID::zero())
|
|
}
|
|
|
|
pub fn get_version_folder(listing: UUID, inventory: &Inventory) -> Result<UUID, Error> {
|
|
if Self::get_role(listing, inventory)? != MarketplaceFolderRole::Listing {
|
|
return Ok(UUID::zero());
|
|
}
|
|
Ok(child_folders(inventory, listing)
|
|
.into_iter()
|
|
.find(|folder| folder.preferred_type() == FolderType::MarketplaceVersion)
|
|
.map_or_else(UUID::zero, |folder| folder.base.uuid()))
|
|
}
|
|
|
|
pub fn get_stock_count(version: UUID, inventory: &Inventory) -> Result<i32, Error> {
|
|
if version == UUID::zero() {
|
|
return Ok(0);
|
|
}
|
|
let count = inventory
|
|
.get_contents_with_uuid(version)
|
|
.unwrap_or_default()
|
|
.iter()
|
|
.filter(|value| value.as_any().downcast_ref::<InventoryFolder>().is_none())
|
|
.count();
|
|
Ok(i32::try_from(count).unwrap_or(i32::MAX))
|
|
}
|
|
|
|
pub fn get_all_listing_folder_ids(inventory: &Inventory) -> Result<Vec<UUID>, Error> {
|
|
let root = Self::get_listings_root(inventory)?;
|
|
if root == UUID::zero() {
|
|
return Ok(Vec::new());
|
|
}
|
|
Ok(child_folders(inventory, root)
|
|
.into_iter()
|
|
.map(|folder| folder.base.uuid())
|
|
.collect())
|
|
}
|
|
|
|
pub fn validate_listing(
|
|
listing: UUID,
|
|
inventory: &Inventory,
|
|
) -> Result<MarketplaceValidationFlags, Error> {
|
|
if Self::get_role(listing, inventory)? != MarketplaceFolderRole::Listing {
|
|
return Ok(MarketplaceValidationFlags::INVALID_STRUCTURE);
|
|
}
|
|
let versions: Vec<_> = child_folders(inventory, listing)
|
|
.into_iter()
|
|
.filter(|folder| folder.preferred_type() == FolderType::MarketplaceVersion)
|
|
.collect();
|
|
match versions.as_slice() {
|
|
[] => Ok(MarketplaceValidationFlags::MISSING_VERSION_FOLDER),
|
|
[version] if Self::get_stock_count(version.base.uuid(), inventory)? == 0 => {
|
|
Ok(MarketplaceValidationFlags::EMPTY_LISTING)
|
|
}
|
|
[_] => Ok(MarketplaceValidationFlags::VALID),
|
|
_ => Ok(MarketplaceValidationFlags::MULTIPLE_VERSION_FOLDERS),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct MarketplaceListing {
|
|
edit_url: Option<String>,
|
|
last_sync_utc: SystemTime,
|
|
listing_folder_uuid: UUID,
|
|
listing_id: i32,
|
|
status: MarketplaceListingStatus,
|
|
stock_count: i32,
|
|
version_folder_uuid: UUID,
|
|
}
|
|
|
|
impl MarketplaceListing {
|
|
pub fn new() -> Result<Self, Error> {
|
|
Ok(Self {
|
|
edit_url: None,
|
|
last_sync_utc: UNIX_EPOCH,
|
|
listing_folder_uuid: UUID::zero(),
|
|
listing_id: 0,
|
|
status: MarketplaceListingStatus::Unknown,
|
|
stock_count: 0,
|
|
version_folder_uuid: UUID::zero(),
|
|
})
|
|
}
|
|
pub fn edit_url(&self) -> Option<String> {
|
|
self.edit_url.clone()
|
|
}
|
|
pub fn set_edit_url(&mut self, value: Option<String>) {
|
|
self.edit_url = value;
|
|
}
|
|
pub fn last_sync_utc(&self) -> SystemTime {
|
|
self.last_sync_utc
|
|
}
|
|
pub fn set_last_sync_utc(&mut self, value: SystemTime) {
|
|
self.last_sync_utc = value;
|
|
}
|
|
pub fn listing_folder_uuid(&self) -> UUID {
|
|
self.listing_folder_uuid
|
|
}
|
|
pub fn set_listing_folder_uuid(&mut self, value: UUID) {
|
|
self.listing_folder_uuid = value;
|
|
}
|
|
pub fn listing_id(&self) -> i32 {
|
|
self.listing_id
|
|
}
|
|
pub fn set_listing_id(&mut self, value: i32) {
|
|
self.listing_id = value;
|
|
}
|
|
pub fn status(&self) -> MarketplaceListingStatus {
|
|
self.status
|
|
}
|
|
pub fn set_status(&mut self, value: MarketplaceListingStatus) {
|
|
self.status = value;
|
|
}
|
|
pub fn stock_count(&self) -> i32 {
|
|
self.stock_count
|
|
}
|
|
pub fn set_stock_count(&mut self, value: i32) {
|
|
self.stock_count = value;
|
|
}
|
|
pub fn version_folder_uuid(&self) -> UUID {
|
|
self.version_folder_uuid
|
|
}
|
|
pub fn set_version_folder_uuid(&mut self, value: UUID) {
|
|
self.version_folder_uuid = value;
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct MarketplaceErrorEventArgs {
|
|
message: String,
|
|
exception: Option<Arc<ExternalError>>,
|
|
}
|
|
impl MarketplaceErrorEventArgs {
|
|
pub fn new(message: String, exception: Option<Arc<ExternalError>>) -> Result<Self, Error> {
|
|
Ok(Self { message, exception })
|
|
}
|
|
pub fn message(&self) -> String {
|
|
self.message.clone()
|
|
}
|
|
pub fn exception(&self) -> Option<Arc<ExternalError>> {
|
|
self.exception.clone()
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct MarketplaceListingChangedEventArgs {
|
|
listing_id: i32,
|
|
listing: Option<Arc<MarketplaceListing>>,
|
|
}
|
|
impl MarketplaceListingChangedEventArgs {
|
|
pub fn new(listing_id: i32, listing: Option<Arc<MarketplaceListing>>) -> Result<Self, Error> {
|
|
Ok(Self {
|
|
listing_id,
|
|
listing,
|
|
})
|
|
}
|
|
pub fn listing_id(&self) -> i32 {
|
|
self.listing_id
|
|
}
|
|
pub fn listing(&self) -> Option<Arc<MarketplaceListing>> {
|
|
self.listing.clone()
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct MarketplaceListingsSyncedEventArgs {
|
|
listings: Vec<MarketplaceListing>,
|
|
}
|
|
impl MarketplaceListingsSyncedEventArgs {
|
|
pub fn new(listings: Vec<MarketplaceListing>) -> Result<Self, Error> {
|
|
Ok(Self { listings })
|
|
}
|
|
pub fn listings(&self) -> Vec<MarketplaceListing> {
|
|
self.listings.clone()
|
|
}
|
|
}
|
|
|
|
enum MarketplaceClient {
|
|
Standalone(GridClient),
|
|
ClientOwned(ClientWeakHandle),
|
|
}
|
|
|
|
struct MarketplaceState {
|
|
client: RwLock<MarketplaceClient>,
|
|
by_id: RwLock<HashMap<i32, MarketplaceListing>>,
|
|
by_folder: RwLock<HashMap<UUID, MarketplaceListing>>,
|
|
errors: EventRegistry<MarketplaceErrorEventArgs>,
|
|
changed: EventRegistry<MarketplaceListingChangedEventArgs>,
|
|
synced: EventRegistry<MarketplaceListingsSyncedEventArgs>,
|
|
live_mutations: AtomicBool,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct MarketplaceManager(Arc<MarketplaceState>);
|
|
|
|
impl MarketplaceManager {
|
|
pub fn new(client: GridClient) -> Result<Self, Error> {
|
|
Ok(Self::with_client(MarketplaceClient::Standalone(client)))
|
|
}
|
|
|
|
fn with_client(client: MarketplaceClient) -> Self {
|
|
Self(Arc::new(MarketplaceState {
|
|
client: RwLock::new(client),
|
|
by_id: RwLock::new(HashMap::new()),
|
|
by_folder: RwLock::new(HashMap::new()),
|
|
errors: EventRegistry::default(),
|
|
changed: EventRegistry::default(),
|
|
synced: EventRegistry::default(),
|
|
live_mutations: AtomicBool::new(false),
|
|
}))
|
|
}
|
|
|
|
pub(crate) fn native_new_client_owned(client: &GridClient) -> Self {
|
|
Self::with_client(MarketplaceClient::ClientOwned(client.native_weak_handle()))
|
|
}
|
|
|
|
pub(crate) fn native_bind_to_client(&self, client: &GridClient) {
|
|
*write(&self.0.client) = MarketplaceClient::ClientOwned(client.native_weak_handle());
|
|
}
|
|
|
|
fn client(&self) -> Result<GridClient, Error> {
|
|
match &*read(&self.0.client) {
|
|
MarketplaceClient::Standalone(client) => Ok(client.clone()),
|
|
MarketplaceClient::ClientOwned(client) => {
|
|
client.upgrade().ok_or(Error::InvalidOperation)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Explicitly enables mutation requests to non-test capability hosts.
|
|
pub fn enable_live_mutations(&self) {
|
|
self.0.live_mutations.store(true, Ordering::Release);
|
|
}
|
|
|
|
pub fn subscribe_error(
|
|
&self,
|
|
handler: EventHandler<MarketplaceErrorEventArgs>,
|
|
) -> Subscription {
|
|
self.0.errors.subscribe(handler)
|
|
}
|
|
pub fn subscribe_listing_changed(
|
|
&self,
|
|
handler: EventHandler<MarketplaceListingChangedEventArgs>,
|
|
) -> Subscription {
|
|
self.0.changed.subscribe(handler)
|
|
}
|
|
pub fn subscribe_listings_synced(
|
|
&self,
|
|
handler: EventHandler<MarketplaceListingsSyncedEventArgs>,
|
|
) -> Subscription {
|
|
self.0.synced.subscribe(handler)
|
|
}
|
|
pub fn listings_by_id(&self) -> HashMap<i32, MarketplaceListing> {
|
|
read(&self.0.by_id).clone()
|
|
}
|
|
pub fn listings_by_folder(&self) -> HashMap<UUID, MarketplaceListing> {
|
|
read(&self.0.by_folder).clone()
|
|
}
|
|
pub fn try_get_by_id(&self, id: i32, output: &mut Option<MarketplaceListing>) -> bool {
|
|
*output = read(&self.0.by_id).get(&id).cloned();
|
|
output.is_some()
|
|
}
|
|
pub fn try_get_by_folder(&self, folder: UUID, output: &mut Option<MarketplaceListing>) -> bool {
|
|
*output = read(&self.0.by_folder).get(&folder).cloned();
|
|
output.is_some()
|
|
}
|
|
|
|
fn emit_error(&self, message: impl Into<String>, error: Option<&Error>) {
|
|
let exception = error.map(|error| Arc::new(ExternalError(error.to_string())));
|
|
self.0.errors.emit(MarketplaceErrorEventArgs {
|
|
message: message.into(),
|
|
exception,
|
|
});
|
|
}
|
|
|
|
async fn base_uri(&self, token: &CancellationToken) -> Result<Uri, Error> {
|
|
token.throw_if_cancellation_requested()?;
|
|
let simulator = self
|
|
.client()?
|
|
.native_network()?
|
|
.native_current_sim()
|
|
.ok_or(Error::InvalidOperation)?;
|
|
if let Some(caps) = simulator.native_caps()
|
|
&& let Some(uri) = caps.capability_uri("DirectDelivery".into())?
|
|
{
|
|
return Ok(uri);
|
|
}
|
|
// Seed discovery runs independently of callers. If it has not installed
|
|
// its map yet, resolve the one required capability through the same
|
|
// bounded, injected HTTP client instead of racing the background task.
|
|
let seed = simulator.seed_capability().ok_or(Error::InvalidOperation)?;
|
|
let simulator_client = simulator.native_data_arc().client.clone();
|
|
let (response, body) = simulator_client
|
|
.native_http_caps_client()
|
|
.get(seed, token.clone(), None)
|
|
.await?;
|
|
if !(200..300).contains(&response.status_code) {
|
|
return Err(Error::InvalidOperation);
|
|
}
|
|
let OSD::Map(map) = OSDParser::deserialize_with_bytes(body)? else {
|
|
return Err(Error::InvalidOperation);
|
|
};
|
|
map.get("DirectDelivery")
|
|
.and_then(|value| value.as_uri().ok().flatten())
|
|
.ok_or(Error::InvalidOperation)
|
|
}
|
|
|
|
async fn request(
|
|
&self,
|
|
method: &str,
|
|
path: &str,
|
|
payload: Vec<u8>,
|
|
token: CancellationToken,
|
|
mutation: bool,
|
|
) -> Result<(HttpResponse, Vec<u8>), Error> {
|
|
let base = self.base_uri(&token).await?;
|
|
if mutation
|
|
&& !crate::is_offline_fixture_uri(&base)
|
|
&& !self.0.live_mutations.load(Ordering::Acquire)
|
|
{
|
|
return Err(Error::InvalidOperation);
|
|
}
|
|
let uri = Uri(format!("{}/{}", base.0.trim_end_matches('/'), path));
|
|
let client = self.client()?.native_http_caps_client();
|
|
match method {
|
|
"GET" => client.get(uri, token, None).await,
|
|
"POST" => {
|
|
client
|
|
.post_with_uri_string_bytes_cancellation_token_i_progress(
|
|
uri,
|
|
JSON_CONTENT_TYPE.into(),
|
|
payload,
|
|
token,
|
|
None,
|
|
)
|
|
.await
|
|
}
|
|
"PUT" => {
|
|
client
|
|
.put_with_uri_string_bytes_cancellation_token_i_progress(
|
|
uri,
|
|
JSON_CONTENT_TYPE.into(),
|
|
payload,
|
|
token,
|
|
None,
|
|
)
|
|
.await
|
|
}
|
|
"DELETE" => {
|
|
client
|
|
.delete_with_uri_string_bytes_cancellation_token_i_progress(
|
|
uri,
|
|
JSON_CONTENT_TYPE.into(),
|
|
payload,
|
|
token,
|
|
None,
|
|
)
|
|
.await
|
|
}
|
|
_ => Err(Error::Argument),
|
|
}
|
|
}
|
|
|
|
fn parse_listings(bytes: &[u8]) -> Result<Vec<MarketplaceListing>, Error> {
|
|
let root: Value = serde_json::from_slice(bytes).map_err(|error| Error::Parse {
|
|
position: error.column(),
|
|
context: "marketplace JSON",
|
|
})?;
|
|
let array = root
|
|
.get("listings")
|
|
.and_then(Value::as_array)
|
|
.ok_or(Error::Parse {
|
|
position: 0,
|
|
context: "marketplace listings array",
|
|
})?;
|
|
array.iter().map(Self::parse_listing).collect()
|
|
}
|
|
|
|
fn parse_listing(value: &Value) -> Result<MarketplaceListing, Error> {
|
|
let mut listing = MarketplaceListing::new()?;
|
|
listing.listing_id = value
|
|
.get("id")
|
|
.and_then(Value::as_i64)
|
|
.and_then(|id| i32::try_from(id).ok())
|
|
.unwrap_or_default();
|
|
listing.status = match value.get("is_listed").and_then(Value::as_bool) {
|
|
Some(true) => MarketplaceListingStatus::Listed,
|
|
Some(false) => MarketplaceListingStatus::Unlisted,
|
|
None => MarketplaceListingStatus::Unknown,
|
|
};
|
|
listing.edit_url = value
|
|
.get("edit_url")
|
|
.and_then(Value::as_str)
|
|
.map(str::to_owned);
|
|
if let Some(info) = value.get("inventory_info") {
|
|
listing.listing_folder_uuid = parse_uuid(info.get("listing_folder_id"));
|
|
listing.version_folder_uuid = parse_uuid(info.get("version_folder_id"));
|
|
listing.stock_count = info
|
|
.get("count_on_hand")
|
|
.and_then(Value::as_i64)
|
|
.and_then(|count| i32::try_from(count).ok())
|
|
.unwrap_or_default();
|
|
}
|
|
listing.last_sync_utc = SystemTime::now();
|
|
Ok(listing)
|
|
}
|
|
|
|
fn replace_cache(&self, listings: &[MarketplaceListing]) {
|
|
let by_id = listings
|
|
.iter()
|
|
.cloned()
|
|
.map(|listing| (listing.listing_id, listing))
|
|
.collect();
|
|
let by_folder = listings
|
|
.iter()
|
|
.filter(|listing| listing.listing_folder_uuid != UUID::zero())
|
|
.cloned()
|
|
.map(|listing| (listing.listing_folder_uuid, listing))
|
|
.collect();
|
|
*write(&self.0.by_id) = by_id;
|
|
*write(&self.0.by_folder) = by_folder;
|
|
}
|
|
|
|
fn cache_listing(&self, listing: MarketplaceListing) {
|
|
write(&self.0.by_id).insert(listing.listing_id, listing.clone());
|
|
if listing.listing_folder_uuid != UUID::zero() {
|
|
write(&self.0.by_folder).insert(listing.listing_folder_uuid, listing);
|
|
}
|
|
}
|
|
|
|
pub async fn fetch_listings(&self, ct: Option<CancellationToken>) -> Result<(), Error> {
|
|
let token = ct.unwrap_or_default();
|
|
let response = self
|
|
.request("GET", "listings", Vec::new(), token, false)
|
|
.await;
|
|
let (response, body) = match response {
|
|
Ok(value) => value,
|
|
Err(Error::Cancelled) => return Err(Error::Cancelled),
|
|
Err(error) => {
|
|
self.emit_error("Failed to fetch marketplace listings", Some(&error));
|
|
return Ok(());
|
|
}
|
|
};
|
|
if !(200..300).contains(&response.status_code) {
|
|
self.emit_error(
|
|
format!(
|
|
"Marketplace listings request returned HTTP {}",
|
|
response.status_code
|
|
),
|
|
None,
|
|
);
|
|
return Ok(());
|
|
}
|
|
match Self::parse_listings(&body) {
|
|
Ok(listings) => {
|
|
self.replace_cache(&listings);
|
|
self.0
|
|
.synced
|
|
.emit(MarketplaceListingsSyncedEventArgs { listings });
|
|
}
|
|
Err(error) => self.emit_error("Invalid marketplace listings response", Some(&error)),
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn create_listing(
|
|
&self,
|
|
folder: UUID,
|
|
version: UUID,
|
|
name: String,
|
|
count: Option<i32>,
|
|
ct: Option<CancellationToken>,
|
|
) -> Result<Option<MarketplaceListing>, Error> {
|
|
let body = json!({"inventory_info": {"listing_folder_id": folder.to_string(), "version_folder_id": version.to_string(), "count_on_hand": count, "name": name}});
|
|
let token = ct.unwrap_or_default();
|
|
let result = self
|
|
.request(
|
|
"POST",
|
|
"listings",
|
|
serde_json::to_vec(&body).map_err(|_| Error::InvalidOperation)?,
|
|
token,
|
|
true,
|
|
)
|
|
.await;
|
|
let Some(listing) = self.handle_listing_response(result, "create marketplace listing")?
|
|
else {
|
|
return Ok(None);
|
|
};
|
|
self.cache_listing(listing.clone());
|
|
self.0.changed.emit(MarketplaceListingChangedEventArgs {
|
|
listing_id: listing.listing_id,
|
|
listing: Some(Arc::new(listing.clone())),
|
|
});
|
|
Ok(Some(listing))
|
|
}
|
|
|
|
pub async fn delete_listing(
|
|
&self,
|
|
id: i32,
|
|
ct: Option<CancellationToken>,
|
|
) -> Result<bool, Error> {
|
|
let token = ct.unwrap_or_default();
|
|
let result = self
|
|
.request("DELETE", &format!("listing/{id}"), Vec::new(), token, true)
|
|
.await;
|
|
let (response, _) = match result {
|
|
Ok(value) => value,
|
|
Err(Error::Cancelled) => return Err(Error::Cancelled),
|
|
Err(error) => {
|
|
self.emit_error("Failed to delete marketplace listing", Some(&error));
|
|
return Ok(false);
|
|
}
|
|
};
|
|
if !(200..300).contains(&response.status_code) {
|
|
self.emit_error(
|
|
format!("Marketplace delete returned HTTP {}", response.status_code),
|
|
None,
|
|
);
|
|
return Ok(false);
|
|
}
|
|
if let Some(old) = write(&self.0.by_id).remove(&id) {
|
|
write(&self.0.by_folder).remove(&old.listing_folder_uuid);
|
|
}
|
|
self.0.changed.emit(MarketplaceListingChangedEventArgs {
|
|
listing_id: id,
|
|
listing: None,
|
|
});
|
|
Ok(true)
|
|
}
|
|
|
|
pub async fn activate_listing(
|
|
&self,
|
|
id: i32,
|
|
ct: Option<CancellationToken>,
|
|
) -> Result<bool, Error> {
|
|
self.set_listing_active(id, true, ct).await
|
|
}
|
|
pub async fn deactivate_listing(
|
|
&self,
|
|
id: i32,
|
|
ct: Option<CancellationToken>,
|
|
) -> Result<bool, Error> {
|
|
self.set_listing_active(id, false, ct).await
|
|
}
|
|
|
|
async fn set_listing_active(
|
|
&self,
|
|
id: i32,
|
|
active: bool,
|
|
ct: Option<CancellationToken>,
|
|
) -> Result<bool, Error> {
|
|
let Some(cached) = read(&self.0.by_id).get(&id).cloned() else {
|
|
return Ok(false);
|
|
};
|
|
let body = json!({"id": id, "is_listed": active, "inventory_info": {"listing_folder_id": cached.listing_folder_uuid.to_string(), "version_folder_id": cached.version_folder_uuid.to_string(), "count_on_hand": cached.stock_count}});
|
|
let result = self
|
|
.request(
|
|
"PUT",
|
|
&format!("listing/{id}"),
|
|
serde_json::to_vec(&body).map_err(|_| Error::InvalidOperation)?,
|
|
ct.unwrap_or_default(),
|
|
true,
|
|
)
|
|
.await;
|
|
let Some(listing) = self.handle_listing_response(result, "update marketplace listing")?
|
|
else {
|
|
return Ok(false);
|
|
};
|
|
self.cache_listing(listing.clone());
|
|
self.0.changed.emit(MarketplaceListingChangedEventArgs {
|
|
listing_id: id,
|
|
listing: Some(Arc::new(listing)),
|
|
});
|
|
Ok(true)
|
|
}
|
|
|
|
fn handle_listing_response(
|
|
&self,
|
|
result: Result<(HttpResponse, Vec<u8>), Error>,
|
|
operation: &str,
|
|
) -> Result<Option<MarketplaceListing>, Error> {
|
|
let (response, body) = match result {
|
|
Ok(value) => value,
|
|
Err(Error::Cancelled) => return Err(Error::Cancelled),
|
|
Err(error) => {
|
|
self.emit_error(format!("Failed to {operation}"), Some(&error));
|
|
return Ok(None);
|
|
}
|
|
};
|
|
if !(200..300).contains(&response.status_code) {
|
|
self.emit_error(
|
|
format!(
|
|
"Marketplace operation returned HTTP {}",
|
|
response.status_code
|
|
),
|
|
None,
|
|
);
|
|
return Ok(None);
|
|
}
|
|
match Self::parse_listings(&body)
|
|
.and_then(|mut listings| listings.drain(..).next().ok_or(Error::InvalidOperation))
|
|
{
|
|
Ok(listing) => Ok(Some(listing)),
|
|
Err(error) => {
|
|
self.emit_error("Invalid marketplace listing response", Some(&error));
|
|
Ok(None)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn parse_uuid(value: Option<&Value>) -> UUID {
|
|
value
|
|
.and_then(Value::as_str)
|
|
.and_then(|value| UUID::new_with_string(value.to_owned()).ok())
|
|
.unwrap_or_else(UUID::zero)
|
|
}
|