#![allow(missing_docs)]
const ASSETDELIVERY_ASSET_API: &str = "https://assetdelivery.roblox.com/v1/asset/?ID={id}";
const ASSETDELIVERY_V2_API: &str = "https://assetdelivery.roblox.com/v2";
use crate::catalog::AssetType;
use crate::validation::RobloxErrorRaw;
use crate::{Client, RoboatError, XCSRF_HEADER};
use bytes::Bytes;
use reqwest::header;
use serde_with::skip_serializing_none;
pub mod request_types;
use serde::Deserialize;
use serde::Serialize;
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AssetIdResponse {
pub errors: Option<Vec<RobloxErrorRaw>>,
pub locations: Vec<request_types::AssetLocation>,
pub request_id: String,
#[serde(rename = "IsHashDynamic")]
pub is_hash_dynamic: bool,
#[serde(rename = "IsCopyrightProtected")]
pub is_copyright_protected: bool,
pub is_archived: bool,
pub asset_type_id: u64,
pub is_recordable: bool,
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[serde(default)]
#[serde(rename_all = "camelCase")]
pub struct AssetBatchPayload {
pub asset_name: Option<String>,
pub asset_type: Option<String>,
pub client_insert: Option<bool>,
pub place_id: Option<String>,
pub request_id: Option<String>,
pub script_insert: Option<bool>,
pub server_place_id: Option<String>,
pub universe_id: Option<String>,
pub accept: Option<String>,
pub encoding: Option<String>,
pub hash: Option<String>,
pub user_asset_id: Option<String>,
pub asset_id: Option<String>,
pub version: Option<String>,
pub asset_version_id: Option<String>,
pub module_place_id: Option<String>,
pub asset_format: Option<String>,
#[serde(rename = "roblox-assetFormat")]
pub roblox_asset_format: Option<String>,
pub content_representation_priority_list: Option<String>,
pub do_not_fallback_to_baseline_representation: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AssetBatchResponse {
pub errors: Option<Vec<RobloxErrorRaw>>,
pub locations: Option<Vec<request_types::Location>>,
pub request_id: Option<String>,
pub is_hash_dynamic: Option<bool>,
pub is_copyright_protected: Option<bool>,
pub is_archived: Option<bool>,
pub asset_type_id: Option<u8>,
pub asset_type: Option<AssetType>,
pub content_representation_specifier: Option<request_types::ContentRepresentationSpecifier>,
pub is_recordable: Option<bool>,
}
impl Client {
pub async fn fetch_asset_metadata(
&self,
asset_id: u64,
) -> Result<AssetIdResponse, RoboatError> {
match self.fetch_asset_metadata_internal(asset_id).await {
Ok(x) => Ok(x),
Err(e) => match e {
RoboatError::InvalidXcsrf(new_xcsrf) => {
self.set_xcsrf(new_xcsrf).await;
self.fetch_asset_metadata_internal(asset_id).await
}
_ => Err(e),
},
}
}
pub async fn post_asset_metadata_batch(
&self,
asset_batch: Vec<AssetBatchPayload>,
) -> Result<Vec<AssetBatchResponse>, RoboatError> {
match self
.post_asset_metadata_batch_internal(asset_batch.clone())
.await
{
Ok(x) => Ok(x),
Err(e) => match e {
RoboatError::InvalidXcsrf(new_xcsrf) => {
self.set_xcsrf(new_xcsrf).await;
self.post_asset_metadata_batch_internal(asset_batch).await
}
_ => Err(e),
},
}
}
pub async fn fetch_asset_data(&self, asset_id: u64) -> Result<Bytes, RoboatError> {
let cookie_string = self.cookie_string()?;
let formatted_url = ASSETDELIVERY_ASSET_API.replace("{id}", &asset_id.to_string());
let xcsrf = self.xcsrf().await;
let response_result = self
.reqwest_client
.get(&formatted_url)
.header(header::COOKIE, cookie_string)
.header(XCSRF_HEADER, xcsrf)
.send()
.await;
let response = Self::validate_request_result(response_result).await?;
let bytes = response.bytes().await.map_err(RoboatError::ReqwestError)?;
Ok(bytes)
}
}
mod internal {
use crate::assetdelivery::AssetBatchPayload;
use crate::assetdelivery::AssetBatchResponse;
use crate::assetdelivery::AssetIdResponse;
use crate::assetdelivery::ASSETDELIVERY_V2_API;
use crate::catalog::catalog_types;
use crate::{Client, RoboatError};
use reqwest::header;
impl Client {
pub(super) async fn post_asset_metadata_batch_internal(
&self,
asset_payload: Vec<AssetBatchPayload>,
) -> Result<Vec<AssetBatchResponse>, RoboatError> {
let cookie = self.cookie_string()?;
let formatted_url = format!("{ASSETDELIVERY_V2_API}/assets/batch");
let request_result = self
.reqwest_client
.post(formatted_url)
.header(header::COOKIE, cookie)
.json(&asset_payload)
.send()
.await;
let response = Self::validate_request_result(request_result).await?;
let mut meta_data = Self::parse_to_raw::<Vec<AssetBatchResponse>>(response).await?;
for batch_resp in &mut meta_data {
if let Some(id) = batch_resp.asset_type_id {
match catalog_types::AssetType::try_from(id as u64) {
Ok(e) => batch_resp.asset_type = Some(e),
Err(..) => {}
}
}
if let Some(roblox_error_raw) = &batch_resp.errors {
for error in roblox_error_raw {
if error.code == 401 {
return Err(RoboatError::InvalidRoblosecurity);
}
}
}
}
Ok(meta_data)
}
pub(super) async fn fetch_asset_metadata_internal(
&self,
asset_id: u64,
) -> Result<AssetIdResponse, RoboatError> {
let cookie = self.cookie_string()?;
let formatted_url = format!("{}/assetid/{}", ASSETDELIVERY_V2_API, asset_id);
let request_result = self
.reqwest_client
.get(formatted_url)
.header(header::COOKIE, cookie)
.send()
.await;
let response = Self::validate_request_result(request_result).await?;
let meta_data = Self::parse_to_raw::<AssetIdResponse>(response).await?;
if let Some(roblox_error_raw) = &meta_data.errors {
let first_error = roblox_error_raw.first().unwrap();
if first_error.code == 401 {
return Err(RoboatError::InvalidRoblosecurity);
} else {
return Err(RoboatError::UnidentifiedStatusCode(first_error.code));
}
}
Ok(meta_data)
}
}
}