v0.4.4 api tweaks #15

Merged
Quaternions merged 8 commits from staging into master 2025-04-11 01:29:35 +00:00
8 changed files with 216 additions and 154 deletions

47
Cargo.lock generated

@ -212,9 +212,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.17" version = "1.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c"
dependencies = [ dependencies = [
"jobserver", "jobserver",
"libc", "libc",
@ -378,9 +378,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]] [[package]]
name = "errno" name = "errno"
version = "0.3.10" version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.59.0", "windows-sys 0.59.0",
@ -874,9 +874,9 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.8.0" version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown", "hashbrown",
@ -997,9 +997,9 @@ dependencies = [
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.9.3" version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
[[package]] [[package]]
name = "litemap" name = "litemap"
@ -1056,9 +1056,9 @@ dependencies = [
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.6" version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29c72f6929239626840b28f919ce8981a317fc5dc63ce25c30d2ab372f94886f" checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
dependencies = [ dependencies = [
"adler2", "adler2",
] ]
@ -1117,9 +1117,9 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]] [[package]]
name = "openssl" name = "openssl"
version = "0.10.71" version = "0.10.72"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd" checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da"
dependencies = [ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.0",
"cfg-if", "cfg-if",
@ -1149,9 +1149,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]] [[package]]
name = "openssl-sys" name = "openssl-sys"
version = "0.9.106" version = "0.9.107"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd" checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",
@ -1293,8 +1293,9 @@ dependencies = [
[[package]] [[package]]
name = "rbx_asset" name = "rbx_asset"
version = "0.4.2" version = "0.4.4"
dependencies = [ dependencies = [
"bytes",
"chrono", "chrono",
"flate2", "flate2",
"reqwest", "reqwest",
@ -1523,9 +1524,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls" name = "rustls"
version = "0.23.25" version = "0.23.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c" checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"rustls-pki-types", "rustls-pki-types",
@ -1665,9 +1666,9 @@ dependencies = [
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.14.0" version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
[[package]] [[package]]
name = "socket2" name = "socket2"
@ -1794,9 +1795,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.44.1" version = "1.44.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
@ -2310,9 +2311,9 @@ checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]] [[package]]
name = "xml-rs" name = "xml-rs"
version = "0.8.25" version = "0.8.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4" checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda"
[[package]] [[package]]
name = "yoke" name = "yoke"

@ -1,6 +1,6 @@
[package] [package]
name = "rbx_asset" name = "rbx_asset"
version = "0.4.2" version = "0.4.4"
edition = "2021" edition = "2021"
publish = ["strafesnet"] publish = ["strafesnet"]
repository = "https://git.itzana.me/StrafesNET/asset-tool" repository = "https://git.itzana.me/StrafesNET/asset-tool"
@ -10,9 +10,14 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = ["gzip"]
gzip = ["dep:flate2"]
[dependencies] [dependencies]
bytes = "1.10.1"
chrono = { version = "0.4.38", features = ["serde"] } chrono = { version = "0.4.38", features = ["serde"] }
flate2 = "1.0.29" flate2 = { version = "1.0.29", optional = true }
reqwest = { version = "0.12.4", features = ["json","multipart"] } reqwest = { version = "0.12.4", features = ["json","multipart"] }
serde = { version = "1.0.199", features = ["derive"] } serde = { version = "1.0.199", features = ["derive"] }
serde_json = "1.0.111" serde_json = "1.0.111"

@ -1,4 +1,5 @@
use crate::{ResponseError,ReaderType,maybe_gzip_decode,read_readable}; use crate::util::{serialize_u64,deserialize_u64,response_ok};
use crate::types::{ResponseError,MaybeGzippedBytes};
#[derive(Debug,serde::Deserialize,serde::Serialize)] #[derive(Debug,serde::Deserialize,serde::Serialize)]
#[allow(nonstandard_style,dead_code)] #[allow(nonstandard_style,dead_code)]
@ -65,8 +66,8 @@ pub struct UpdateAssetRequest{
#[derive(Clone,Debug,serde::Deserialize,serde::Serialize)] #[derive(Clone,Debug,serde::Deserialize,serde::Serialize)]
#[allow(nonstandard_style,dead_code)] #[allow(nonstandard_style,dead_code)]
pub enum Creator{ pub enum Creator{
userId(String),//u64 string userId(#[serde(deserialize_with="deserialize_u64",serialize_with="serialize_u64")]u64),
groupId(String),//u64 string groupId(#[serde(deserialize_with="deserialize_u64",serialize_with="serialize_u64")]u64),
} }
#[derive(Debug,serde::Deserialize,serde::Serialize)] #[derive(Debug,serde::Deserialize,serde::Serialize)]
#[allow(nonstandard_style,dead_code)] #[allow(nonstandard_style,dead_code)]
@ -146,14 +147,19 @@ pub struct GetAssetLatestRequest{
#[derive(Debug,serde::Deserialize,serde::Serialize)] #[derive(Debug,serde::Deserialize,serde::Serialize)]
#[allow(nonstandard_style,dead_code)] #[allow(nonstandard_style,dead_code)]
pub struct AssetResponse{ pub struct AssetResponse{
pub assetId:String,//u64 wrapped in quotes wohoo!! //u64 wrapped in quotes wohoo!!
#[serde(deserialize_with="deserialize_u64")]
#[serde(serialize_with="serialize_u64")]
pub assetId:u64,
pub assetType:AssetType, pub assetType:AssetType,
pub creationContext:CreationContext, pub creationContext:CreationContext,
pub description:Option<String>, pub description:Option<String>,
pub displayName:String, pub displayName:String,
pub path:String, pub path:String,
pub revisionCreateTime:chrono::DateTime<chrono::Utc>, pub revisionCreateTime:chrono::DateTime<chrono::Utc>,
pub revisionId:String,//u64 #[serde(deserialize_with="deserialize_u64")]
#[serde(serialize_with="serialize_u64")]
pub revisionId:u64,
pub moderationResult:ModerationResult, pub moderationResult:ModerationResult,
pub icon:Option<String>, pub icon:Option<String>,
#[serde(default)] #[serde(default)]
@ -169,7 +175,6 @@ pub enum GetError{
ParseError(url::ParseError), ParseError(url::ParseError),
Response(ResponseError), Response(ResponseError),
Reqwest(reqwest::Error), Reqwest(reqwest::Error),
IO(std::io::Error)
} }
impl std::fmt::Display for GetError{ impl std::fmt::Display for GetError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
@ -178,22 +183,28 @@ impl std::fmt::Display for GetError{
} }
impl std::error::Error for GetError{} impl std::error::Error for GetError{}
#[derive(Debug,serde::Deserialize,serde::Serialize)]
pub struct AssetLocation(
// the location is private so users cannot mutate it
String
);
impl AssetLocation{
pub fn location(&self)->&str{
let Self(location)=self;
location
}
}
#[derive(Debug,serde::Deserialize)] #[derive(Debug,serde::Deserialize)]
#[allow(nonstandard_style,dead_code)] #[allow(nonstandard_style,dead_code)]
pub struct AssetLocation{ pub struct AssetLocationInfo{
// this field is private so users cannot mutate it pub location:Option<AssetLocation>,
location:String,
pub requestId:String, pub requestId:String,
pub IsHashDynamic:bool, pub IsHashDynamic:bool,
pub IsCopyrightProtected:bool, pub IsCopyrightProtected:bool,
pub isArchived:bool, pub isArchived:bool,
pub assetTypeId:u32, pub assetTypeId:u32,
} }
impl AssetLocation{
pub fn location(&self)->&str{
&self.location
}
}
pub struct AssetVersionsRequest{ pub struct AssetVersionsRequest{
pub asset_id:u64, pub asset_id:u64,
@ -369,7 +380,7 @@ impl Context{
.text("request",request_config) .text("request",request_config)
.part("fileContent",part); .part("fileContent",part);
let operation=crate::response_ok( let operation=response_ok(
self.post_form(url,form).await.map_err(CreateError::Reqwest)? self.post_form(url,form).await.map_err(CreateError::Reqwest)?
).await.map_err(CreateError::Response)? ).await.map_err(CreateError::Response)?
.json::<RobloxOperation>().await.map_err(CreateError::Reqwest)?; .json::<RobloxOperation>().await.map_err(CreateError::Reqwest)?;
@ -388,7 +399,7 @@ impl Context{
.text("request",request_config) .text("request",request_config)
.part("fileContent",reqwest::multipart::Part::bytes(body)); .part("fileContent",reqwest::multipart::Part::bytes(body));
let operation=crate::response_ok( let operation=response_ok(
self.patch_form(url,form).await.map_err(UpdateError::Reqwest)? self.patch_form(url,form).await.map_err(UpdateError::Reqwest)?
).await.map_err(UpdateError::Response)? ).await.map_err(UpdateError::Response)?
.json::<RobloxOperation>().await.map_err(UpdateError::Reqwest)?; .json::<RobloxOperation>().await.map_err(UpdateError::Reqwest)?;
@ -401,7 +412,7 @@ impl Context{
let raw_url=format!("https://apis.roblox.com/assets/v1/operations/{}",config.operation_id); let raw_url=format!("https://apis.roblox.com/assets/v1/operations/{}",config.operation_id);
let url=reqwest::Url::parse(raw_url.as_str()).map_err(GetError::ParseError)?; let url=reqwest::Url::parse(raw_url.as_str()).map_err(GetError::ParseError)?;
crate::response_ok( response_ok(
self.get(url).await.map_err(GetError::Reqwest)? self.get(url).await.map_err(GetError::Reqwest)?
).await.map_err(GetError::Response)? ).await.map_err(GetError::Response)?
.json::<RobloxOperation>().await.map_err(GetError::Reqwest) .json::<RobloxOperation>().await.map_err(GetError::Reqwest)
@ -410,7 +421,7 @@ impl Context{
let raw_url=format!("https://apis.roblox.com/assets/v1/assets/{}",config.asset_id); let raw_url=format!("https://apis.roblox.com/assets/v1/assets/{}",config.asset_id);
let url=reqwest::Url::parse(raw_url.as_str()).map_err(GetError::ParseError)?; let url=reqwest::Url::parse(raw_url.as_str()).map_err(GetError::ParseError)?;
crate::response_ok( response_ok(
self.get(url).await.map_err(GetError::Reqwest)? self.get(url).await.map_err(GetError::Reqwest)?
).await.map_err(GetError::Response)? ).await.map_err(GetError::Response)?
.json::<AssetResponse>().await.map_err(GetError::Reqwest) .json::<AssetResponse>().await.map_err(GetError::Reqwest)
@ -419,48 +430,44 @@ impl Context{
let raw_url=format!("https://apis.roblox.com/assets/v1/assets/{}/versions/{}",config.asset_id,config.version); let raw_url=format!("https://apis.roblox.com/assets/v1/assets/{}/versions/{}",config.asset_id,config.version);
let url=reqwest::Url::parse(raw_url.as_str()).map_err(GetError::ParseError)?; let url=reqwest::Url::parse(raw_url.as_str()).map_err(GetError::ParseError)?;
crate::response_ok( response_ok(
self.get(url).await.map_err(GetError::Reqwest)? self.get(url).await.map_err(GetError::Reqwest)?
).await.map_err(GetError::Response)? ).await.map_err(GetError::Response)?
.json::<AssetResponse>().await.map_err(GetError::Reqwest) .json::<AssetResponse>().await.map_err(GetError::Reqwest)
} }
pub async fn get_asset_location(&self,config:GetAssetLatestRequest)->Result<AssetLocation,GetError>{ pub async fn get_asset_location(&self,config:GetAssetLatestRequest)->Result<AssetLocationInfo,GetError>{
let raw_url=format!("https://apis.roblox.com/asset-delivery-api/v1/assetId/{}",config.asset_id); let raw_url=format!("https://apis.roblox.com/asset-delivery-api/v1/assetId/{}",config.asset_id);
let url=reqwest::Url::parse(raw_url.as_str()).map_err(GetError::ParseError)?; let url=reqwest::Url::parse(raw_url.as_str()).map_err(GetError::ParseError)?;
crate::response_ok( response_ok(
self.get(url).await.map_err(GetError::Reqwest)? self.get(url).await.map_err(GetError::Reqwest)?
).await.map_err(GetError::Response)? ).await.map_err(GetError::Response)?
.json().await.map_err(GetError::Reqwest) .json().await.map_err(GetError::Reqwest)
} }
pub async fn get_asset_version_location(&self,config:GetAssetVersionRequest)->Result<AssetLocation,GetError>{ pub async fn get_asset_version_location(&self,config:GetAssetVersionRequest)->Result<AssetLocationInfo,GetError>{
let raw_url=format!("https://apis.roblox.com/asset-delivery-api/v1/assetId/{}/version/{}",config.asset_id,config.version); let raw_url=format!("https://apis.roblox.com/asset-delivery-api/v1/assetId/{}/version/{}",config.asset_id,config.version);
let url=reqwest::Url::parse(raw_url.as_str()).map_err(GetError::ParseError)?; let url=reqwest::Url::parse(raw_url.as_str()).map_err(GetError::ParseError)?;
crate::response_ok( response_ok(
self.get(url).await.map_err(GetError::Reqwest)? self.get(url).await.map_err(GetError::Reqwest)?
).await.map_err(GetError::Response)? ).await.map_err(GetError::Response)?
.json().await.map_err(GetError::Reqwest) .json().await.map_err(GetError::Reqwest)
} }
pub async fn get_asset(&self,config:&AssetLocation)->Result<Vec<u8>,GetError>{ pub async fn get_asset(&self,config:&AssetLocation)->Result<MaybeGzippedBytes,GetError>{
let url=reqwest::Url::parse(config.location.as_str()).map_err(GetError::ParseError)?; let url=reqwest::Url::parse(config.location()).map_err(GetError::ParseError)?;
let body=crate::response_ok( let bytes=response_ok(
self.get(url).await.map_err(GetError::Reqwest)? self.get(url).await.map_err(GetError::Reqwest)?
).await.map_err(GetError::Response)? ).await.map_err(GetError::Response)?
.bytes().await.map_err(GetError::Reqwest)?; .bytes().await.map_err(GetError::Reqwest)?;
match maybe_gzip_decode(std::io::Cursor::new(body)){ Ok(MaybeGzippedBytes::new(bytes))
Ok(ReaderType::GZip(readable))=>read_readable(readable),
Ok(ReaderType::Raw(readable))=>read_readable(readable),
Err(e)=>Err(e),
}.map_err(GetError::IO)
} }
pub async fn get_asset_versions(&self,config:AssetVersionsRequest)->Result<AssetVersionsResponse,AssetVersionsError>{ pub async fn get_asset_versions(&self,config:AssetVersionsRequest)->Result<AssetVersionsResponse,AssetVersionsError>{
let raw_url=format!("https://apis.roblox.com/assets/v1/assets/{}/versions",config.asset_id); let raw_url=format!("https://apis.roblox.com/assets/v1/assets/{}/versions",config.asset_id);
let url=reqwest::Url::parse(raw_url.as_str()).map_err(AssetVersionsError::ParseError)?; let url=reqwest::Url::parse(raw_url.as_str()).map_err(AssetVersionsError::ParseError)?;
crate::response_ok( response_ok(
self.get(url).await.map_err(AssetVersionsError::Reqwest)? self.get(url).await.map_err(AssetVersionsError::Reqwest)?
).await.map_err(AssetVersionsError::Response)? ).await.map_err(AssetVersionsError::Response)?
.json::<AssetVersionsResponse>().await.map_err(AssetVersionsError::Reqwest) .json::<AssetVersionsResponse>().await.map_err(AssetVersionsError::Reqwest)
@ -475,7 +482,7 @@ impl Context{
} }
} }
crate::response_ok( response_ok(
self.get(url).await.map_err(InventoryPageError::Reqwest)? self.get(url).await.map_err(InventoryPageError::Reqwest)?
).await.map_err(InventoryPageError::Response)? ).await.map_err(InventoryPageError::Response)?
.json::<InventoryPageResponse>().await.map_err(InventoryPageError::Reqwest) .json::<InventoryPageResponse>().await.map_err(InventoryPageError::Reqwest)
@ -489,7 +496,7 @@ impl Context{
query.append_pair("versionType","Published"); query.append_pair("versionType","Published");
} }
crate::response_ok( response_ok(
self.post(url,body).await.map_err(UpdateError::Reqwest)? self.post(url,body).await.map_err(UpdateError::Reqwest)?
).await.map_err(UpdateError::Response)? ).await.map_err(UpdateError::Response)?
.json::<UpdatePlaceResponse>().await.map_err(UpdateError::Reqwest) .json::<UpdatePlaceResponse>().await.map_err(UpdateError::Reqwest)

@ -1,4 +1,5 @@
use crate::{ResponseError,ReaderType,maybe_gzip_decode,read_readable}; use crate::util::response_ok;
use crate::types::{ResponseError,MaybeGzippedBytes};
#[derive(Debug)] #[derive(Debug)]
pub enum PostError{ pub enum PostError{
@ -91,7 +92,6 @@ pub enum GetError{
ParseError(url::ParseError), ParseError(url::ParseError),
Response(ResponseError), Response(ResponseError),
Reqwest(reqwest::Error), Reqwest(reqwest::Error),
IO(std::io::Error)
} }
impl std::fmt::Display for GetError{ impl std::fmt::Display for GetError{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
@ -371,7 +371,7 @@ impl Context{
query.append_pair("groupId",group_id.to_string().as_str()); query.append_pair("groupId",group_id.to_string().as_str());
} }
} }
let response=crate::response_ok( let response=response_ok(
self.post(url,body).await.map_err(CreateError::PostError)? self.post(url,body).await.map_err(CreateError::PostError)?
).await.map_err(CreateError::Response)?; ).await.map_err(CreateError::Response)?;
@ -423,7 +423,7 @@ impl Context{
query.append_pair("groupId",group_id.to_string().as_str()); query.append_pair("groupId",group_id.to_string().as_str());
} }
} }
let response=crate::response_ok( let response=response_ok(
self.post(url,body).await.map_err(UploadError::PostError)? self.post(url,body).await.map_err(UploadError::PostError)?
).await.map_err(UploadError::Response)?; ).await.map_err(UploadError::Response)?;
@ -449,7 +449,7 @@ impl Context{
}) })
} }
} }
pub async fn get_asset(&self,config:GetAssetRequest)->Result<Vec<u8>,GetError>{ pub async fn get_asset(&self,config:GetAssetRequest)->Result<MaybeGzippedBytes,GetError>{
let mut url=reqwest::Url::parse("https://assetdelivery.roblox.com/v1/asset/").map_err(GetError::ParseError)?; let mut url=reqwest::Url::parse("https://assetdelivery.roblox.com/v1/asset/").map_err(GetError::ParseError)?;
//url borrow scope //url borrow scope
{ {
@ -459,16 +459,13 @@ impl Context{
query.append_pair("version",version.to_string().as_str()); query.append_pair("version",version.to_string().as_str());
} }
} }
let body=crate::response_ok(
let bytes=response_ok(
self.get(url).await.map_err(GetError::Reqwest)? self.get(url).await.map_err(GetError::Reqwest)?
).await.map_err(GetError::Response)? ).await.map_err(GetError::Response)?
.bytes().await.map_err(GetError::Reqwest)?; .bytes().await.map_err(GetError::Reqwest)?;
match maybe_gzip_decode(std::io::Cursor::new(body)){ Ok(MaybeGzippedBytes::new(bytes))
Ok(ReaderType::GZip(readable))=>read_readable(readable),
Ok(ReaderType::Raw(readable))=>read_readable(readable),
Err(e)=>Err(e),
}.map_err(GetError::IO)
} }
pub async fn get_asset_v2(&self,config:GetAssetRequest)->Result<GetAssetV2,GetAssetV2Error>{ pub async fn get_asset_v2(&self,config:GetAssetRequest)->Result<GetAssetV2,GetAssetV2Error>{
let mut url=reqwest::Url::parse("https://assetdelivery.roblox.com/v2/asset").map_err(GetAssetV2Error::ParseError)?; let mut url=reqwest::Url::parse("https://assetdelivery.roblox.com/v2/asset").map_err(GetAssetV2Error::ParseError)?;
@ -480,7 +477,7 @@ impl Context{
query.append_pair("version",version.to_string().as_str()); query.append_pair("version",version.to_string().as_str());
} }
} }
let response=crate::response_ok( let response=response_ok(
self.get(url).await.map_err(GetAssetV2Error::Reqwest)? self.get(url).await.map_err(GetAssetV2Error::Reqwest)?
).await.map_err(GetAssetV2Error::Response)?; ).await.map_err(GetAssetV2Error::Response)?;
@ -500,23 +497,19 @@ impl Context{
info, info,
}) })
} }
pub async fn get_asset_v2_download(&self,config:&GetAssetV2Location)->Result<Vec<u8>,GetError>{ pub async fn get_asset_v2_download(&self,config:&GetAssetV2Location)->Result<MaybeGzippedBytes,GetError>{
let url=reqwest::Url::parse(config.location.as_str()).map_err(GetError::ParseError)?; let url=reqwest::Url::parse(config.location.as_str()).map_err(GetError::ParseError)?;
let body=crate::response_ok( let bytes=response_ok(
self.get(url).await.map_err(GetError::Reqwest)? self.get(url).await.map_err(GetError::Reqwest)?
).await.map_err(GetError::Response)? ).await.map_err(GetError::Response)?
.bytes().await.map_err(GetError::Reqwest)?; .bytes().await.map_err(GetError::Reqwest)?;
match maybe_gzip_decode(std::io::Cursor::new(body)){ Ok(MaybeGzippedBytes::new(bytes))
Ok(ReaderType::GZip(readable))=>read_readable(readable),
Ok(ReaderType::Raw(readable))=>read_readable(readable),
Err(e)=>Err(e),
}.map_err(GetError::IO)
} }
pub async fn get_asset_details(&self,config:GetAssetDetailsRequest)->Result<AssetDetails,GetError>{ pub async fn get_asset_details(&self,config:GetAssetDetailsRequest)->Result<AssetDetails,GetError>{
let url=reqwest::Url::parse(format!("https://economy.roblox.com/v2/assets/{}/details",config.asset_id).as_str()).map_err(GetError::ParseError)?; let url=reqwest::Url::parse(format!("https://economy.roblox.com/v2/assets/{}/details",config.asset_id).as_str()).map_err(GetError::ParseError)?;
crate::response_ok( response_ok(
self.get(url).await.map_err(GetError::Reqwest)? self.get(url).await.map_err(GetError::Reqwest)?
).await.map_err(GetError::Response)? ).await.map_err(GetError::Response)?
.json().await.map_err(GetError::Reqwest) .json().await.map_err(GetError::Reqwest)
@ -533,7 +526,7 @@ impl Context{
query.append_pair("cursor",cursor); query.append_pair("cursor",cursor);
} }
} }
crate::response_ok( response_ok(
self.get(url).await.map_err(PageError::Reqwest)? self.get(url).await.map_err(PageError::Reqwest)?
).await.map_err(PageError::Response)? ).await.map_err(PageError::Response)?
.json::<AssetVersionsPageResponse>().await.map_err(PageError::Reqwest) .json::<AssetVersionsPageResponse>().await.map_err(PageError::Reqwest)
@ -548,7 +541,7 @@ impl Context{
query.append_pair("cursor",cursor); query.append_pair("cursor",cursor);
} }
} }
crate::response_ok( response_ok(
self.get(url).await.map_err(PageError::Reqwest)? self.get(url).await.map_err(PageError::Reqwest)?
).await.map_err(PageError::Response)? ).await.map_err(PageError::Response)?
.json::<CreationsPageResponse>().await.map_err(PageError::Reqwest) .json::<CreationsPageResponse>().await.map_err(PageError::Reqwest)
@ -562,7 +555,7 @@ impl Context{
query.append_pair("cursor",cursor); query.append_pair("cursor",cursor);
} }
} }
crate::response_ok( response_ok(
self.get(url).await.map_err(PageError::Reqwest)? self.get(url).await.map_err(PageError::Reqwest)?
).await.map_err(PageError::Response)? ).await.map_err(PageError::Response)?
.json::<UserInventoryPageResponse>().await.map_err(PageError::Reqwest) .json::<UserInventoryPageResponse>().await.map_err(PageError::Reqwest)

