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]]
name = "rbx_asset"
version = "0.3.4"
version = "0.4.2"
dependencies = [
"chrono",
"flate2",

@ -1,6 +1,6 @@
[package]
name = "rbx_asset"
version = "0.3.4"
version = "0.4.2"
edition = "2021"
publish = ["strafesnet"]
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)]
#[allow(nonstandard_style,dead_code)]
@ -31,7 +31,7 @@ pub struct AssetOperation{
operation:RobloxOperation,
}
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(
self.operation
.try_get_reponse(context).await
@ -119,7 +119,7 @@ impl std::error::Error for UpdateError{}
struct GetAssetOperationRequest{
operation_id:String,
}
pub struct GetAssetInfoRequest{
pub struct GetAssetLatestRequest{
pub asset_id:u64,
}
/*
@ -149,25 +149,21 @@ pub struct AssetResponse{
pub assetId:String,//u64 wrapped in quotes wohoo!!
pub assetType:AssetType,
pub creationContext:CreationContext,
pub description:String,
pub description:Option<String>,
pub displayName:String,
pub path:String,
pub revisionCreateTime:chrono::DateTime<chrono::Utc>,
pub revisionId:String,//u64
pub moderationResult:ModerationResult,
pub icon:Option<String>,
pub previews:Option<Vec<Preview>>,
#[serde(default)]
pub previews:Vec<Preview>,
}
#[allow(nonstandard_style,dead_code)]
pub struct GetAssetVersionRequest{
pub asset_id:u64,
pub version:u64,
}
#[allow(nonstandard_style,dead_code)]
pub struct GetAssetRequest{
pub asset_id:u64,
pub version:Option<u64>,
}
#[derive(Debug)]
pub enum GetError{
ParseError(url::ParseError),
@ -182,6 +178,23 @@ impl std::fmt::Display 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 asset_id:u64,
pub cursor:Option<String>,
@ -286,7 +299,7 @@ impl RobloxOperation{
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{
operation_id:self.operation_id()
.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)]
pub struct ApiKey(String);
impl ApiKey{
@ -327,12 +321,12 @@ impl ApiKey{
}
#[derive(Clone)]
pub struct CloudContext{
pub api_key:String,
pub client:reqwest::Client,
pub struct Context{
api_key:String,
client:reqwest::Client,
}
impl CloudContext{
impl Context{
pub fn new(api_key:ApiKey)->Self{
Self{
api_key:api_key.get(),
@ -412,7 +406,7 @@ impl CloudContext{
).await.map_err(GetError::Response)?
.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 url=reqwest::Url::parse(raw_url.as_str()).map_err(GetError::ParseError)?;
@ -421,31 +415,47 @@ impl CloudContext{
).await.map_err(GetError::Response)?
.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 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(
self.get(url).await.map_err(GetError::Reqwest)?
).await.map_err(GetError::Response)?
.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::Raw(readable))=>read_readable(readable),
Err(e)=>Err(e),
}.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>{
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)?;

@ -1,4 +1,4 @@
use crate::ResponseError;
use crate::{ResponseError,ReaderType,maybe_gzip_decode,read_readable};
#[derive(Debug)]
pub enum PostError{
@ -306,25 +306,6 @@ pub struct UserInventoryPageResponse{
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)]
pub struct Cookie(String);
impl Cookie{
@ -337,12 +318,12 @@ impl Cookie{
}
}
#[derive(Clone)]
pub struct CookieContext{
pub cookie:String,
pub client:reqwest::Client,
pub struct Context{
cookie:String,
client:reqwest::Client,
}
impl CookieContext{
impl Context{
pub fn new(cookie:Cookie)->Self{
Self{
cookie:cookie.get(),

@ -20,7 +20,7 @@ impl std::fmt::Display for ResponseError{
}
impl std::error::Error for ResponseError{}
// 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();
if status_code.is_success(){
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 anyhow::{anyhow,Result as AResult};
use futures::StreamExt;
use rbx_asset::cloud::{ApiKey,CloudContext};
use rbx_asset::cookie::{Cookie,CookieContext,AssetVersion,CreationsItem};
use rbx_asset::cloud::{ApiKey,Context as CloudContext};
use rbx_asset::cookie::{Cookie,Context as CookieContext,AssetVersion,CreationsItem};
type AssetID=u64;
type AssetIDFileMap=Vec<(AssetID,PathBuf)>;