Merge pull request 'rbx_asset: cloud: implement new asset-delivery-api' () from staging into master

Reviewed-on: 
This commit is contained in:
Quaternions 2025-04-06 22:23:12 +00:00
commit ad8e1865f3
6 changed files with 86 additions and 76 deletions

2
Cargo.lock generated

@ -1293,7 +1293,7 @@ dependencies = [
[[package]] [[package]]
name = "rbx_asset" name = "rbx_asset"
version = "0.3.4" version = "0.4.2"
dependencies = [ dependencies = [
"chrono", "chrono",
"flate2", "flate2",

@ -1,6 +1,6 @@
[package] [package]
name = "rbx_asset" name = "rbx_asset"
version = "0.3.4" version = "0.4.2"
edition = "2021" edition = "2021"
publish = ["strafesnet"] publish = ["strafesnet"]
repository = "https://git.itzana.me/StrafesNET/asset-tool" repository = "https://git.itzana.me/StrafesNET/asset-tool"

@ -1,4 +1,4 @@
use crate::ResponseError; use crate::{ResponseError,ReaderType,maybe_gzip_decode,read_readable};
#[derive(Debug,serde::Deserialize,serde::Serialize)] #[derive(Debug,serde::Deserialize,serde::Serialize)]
#[allow(nonstandard_style,dead_code)] #[allow(nonstandard_style,dead_code)]
@ -31,7 +31,7 @@ pub struct AssetOperation{
operation:RobloxOperation, operation:RobloxOperation,
} }
impl AssetOperation{ impl AssetOperation{
pub async fn try_get_asset(&self,context:&CloudContext)->Result<AssetResponse,AssetOperationError>{ pub async fn try_get_asset(&self,context:&Context)->Result<AssetResponse,AssetOperationError>{
serde_json::from_value( serde_json::from_value(
self.operation self.operation
.try_get_reponse(context).await .try_get_reponse(context).await
@ -119,7 +119,7 @@ impl std::error::Error for UpdateError{}
struct GetAssetOperationRequest{ struct GetAssetOperationRequest{
operation_id:String, operation_id:String,
} }
pub struct GetAssetInfoRequest{ pub struct GetAssetLatestRequest{
pub asset_id:u64, pub asset_id:u64,
} }
/* /*
@ -149,25 +149,21 @@ pub struct AssetResponse{
pub assetId:String,//u64 wrapped in quotes wohoo!! pub assetId:String,//u64 wrapped in quotes wohoo!!
pub assetType:AssetType, pub assetType:AssetType,
pub creationContext:CreationContext, pub creationContext:CreationContext,
pub description: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 pub revisionId:String,//u64
pub moderationResult:ModerationResult, pub moderationResult:ModerationResult,
pub icon:Option<String>, pub icon:Option<String>,
pub previews:Option<Vec<Preview>>, #[serde(default)]
pub previews:Vec<Preview>,
} }
#[allow(nonstandard_style,dead_code)] #[allow(nonstandard_style,dead_code)]
pub struct GetAssetVersionRequest{ pub struct GetAssetVersionRequest{
pub asset_id:u64, pub asset_id:u64,
pub version:u64, pub version:u64,
} }
#[allow(nonstandard_style,dead_code)]
pub struct GetAssetRequest{
pub asset_id:u64,
pub version:Option<u64>,
}
#[derive(Debug)] #[derive(Debug)]
pub enum GetError{ pub enum GetError{
ParseError(url::ParseError), ParseError(url::ParseError),
@ -182,6 +178,23 @@ impl std::fmt::Display for GetError{
} }
impl std::error::Error for GetError{} impl std::error::Error for GetError{}
#[derive(Debug,serde::Deserialize)]
#[allow(nonstandard_style,dead_code)]
pub struct AssetLocation{
// this field is private so users cannot mutate it
location:String,
pub requestId:String,
pub IsHashDynamic:bool,
pub IsCopyrightProtected:bool,
pub isArchived:bool,
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,
pub cursor:Option<String>, pub cursor:Option<String>,
@ -286,7 +299,7 @@ impl RobloxOperation{
None=>self.path.as_deref()?.get(11..), None=>self.path.as_deref()?.get(11..),
} }
} }
pub async fn try_get_reponse(&self,context:&CloudContext)->Result<serde_json::Value,OperationError>{ pub async fn try_get_reponse(&self,context:&Context)->Result<serde_json::Value,OperationError>{
context.get_asset_operation(GetAssetOperationRequest{ context.get_asset_operation(GetAssetOperationRequest{
operation_id:self.operation_id() operation_id:self.operation_id()
.ok_or(OperationError::NoOperationId)? .ok_or(OperationError::NoOperationId)?
@ -296,25 +309,6 @@ impl RobloxOperation{
} }
} }
//idk how to do this better
enum ReaderType<R:std::io::Read>{
GZip(flate2::read::GzDecoder<std::io::BufReader<R>>),
Raw(std::io::BufReader<R>),
}
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)),
}
}
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)
}
#[derive(Clone)] #[derive(Clone)]
pub struct ApiKey(String); pub struct ApiKey(String);
impl ApiKey{ impl ApiKey{
@ -327,12 +321,12 @@ impl ApiKey{
} }
#[derive(Clone)] #[derive(Clone)]
pub struct CloudContext{ pub struct Context{
pub api_key:String, api_key:String,
pub client:reqwest::Client, client:reqwest::Client,
} }
impl CloudContext{ impl Context{
pub fn new(api_key:ApiKey)->Self{ pub fn new(api_key:ApiKey)->Self{
Self{ Self{
api_key:api_key.get(), api_key:api_key.get(),
@ -412,7 +406,7 @@ impl CloudContext{
).await.map_err(GetError::Response)? ).await.map_err(GetError::Response)?
.json::<RobloxOperation>().await.map_err(GetError::Reqwest) .json::<RobloxOperation>().await.map_err(GetError::Reqwest)
} }
pub async fn get_asset_info(&self,config:GetAssetInfoRequest)->Result<AssetResponse,GetError>{ pub async fn get_asset_info(&self,config:GetAssetLatestRequest)->Result<AssetResponse,GetError>{
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)?;
@ -421,31 +415,47 @@ impl CloudContext{
).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_version(&self,config:GetAssetVersionRequest)->Result<Vec<u8>,GetError>{ pub async fn get_asset_version_info(&self,config:GetAssetVersionRequest)->Result<AssetResponse,GetError>{
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(
self.get(url).await.map_err(GetError::Reqwest)?
).await.map_err(GetError::Response)?
.json::<AssetResponse>().await.map_err(GetError::Reqwest)
}
pub async fn get_asset_location(&self,config:GetAssetLatestRequest)->Result<AssetLocation,GetError>{
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)?;
crate::response_ok(
self.get(url).await.map_err(GetError::Reqwest)?
).await.map_err(GetError::Response)?
.json().await.map_err(GetError::Reqwest)
}
pub async fn get_asset_version_location(&self,config:GetAssetVersionRequest)->Result<AssetLocation,GetError>{
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)?;
crate::response_ok(
self.get(url).await.map_err(GetError::Reqwest)?
).await.map_err(GetError::Response)?
.json().await.map_err(GetError::Reqwest)
}
pub async fn get_asset(&self,config:&AssetLocation)->Result<Vec<u8>,GetError>{
let url=reqwest::Url::parse(config.location.as_str()).map_err(GetError::ParseError)?;
let body=crate::response_ok( let body=crate::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(&mut std::io::Cursor::new(body)){ match maybe_gzip_decode(std::io::Cursor::new(body)){
Ok(ReaderType::GZip(readable))=>read_readable(readable), Ok(ReaderType::GZip(readable))=>read_readable(readable),
Ok(ReaderType::Raw(readable))=>read_readable(readable), Ok(ReaderType::Raw(readable))=>read_readable(readable),
Err(e)=>Err(e), Err(e)=>Err(e),
}.map_err(GetError::IO) }.map_err(GetError::IO)
} }
pub async fn get_asset(&self,config:GetAssetRequest)->Result<Vec<u8>,GetError>{
let version=match config.version{
Some(version)=>version,
None=>self.get_asset_info(GetAssetInfoRequest{asset_id:config.asset_id}).await?.revisionId.parse().unwrap(),
};
self.get_asset_version(GetAssetVersionRequest{
asset_id:config.asset_id,
version,
}).await
}
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)?;

@ -1,4 +1,4 @@
use crate::ResponseError; use crate::{ResponseError,ReaderType,maybe_gzip_decode,read_readable};
#[derive(Debug)] #[derive(Debug)]
pub enum PostError{ pub enum PostError{
@ -306,25 +306,6 @@ pub struct UserInventoryPageResponse{
pub data:Vec<UserInventoryItem>, pub data:Vec<UserInventoryItem>,
} }
//idk how to do this better
enum ReaderType<R:std::io::Read>{
GZip(flate2::read::GzDecoder<std::io::BufReader<R>>),
Raw(std::io::BufReader<R>),
}
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)),
}
}
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)
}
#[derive(Clone)] #[derive(Clone)]
pub struct Cookie(String); pub struct Cookie(String);
impl Cookie{ impl Cookie{
@ -337,12 +318,12 @@ impl Cookie{
} }
} }
#[derive(Clone)] #[derive(Clone)]
pub struct CookieContext{ pub struct Context{
pub cookie:String, cookie:String,
pub client:reqwest::Client, client:reqwest::Client,
} }
impl CookieContext{ impl Context{
pub fn new(cookie:Cookie)->Self{ pub fn new(cookie:Cookie)->Self{
Self{ Self{
cookie:cookie.get(), cookie:cookie.get(),

@ -20,7 +20,7 @@ impl std::fmt::Display for ResponseError{
} }
impl std::error::Error for ResponseError{} impl std::error::Error for ResponseError{}
// lazy function to draw out meaningful info from http response on failure // lazy function to draw out meaningful info from http response on failure
pub async fn response_ok(response:reqwest::Response)->Result<reqwest::Response,ResponseError>{ pub(crate) async fn response_ok(response:reqwest::Response)->Result<reqwest::Response,ResponseError>{
let status_code=response.status(); let status_code=response.status();
if status_code.is_success(){ if status_code.is_success(){
Ok(response) Ok(response)
@ -35,3 +35,22 @@ pub async fn response_ok(response:reqwest::Response)->Result<reqwest::Response,R
})) }))
} }
} }
//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)
}

@ -2,8 +2,8 @@ use std::{io::Read,path::PathBuf};
use clap::{Args,Parser,Subcommand}; use clap::{Args,Parser,Subcommand};
use anyhow::{anyhow,Result as AResult}; use anyhow::{anyhow,Result as AResult};
use futures::StreamExt; use futures::StreamExt;
use rbx_asset::cloud::{ApiKey,CloudContext}; use rbx_asset::cloud::{ApiKey,Context as CloudContext};
use rbx_asset::cookie::{Cookie,CookieContext,AssetVersion,CreationsItem}; use rbx_asset::cookie::{Cookie,Context as CookieContext,AssetVersion,CreationsItem};
type AssetID=u64; type AssetID=u64;
type AssetIDFileMap=Vec<(AssetID,PathBuf)>; type AssetIDFileMap=Vec<(AssetID,PathBuf)>;