@ -1,56 +1,4 @@
pub mod cloud; pub mod cloud;
pub mod cookie; pub mod cookie;
pub mod types;
#[allow(dead_code)] mod util;
#[derive(Debug)]
pub struct StatusCodeWithUrlAndBody{
pub status_code:reqwest::StatusCode,
pub url:url::Url,
pub body:String,
}
#[derive(Debug)]
pub enum ResponseError{
Reqwest(reqwest::Error),
StatusCodeWithUrlAndBody(StatusCodeWithUrlAndBody),
}
impl std::fmt::Display for ResponseError{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f,"{self:?}")
}
}
impl std::error::Error for ResponseError{}
// lazy function to draw out meaningful info from http response on failure
pub(crate) async fn response_ok(response:reqwest::Response)->Result<reqwest::Response,ResponseError>{
let status_code=response.status();
if status_code.is_success(){
Ok(response)
}else{
let url=response.url().to_owned();
let bytes=response.bytes().await.map_err(ResponseError::Reqwest)?;
let body=String::from_utf8_lossy(&bytes).to_string();
Err(ResponseError::StatusCodeWithUrlAndBody(StatusCodeWithUrlAndBody{
status_code,
url,
body,
}))
}
}
//idk how to do this better
pub(crate) enum ReaderType<R:std::io::Read>{
GZip(flate2::read::GzDecoder<std::io::BufReader<R>>),
Raw(std::io::BufReader<R>),
}
pub(crate) fn maybe_gzip_decode<R:std::io::Read>(input:R)->std::io::Result<ReaderType<R>>{
let mut buf=std::io::BufReader::new(input);
let peek=std::io::BufRead::fill_buf(&mut buf)?;
match &peek[0..2]{
b"\x1f\x8b"=>Ok(ReaderType::GZip(flate2::read::GzDecoder::new(buf))),
_=>Ok(ReaderType::Raw(buf)),
}
}
pub(crate) fn read_readable(mut readable:impl std::io::Read)->std::io::Result<Vec<u8>>{
let mut contents=Vec::new();
readable.read_to_end(&mut contents)?;
Ok(contents)
}

68
rbx_asset/src/types.rs Normal file

@ -0,0 +1,68 @@
#[allow(dead_code)]
#[derive(Debug)]
pub struct StatusCodeWithUrlAndBody{
pub status_code:reqwest::StatusCode,
pub url:url::Url,
pub body:String,
}
#[derive(Debug)]
pub enum ResponseError{
Reqwest(reqwest::Error),
StatusCodeWithUrlAndBody(StatusCodeWithUrlAndBody),
}
impl std::fmt::Display for ResponseError{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f,"{self:?}")
}
}
impl std::error::Error for ResponseError{}
#[cfg(feature="gzip")]
use std::io::Cursor;
#[cfg(feature="gzip")]
use flate2::read::GzDecoder;
/// Some bytes that might be gzipped. Use the read_with or to_vec methods to transparently decode gzip.
pub struct MaybeGzippedBytes{
bytes:bytes::Bytes,
}
impl MaybeGzippedBytes{
pub(crate) fn new(bytes:bytes::Bytes)->Self{
Self{bytes}
}
pub fn into_inner(self)->bytes::Bytes{
self.bytes
}
/// get a reference to the bytes, ignoring gzip decoding
pub fn as_raw_ref(&self)->&[u8]{
self.bytes.as_ref()
}
/// Transparently decode gzip data, if present (intermediate allocation)
#[cfg(feature="gzip")]
pub fn to_vec(&self)->std::io::Result<Vec<u8>>{
use std::io::Read;
match self.bytes.get(0..2){
Some(b"\x1f\x8b")=>{
let mut buf=Vec::new();
GzDecoder::new(Cursor::new(self.bytes.as_ref())).read_to_end(&mut buf)?;
Ok(buf)
},
_=>Ok(self.bytes.to_vec())
}
}
/// Read the bytes with the provided decoders.
/// The idea is to make a function that is generic over std::io::Read
/// and pass the same function to both closures.
/// This two closure hack must be done because of the different concrete types.
#[cfg(feature="gzip")]
pub fn read_with<'a,ReadGzip,ReadRaw,T>(&'a self,read_gzip:ReadGzip,read_raw:ReadRaw)->T
where
ReadGzip:Fn(GzDecoder<Cursor<&'a [u8]>>)->T,
ReadRaw:Fn(Cursor<&'a [u8]>)->T,
{
match self.bytes.get(0..2){
Some(b"\x1f\x8b")=>read_gzip(GzDecoder::new(Cursor::new(self.bytes.as_ref()))),
_=>read_raw(Cursor::new(self.bytes.as_ref()))
}
}
}

40
rbx_asset/src/util.rs Normal file

@ -0,0 +1,40 @@
use crate::types::{ResponseError,StatusCodeWithUrlAndBody};
// lazy function to draw out meaningful info from http response on failure
pub(crate) async fn response_ok(response:reqwest::Response)->Result<reqwest::Response,ResponseError>{
let status_code=response.status();
if status_code.is_success(){
Ok(response)
}else{
let url=response.url().to_owned();
let bytes=response.bytes().await.map_err(ResponseError::Reqwest)?;
let body=String::from_utf8_lossy(&bytes).to_string();
Err(ResponseError::StatusCodeWithUrlAndBody(StatusCodeWithUrlAndBody{
status_code,
url,
body,
}))
}
}
use serde::de::{Error,Unexpected};
use serde::{Deserializer,Serializer};
struct U64StringVisitor;
impl serde::de::Visitor<'_> for U64StringVisitor{
type Value=u64;
fn expecting(&self,formatter:&mut std::fmt::Formatter)->std::fmt::Result{
write!(formatter,"string value with int")
}
fn visit_str<E:Error>(self,v:&str)->Result<Self::Value,E>{
v.parse().map_err(|_|E::invalid_value(Unexpected::Str(v),&"u64"))
}
}
pub(crate) fn deserialize_u64<'de,D:Deserializer<'de>>(deserializer:D)->Result<u64,D::Error>{
deserializer.deserialize_any(U64StringVisitor)
}
pub(crate) fn serialize_u64<S:Serializer>(v:&u64,serializer:S)->Result<S::Ok,S::Error>{
serializer.serialize_str(v.to_string().as_str())
}

@ -563,8 +563,8 @@ async fn main()->AResult<()>{
subcommand.api_key_file, subcommand.api_key_file,
).await?, ).await?,
creator:match (subcommand.creator_user_id,subcommand.creator_group_id){ creator:match (subcommand.creator_user_id,subcommand.creator_group_id){
(Some(user_id),None)=>rbx_asset::cloud::Creator::userId(user_id.to_string()), (Some(user_id),None)=>rbx_asset::cloud::Creator::userId(user_id),
(None,Some(group_id))=>rbx_asset::cloud::Creator::groupId(group_id.to_string()), (None,Some(group_id))=>rbx_asset::cloud::Creator::groupId(group_id),
other=>Err(anyhow!("Invalid creator {other:?}"))?, other=>Err(anyhow!("Invalid creator {other:?}"))?,
}, },
input_file:subcommand.input_file, input_file:subcommand.input_file,
@ -585,8 +585,8 @@ async fn main()->AResult<()>{
subcommand.cookie_file, subcommand.cookie_file,
).await?, ).await?,
creator:match (subcommand.creator_user_id,subcommand.creator_group_id){ creator:match (subcommand.creator_user_id,subcommand.creator_group_id){
(Some(user_id),None)=>rbx_asset::cloud::Creator::userId(user_id.to_string()), (Some(user_id),None)=>rbx_asset::cloud::Creator::userId(user_id),
(None,Some(group_id))=>rbx_asset::cloud::Creator::groupId(group_id.to_string()), (None,Some(group_id))=>rbx_asset::cloud::Creator::groupId(group_id),
other=>Err(anyhow!("Invalid creator {other:?}"))?, other=>Err(anyhow!("Invalid creator {other:?}"))?,
}, },
description:subcommand.description.unwrap_or_else(||String::with_capacity(0)), description:subcommand.description.unwrap_or_else(||String::with_capacity(0)),
@ -903,11 +903,11 @@ async fn create_asset_medias(config:CreateAssetMediasConfig)->AResult<()>{
async move{(path, async move{(path,
async move{ async move{
let asset_response=asset_response_result.map_err(DownloadDecalError::PollOperation)?; let asset_response=asset_response_result.map_err(DownloadDecalError::PollOperation)?;
let file=cookie_context.get_asset(rbx_asset::cookie::GetAssetRequest{ let maybe_gzip=cookie_context.get_asset(rbx_asset::cookie::GetAssetRequest{
asset_id:asset_response.assetId.parse().map_err(DownloadDecalError::ParseInt)?, asset_id:asset_response.assetId,
version:None, version:None,
}).await.map_err(DownloadDecalError::Get)?; }).await.map_err(DownloadDecalError::Get)?;
let dom=load_dom(std::io::Cursor::new(file)).map_err(DownloadDecalError::LoadDom)?; let dom=maybe_gzip.read_with(load_dom,load_dom).map_err(DownloadDecalError::LoadDom)?;
let instance=dom.get_by_ref( let instance=dom.get_by_ref(
*dom.root().children().first().ok_or(DownloadDecalError::NoFirstInstance)? *dom.root().children().first().ok_or(DownloadDecalError::NoFirstInstance)?
).ok_or(DownloadDecalError::NoFirstInstance)?; ).ok_or(DownloadDecalError::NoFirstInstance)?;
@ -993,8 +993,8 @@ async fn asset_details(cookie:Cookie,asset_id:AssetID)->AResult<()>{
async fn download_version(cookie:Cookie,asset_id:AssetID,version:Option<u64>,dest:PathBuf)->AResult<()>{ async fn download_version(cookie:Cookie,asset_id:AssetID,version:Option<u64>,dest:PathBuf)->AResult<()>{
let context=CookieContext::new(cookie); let context=CookieContext::new(cookie);
let data=context.get_asset(rbx_asset::cookie::GetAssetRequest{asset_id,version}).await?; let maybe_gzip=context.get_asset(rbx_asset::cookie::GetAssetRequest{asset_id,version}).await?;
tokio::fs::write(dest,data).await?; tokio::fs::write(dest,maybe_gzip.to_vec()?).await?;
Ok(()) Ok(())
} }
@ -1006,9 +1006,9 @@ async fn download_version_v2(cookie:Cookie,asset_id:AssetID,version:Option<u64>,
println!("version:{}",info.version); println!("version:{}",info.version);
let location=info.info.locations.first().ok_or(anyhow::Error::msg("No locations"))?; let location=info.info.locations.first().ok_or(anyhow::Error::msg("No locations"))?;
let data=context.get_asset_v2_download(location).await?; let maybe_gzip=context.get_asset_v2_download(location).await?;
tokio::fs::write(dest,data).await?; tokio::fs::write(dest,maybe_gzip.to_vec()?).await?;
Ok(()) Ok(())
} }
@ -1024,7 +1024,7 @@ async fn download_list(cookie:Cookie,asset_id_file_map:AssetIDFileMap)->AResult<
.buffer_unordered(CONCURRENT_REQUESTS) .buffer_unordered(CONCURRENT_REQUESTS)
.for_each(|b:AResult<_>|async{ .for_each(|b:AResult<_>|async{
match b{ match b{
Ok((dest,data))=>if let Err(e)=tokio::fs::write(dest,data).await{ Ok((dest,maybe_gzip))=>if let Err(e)=(async||{tokio::fs::write(dest,maybe_gzip.to_vec()?).await})().await{
eprintln!("fs error: {}",e); eprintln!("fs error: {}",e);
}, },
Err(e)=>eprintln!("dl error: {}",e), Err(e)=>eprintln!("dl error: {}",e),
@ -1228,9 +1228,9 @@ async fn download_history(mut config:DownloadHistoryConfig)->AResult<()>{
let mut path=output_folder.clone(); let mut path=output_folder.clone();
path.push(format!("{}_v{}.rbxl",config.asset_id,version_number)); path.push(format!("{}_v{}.rbxl",config.asset_id,version_number));
join_set.spawn(async move{ join_set.spawn(async move{
let file=context.get_asset(rbx_asset::cookie::GetAssetRequest{asset_id:config.asset_id,version:Some(version_number)}).await?; let maybe_gzip=context.get_asset(rbx_asset::cookie::GetAssetRequest{asset_id:config.asset_id,version:Some(version_number)}).await?;
tokio::fs::write(path,file).await?; tokio::fs::write(path,maybe_gzip.to_vec()?).await?;
Ok::<_,anyhow::Error>(()) Ok::<_,anyhow::Error>(())
}); });
@ -1350,9 +1350,9 @@ struct DownloadDecompileConfig{
async fn download_decompile(config:DownloadDecompileConfig)->AResult<()>{ async fn download_decompile(config:DownloadDecompileConfig)->AResult<()>{
let context=CookieContext::new(config.cookie); let context=CookieContext::new(config.cookie);
let file=context.get_asset(rbx_asset::cookie::GetAssetRequest{asset_id:config.asset_id,version:None}).await?; let maybe_gzip=context.get_asset(rbx_asset::cookie::GetAssetRequest{asset_id:config.asset_id,version:None}).await?;
let dom=load_dom(std::io::Cursor::new(file))?; let dom=maybe_gzip.read_with(load_dom,load_dom)?;
let context=rox_compiler::DecompiledContext::from_dom(dom); let context=rox_compiler::DecompiledContext::from_dom(dom);
context.write_files(rox_compiler::WriteConfig{ context.write_files(rox_compiler::WriteConfig{
@ -1532,8 +1532,8 @@ async fn download_and_decompile_history_into_git(config:DownloadAndDecompileHist
.map(|asset_version|{ .map(|asset_version|{
let context=context.clone(); let context=context.clone();
tokio::task::spawn(async move{ tokio::task::spawn(async move{
let file=context.get_asset(rbx_asset::cookie::GetAssetRequest{asset_id,version:Some(asset_version.assetVersionNumber)}).await?; let maybe_gzip=context.get_asset(rbx_asset::cookie::GetAssetRequest{asset_id,version:Some(asset_version.assetVersionNumber)}).await?;
let dom=load_dom(std::io::Cursor::new(file))?; let dom=maybe_gzip.read_with(load_dom,load_dom)?;
Ok::<_,anyhow::Error>((asset_version,rox_compiler::DecompiledContext::from_dom(dom))) Ok::<_,anyhow::Error>((asset_version,rox_compiler::DecompiledContext::from_dom(dom)))
}) })
})) }))