Compare commits

..

1 Commits

Author SHA1 Message Date
fe326c8457 investigate podman container from the inside 2024-07-09 12:56:18 -07:00
9 changed files with 182 additions and 653 deletions

4
Cargo.lock generated

@ -110,7 +110,7 @@ checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
[[package]] [[package]]
name = "asset-tool" name = "asset-tool"
version = "0.4.4" version = "0.4.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@ -1166,7 +1166,7 @@ dependencies = [
[[package]] [[package]]
name = "rbx_asset" name = "rbx_asset"
version = "0.2.2" version = "0.2.1"
dependencies = [ dependencies = [
"chrono", "chrono",
"flate2", "flate2",

@ -1,7 +1,7 @@
workspace = { members = ["rbx_asset", "rox_compiler"] } workspace = { members = ["rbx_asset", "rox_compiler"] }
[package] [package]
name = "asset-tool" name = "asset-tool"
version = "0.4.4" version = "0.4.1"
edition = "2021" edition = "2021"
# 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

@ -1,6 +1,6 @@
[package] [package]
name = "rbx_asset" name = "rbx_asset"
version = "0.2.2" version = "0.2.1"
edition = "2021" edition = "2021"
publish = ["strafesnet"] publish = ["strafesnet"]

@ -14,32 +14,9 @@ pub struct CreateAssetRequest{
pub displayName:String, pub displayName:String,
} }
#[derive(Debug)] #[derive(Debug)]
pub enum CreateAssetResponseGetAssetError{
Operation(OperationError),
Serialize(serde_json::Error),
}
impl std::fmt::Display for CreateAssetResponseGetAssetError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for CreateAssetResponseGetAssetError{}
pub struct CreateAssetResponse{
operation:RobloxOperation,
}
impl CreateAssetResponse{
pub async fn try_get_asset(&self,context:&CloudContext)->Result<AssetResponse,CreateAssetResponseGetAssetError>{
serde_json::from_value(
self.operation
.try_get_reponse(context).await
.map_err(CreateAssetResponseGetAssetError::Operation)?
).map_err(CreateAssetResponseGetAssetError::Serialize)
}
}
#[derive(Debug)]
pub enum CreateError{ pub enum CreateError{
Parse(url::ParseError), ParseError(url::ParseError),
Serialize(serde_json::Error), SerializeError(serde_json::Error),
Reqwest(reqwest::Error), Reqwest(reqwest::Error),
} }
impl std::fmt::Display for CreateError{ impl std::fmt::Display for CreateError{
@ -58,29 +35,24 @@ pub struct UpdateAssetRequest{
} }
//woo nested roblox stuff //woo nested roblox stuff
#[derive(Clone,Debug,serde::Deserialize,serde::Serialize)] #[derive(Debug,serde::Deserialize,serde::Serialize)]
#[allow(nonstandard_style,dead_code)] #[allow(nonstandard_style,dead_code)]
pub enum Creator{ pub struct Creator{
userId(String),//u64 string pub userId:u64,
groupId(String),//u64 string pub groupId:u64,
} }
#[derive(Debug,serde::Deserialize,serde::Serialize)] #[derive(Debug,serde::Deserialize,serde::Serialize)]
#[allow(nonstandard_style,dead_code)] #[allow(nonstandard_style,dead_code)]
pub struct CreationContext{ pub struct CreationContext{
pub creator:Creator, pub creator:Creator,
pub expectedPrice:Option<u64>, pub expectedPrice:u64,
} }
#[derive(Debug,serde::Deserialize,serde::Serialize)] #[derive(Debug,serde::Deserialize,serde::Serialize)]
#[allow(nonstandard_style,dead_code)] #[allow(nonstandard_style,dead_code)]
pub enum ModerationState{ pub enum ModerationResult{
Reviewing, MODERATION_STATE_REVIEWING,
Rejected, MODERATION_STATE_REJECTED,
Approved, MODERATION_STATE_APPROVED,
}
#[derive(Debug,serde::Deserialize,serde::Serialize)]
#[allow(nonstandard_style,dead_code)]
pub struct ModerationResult{
pub moderationState:ModerationState,
} }
#[derive(Debug,serde::Deserialize,serde::Serialize)] #[derive(Debug,serde::Deserialize,serde::Serialize)]
#[allow(nonstandard_style,dead_code)] #[allow(nonstandard_style,dead_code)]
@ -105,59 +77,12 @@ pub enum UpdateError{
Reqwest(reqwest::Error), Reqwest(reqwest::Error),
} }
impl std::fmt::Display for UpdateError{ impl std::fmt::Display for UpdateError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f,"{self:?}") write!(f,"{self:?}")
} }
} }
impl std::error::Error for UpdateError{} impl std::error::Error for UpdateError{}
pub struct GetAssetOperationRequest{
pub operation_id:String,
}
pub struct GetAssetInfoRequest{
pub asset_id:u64,
}
/*
{
"assetId": "5692158972",
"assetType": "Model",
"creationContext":{
"creator":
{
"groupId": "6980477"
}
},
"description": "DisplayName: Ares\nCreator: titanicguy54",
"displayName": "bhop_ares.rbxmx",
"path": "assets/5692158972",
"revisionCreateTime": "2020-09-14T16:08:05.063Z",
"revisionId": "1",
"moderationResult":{
"moderationState": "Approved"
},
"state": "Active"
}
*/
#[derive(Debug,serde::Deserialize,serde::Serialize)]
#[allow(nonstandard_style,dead_code)]
pub struct AssetResponse{
pub assetId:String,//u64 wrapped in quotes wohoo!!
pub assetType:AssetType,
pub creationContext:CreationContext,
pub description: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>>,
}
#[allow(nonstandard_style,dead_code)]
pub struct GetAssetVersionRequest{
pub asset_id:u64,
pub version:u64,
}
#[allow(nonstandard_style,dead_code)] #[allow(nonstandard_style,dead_code)]
pub struct GetAssetRequest{ pub struct GetAssetRequest{
pub asset_id:u64, pub asset_id:u64,
@ -170,7 +95,7 @@ pub enum GetError{
IO(std::io::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 {
write!(f,"{self:?}") write!(f,"{self:?}")
} }
} }
@ -205,7 +130,7 @@ pub enum AssetVersionsError{
Reqwest(reqwest::Error), Reqwest(reqwest::Error),
} }
impl std::fmt::Display for AssetVersionsError{ impl std::fmt::Display for AssetVersionsError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f,"{self:?}") write!(f,"{self:?}")
} }
} }
@ -240,52 +165,20 @@ pub enum InventoryPageError{
Reqwest(reqwest::Error), Reqwest(reqwest::Error),
} }
impl std::fmt::Display for InventoryPageError{ impl std::fmt::Display for InventoryPageError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f,"{self:?}") write!(f,"{self:?}")
} }
} }
impl std::error::Error for InventoryPageError{} impl std::error::Error for InventoryPageError{}
#[derive(Debug)]
pub enum OperationError{
Get(GetError),
NoOperationId,
NotDone,
}
impl std::fmt::Display for OperationError{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f,"{self:?}")
}
}
impl std::error::Error for OperationError{}
#[derive(Debug,serde::Deserialize,serde::Serialize)] #[derive(Debug,serde::Deserialize,serde::Serialize)]
#[allow(nonstandard_style,dead_code)] #[allow(nonstandard_style,dead_code)]
pub struct RobloxOperation{ pub struct RobloxOperation{
pub path:Option<String>, pub path:Option<std::path::PathBuf>,
pub metadata:Option<String>, pub metadata:Option<String>,
pub done:Option<bool>, pub done:Option<bool>,
pub error:Option<String>, pub error:Option<String>,
pub response:Option<serde_json::Value>, pub response:Option<String>,
pub operationId:Option<String>,
}
impl RobloxOperation{
pub fn operation_id(&self)->Option<&str>{
match self.operationId.as_deref(){
//try getting it from undocumented operationId first
Some(operation_id)=>Some(operation_id),
//skip the first 11 characters
//operations/[uuid]
None=>self.path.as_deref()?.get(11..),
}
}
pub async fn try_get_reponse(&self,context:&CloudContext)->Result<serde_json::Value,OperationError>{
context.get_asset_operation(GetAssetOperationRequest{
operation_id:self.operation_id()
.ok_or(OperationError::NoOperationId)?
.to_owned(),
}).await.map_err(OperationError::Get)?
.response.ok_or(OperationError::NotDone)
}
} }
//idk how to do this better //idk how to do this better
@ -354,26 +247,19 @@ impl CloudContext{
.multipart(form) .multipart(form)
.send().await .send().await
} }
pub async fn create_asset(&self,config:CreateAssetRequest,body:impl Into<std::borrow::Cow<'static,[u8]>>)->Result<CreateAssetResponse,CreateError>{ pub async fn create_asset(&self,config:CreateAssetRequest,body:impl Into<std::borrow::Cow<'static,[u8]>>)->Result<RobloxOperation,CreateError>{
let url=reqwest::Url::parse("https://apis.roblox.com/assets/v1/assets").map_err(CreateError::Parse)?; let url=reqwest::Url::parse("https://apis.roblox.com/assets/v1/assets").map_err(CreateError::ParseError)?;
let request_config=serde_json::to_string(&config).map_err(CreateError::Serialize)?; let request_config=serde_json::to_string(&config).map_err(CreateError::SerializeError)?;
let part=reqwest::multipart::Part::bytes(body)
//you must have a file name or roblox will 400!!!!!!!!!
.file_name("image");
let form=reqwest::multipart::Form::new() let form=reqwest::multipart::Form::new()
.text("request",request_config) .text("request",request_config)
.part("fileContent",part); .part("fileContent",reqwest::multipart::Part::bytes(body));
let operation=self.post_form(url,form).await.map_err(CreateError::Reqwest)? let resp=self.post_form(url,form).await.map_err(CreateError::Reqwest)?
.error_for_status().map_err(CreateError::Reqwest)? .error_for_status().map_err(CreateError::Reqwest)?;
.json::<RobloxOperation>().await.map_err(CreateError::Reqwest)?;
Ok(CreateAssetResponse{ Ok(resp.json::<RobloxOperation>().await.map_err(CreateError::Reqwest)?)
operation,
})
} }
pub async fn update_asset(&self,config:UpdateAssetRequest,body:impl Into<std::borrow::Cow<'static,[u8]>>)->Result<RobloxOperation,UpdateError>{ pub async fn update_asset(&self,config:UpdateAssetRequest,body:impl Into<std::borrow::Cow<'static,[u8]>>)->Result<RobloxOperation,UpdateError>{
let raw_url=format!("https://apis.roblox.com/assets/v1/assets/{}",config.assetId); let raw_url=format!("https://apis.roblox.com/assets/v1/assets/{}",config.assetId);
@ -385,35 +271,26 @@ impl CloudContext{
.text("request",request_config) .text("request",request_config)
.part("fileContent",reqwest::multipart::Part::bytes(body)); .part("fileContent",reqwest::multipart::Part::bytes(body));
self.patch_form(url,form).await let resp=self.patch_form(url,form).await
.map_err(UpdateError::Reqwest)? .map_err(UpdateError::Reqwest)?
//roblox api documentation is very poor, just give the status code and drop the json //roblox api documentation is very poor, just give the status code and drop the json
.error_for_status().map_err(UpdateError::Reqwest)? .error_for_status().map_err(UpdateError::Reqwest)?;
.json::<RobloxOperation>().await.map_err(UpdateError::Reqwest)
}
pub async fn get_asset_operation(&self,config:GetAssetOperationRequest)->Result<RobloxOperation,GetError>{
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)?;
self.get(url).await.map_err(GetError::Reqwest)? Ok(resp.json::<RobloxOperation>().await.map_err(UpdateError::Reqwest)?)
.error_for_status().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(&self,config:GetAssetRequest)->Result<Vec<u8>,GetError>{
let raw_url=format!("https://apis.roblox.com/assets/v1/assets/{}",config.asset_id); let mut url=reqwest::Url::parse("https://assetdelivery.roblox.com/v1/asset/").map_err(GetError::ParseError)?;
let url=reqwest::Url::parse(raw_url.as_str()).map_err(GetError::ParseError)?; //url borrow scope
{
let mut query=url.query_pairs_mut();//borrow here
query.append_pair("ID",config.asset_id.to_string().as_str());
if let Some(version)=config.version{
query.append_pair("version",version.to_string().as_str());
}
}
let resp=self.get(url).await.map_err(GetError::Reqwest)?;
self.get(url).await.map_err(GetError::Reqwest)? let body=resp.bytes().await.map_err(GetError::Reqwest)?;
.error_for_status().map_err(GetError::Reqwest)?
.json::<AssetResponse>().await.map_err(GetError::Reqwest)
}
pub async fn get_asset_version(&self,config:GetAssetVersionRequest)->Result<Vec<u8>,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)?;
let body=self.get(url).await.map_err(GetError::Reqwest)?
.error_for_status().map_err(GetError::Reqwest)?
.bytes().await.map_err(GetError::Reqwest)?;
match maybe_gzip_decode(&mut std::io::Cursor::new(body)){ match maybe_gzip_decode(&mut std::io::Cursor::new(body)){
Ok(ReaderType::GZip(readable))=>read_readable(readable), Ok(ReaderType::GZip(readable))=>read_readable(readable),
@ -421,23 +298,12 @@ impl CloudContext{
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)?;
self.get(url).await.map_err(AssetVersionsError::Reqwest)? Ok(self.get(url).await.map_err(AssetVersionsError::Reqwest)?
.error_for_status().map_err(AssetVersionsError::Reqwest)? .json::<AssetVersionsResponse>().await.map_err(AssetVersionsError::Reqwest)?)
.json::<AssetVersionsResponse>().await.map_err(AssetVersionsError::Reqwest)
} }
pub async fn inventory_page(&self,config:InventoryPageRequest)->Result<InventoryPageResponse,InventoryPageError>{ pub async fn inventory_page(&self,config:InventoryPageRequest)->Result<InventoryPageResponse,InventoryPageError>{
let mut url=reqwest::Url::parse(format!("https://apis.roblox.com/toolbox-service/v1/creations/group/{}/10?limit=50",config.group).as_str()).map_err(InventoryPageError::ParseError)?; let mut url=reqwest::Url::parse(format!("https://apis.roblox.com/toolbox-service/v1/creations/group/{}/10?limit=50",config.group).as_str()).map_err(InventoryPageError::ParseError)?;
@ -449,9 +315,8 @@ impl CloudContext{
} }
} }
self.get(url).await.map_err(InventoryPageError::Reqwest)? Ok(self.get(url).await.map_err(InventoryPageError::Reqwest)?
.error_for_status().map_err(InventoryPageError::Reqwest)? .json::<InventoryPageResponse>().await.map_err(InventoryPageError::Reqwest)?)
.json::<InventoryPageResponse>().await.map_err(InventoryPageError::Reqwest)
} }
pub async fn update_place(&self,config:UpdatePlaceRequest,body:impl Into<reqwest::Body>+Clone)->Result<UpdatePlaceResponse,UpdateError>{ pub async fn update_place(&self,config:UpdatePlaceRequest,body:impl Into<reqwest::Body>+Clone)->Result<UpdatePlaceResponse,UpdateError>{
let raw_url=format!("https://apis.roblox.com/universes/v1/{}/places/{}/versions",config.universeId,config.placeId); let raw_url=format!("https://apis.roblox.com/universes/v1/{}/places/{}/versions",config.universeId,config.placeId);
@ -462,8 +327,9 @@ impl CloudContext{
query.append_pair("versionType","Published"); query.append_pair("versionType","Published");
} }
self.post(url,body).await.map_err(UpdateError::Reqwest)? let resp=self.post(url,body).await.map_err(UpdateError::Reqwest)?
.error_for_status().map_err(UpdateError::Reqwest)? .error_for_status().map_err(UpdateError::Reqwest)?;
.json::<UpdatePlaceResponse>().await.map_err(UpdateError::Reqwest)
Ok(resp.json::<UpdatePlaceResponse>().await.map_err(UpdateError::Reqwest)?)
} }
} }

@ -228,14 +228,15 @@ impl CookieContext{
query.append_pair("description",config.description.as_str()); query.append_pair("description",config.description.as_str());
query.append_pair("ispublic",if config.ispublic{"True"}else{"False"}); query.append_pair("ispublic",if config.ispublic{"True"}else{"False"});
query.append_pair("allowComments",if config.allowComments{"True"}else{"False"}); query.append_pair("allowComments",if config.allowComments{"True"}else{"False"});
if let Some(group_id)=config.groupId{ match config.groupId{
query.append_pair("groupId",group_id.to_string().as_str()); Some(group_id)=>{query.append_pair("groupId",group_id.to_string().as_str());},
None=>(),
} }
} }
self.post(url,body).await.map_err(CreateError::PostError)? let resp=self.post(url,body).await.map_err(CreateError::PostError)?;
.error_for_status().map_err(CreateError::Reqwest)?
.json::<UploadResponse>().await.map_err(CreateError::Reqwest) Ok(resp.json::<UploadResponse>().await.map_err(CreateError::Reqwest)?)
} }
pub async fn upload(&self,config:UploadRequest,body:impl Into<reqwest::Body>+Clone)->Result<UploadResponse,UploadError>{ pub async fn upload(&self,config:UploadRequest,body:impl Into<reqwest::Body>+Clone)->Result<UploadResponse,UploadError>{
let mut url=reqwest::Url::parse("https://data.roblox.com/Data/Upload.ashx?json=1&type=Model&genreTypeId=1").map_err(UploadError::ParseError)?; let mut url=reqwest::Url::parse("https://data.roblox.com/Data/Upload.ashx?json=1&type=Model&genreTypeId=1").map_err(UploadError::ParseError)?;
@ -264,9 +265,9 @@ impl CookieContext{
} }
} }
self.post(url,body).await.map_err(UploadError::PostError)? let resp=self.post(url,body).await.map_err(UploadError::PostError)?;
.error_for_status().map_err(UploadError::Reqwest)?
.json::<UploadResponse>().await.map_err(UploadError::Reqwest) Ok(resp.json::<UploadResponse>().await.map_err(UploadError::Reqwest)?)
} }
pub async fn get_asset(&self,config:GetAssetRequest)->Result<Vec<u8>,GetError>{ pub async fn get_asset(&self,config:GetAssetRequest)->Result<Vec<u8>,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)?;
@ -278,9 +279,9 @@ impl CookieContext{
query.append_pair("version",version.to_string().as_str()); query.append_pair("version",version.to_string().as_str());
} }
} }
let body=self.get(url).await.map_err(GetError::Reqwest)? let resp=self.get(url).await.map_err(GetError::Reqwest)?;
.error_for_status().map_err(GetError::Reqwest)?
.bytes().await.map_err(GetError::Reqwest)?; let body=resp.bytes().await.map_err(GetError::Reqwest)?;
match maybe_gzip_decode(&mut std::io::Cursor::new(body)){ match maybe_gzip_decode(&mut std::io::Cursor::new(body)){
Ok(ReaderType::GZip(readable))=>read_readable(readable), Ok(ReaderType::GZip(readable))=>read_readable(readable),
@ -301,9 +302,8 @@ impl CookieContext{
} }
} }
self.get(url).await.map_err(AssetVersionsPageError::Reqwest)? Ok(self.get(url).await.map_err(AssetVersionsPageError::Reqwest)?
.error_for_status().map_err(AssetVersionsPageError::Reqwest)? .json::<AssetVersionsPageResponse>().await.map_err(AssetVersionsPageError::Reqwest)?)
.json::<AssetVersionsPageResponse>().await.map_err(AssetVersionsPageError::Reqwest)
} }
pub async fn get_inventory_page(&self,config:InventoryPageRequest)->Result<InventoryPageResponse,InventoryPageError>{ pub async fn get_inventory_page(&self,config:InventoryPageRequest)->Result<InventoryPageResponse,InventoryPageError>{
let mut url=reqwest::Url::parse(format!("https://apis.roblox.com/toolbox-service/v1/creations/group/{}/10?limit=50",config.group).as_str()).map_err(InventoryPageError::ParseError)?; let mut url=reqwest::Url::parse(format!("https://apis.roblox.com/toolbox-service/v1/creations/group/{}/10?limit=50",config.group).as_str()).map_err(InventoryPageError::ParseError)?;
@ -315,8 +315,7 @@ impl CookieContext{
} }
} }
self.get(url).await.map_err(InventoryPageError::Reqwest)? Ok(self.get(url).await.map_err(InventoryPageError::Reqwest)?
.error_for_status().map_err(InventoryPageError::Reqwest)? .json::<InventoryPageResponse>().await.map_err(InventoryPageError::Reqwest)?)
.json::<InventoryPageResponse>().await.map_err(InventoryPageError::Reqwest)
} }
} }

@ -28,6 +28,6 @@ impl std::fmt::Display for PropertiesOverride{
} }
} }
pub(crate) fn sanitize(s:&str)->std::borrow::Cow<'_,str>{ pub(crate) fn sanitize<'a>(s:&'a str)->std::borrow::Cow<'a,str>{
lazy_regex::regex!(r"[^A-Za-z0-9.-]").replace_all(s,"_") lazy_regex::regex!(r"[^A-Za-z0-9.-]").replace_all(s,"_")
} }

@ -1,4 +1,4 @@
use std::path::{Path,PathBuf}; use std::path::PathBuf;
use futures::{StreamExt, TryStreamExt}; use futures::{StreamExt, TryStreamExt};
use tokio::io::AsyncReadExt; use tokio::io::AsyncReadExt;
@ -55,9 +55,9 @@ struct QuerySingle{
script:QueryHandle, script:QueryHandle,
} }
impl QuerySingle{ impl QuerySingle{
fn rox(search_path:&Path,search_name:&str)->Self{ fn rox(search_path:&PathBuf,search_name:&str)->Self{
Self{ Self{
script:tokio::spawn(get_file_async(search_path.to_owned(),format!("{}.lua",search_name))) script:tokio::spawn(get_file_async(search_path.clone(),format!("{}.lua",search_name)))
} }
} }
} }
@ -76,7 +76,7 @@ struct QueryTriple{
client:QueryHandle, client:QueryHandle,
} }
impl QueryTriple{ impl QueryTriple{
fn rox_rojo(search_path:&Path,search_name:&str,search_module:bool)->Self{ fn rox_rojo(search_path:&PathBuf,search_name:&str,search_module:bool)->Self{
//this should be implemented as constructors of Triplet and Quadruplet to fully support Trey's suggestion //this should be implemented as constructors of Triplet and Quadruplet to fully support Trey's suggestion
let module_name=if search_module{ let module_name=if search_module{
format!("{}.module.lua",search_name) format!("{}.module.lua",search_name)
@ -84,12 +84,12 @@ impl QueryTriple{
format!("{}.lua",search_name) format!("{}.lua",search_name)
}; };
Self{ Self{
module:tokio::spawn(get_file_async(search_path.to_owned(),module_name)), module:tokio::spawn(get_file_async(search_path.clone(),module_name)),
server:tokio::spawn(get_file_async(search_path.to_owned(),format!("{}.server.lua",search_name))), server:tokio::spawn(get_file_async(search_path.clone(),format!("{}.server.lua",search_name))),
client:tokio::spawn(get_file_async(search_path.to_owned(),format!("{}.client.lua",search_name))), client:tokio::spawn(get_file_async(search_path.clone(),format!("{}.client.lua",search_name))),
} }
} }
fn rojo(search_path:&Path)->Self{ fn rojo(search_path:&PathBuf)->Self{
QueryTriple::rox_rojo(search_path,"init",false) QueryTriple::rox_rojo(search_path,"init",false)
} }
} }
@ -146,9 +146,9 @@ impl Query for QueryTriple{
async fn resolve(self)->QueryHintResult{ async fn resolve(self)->QueryHintResult{
let (module,server,client)=tokio::join!(self.module,self.server,self.client); let (module,server,client)=tokio::join!(self.module,self.server,self.client);
mega_triple_join(( mega_triple_join((
module.map_err(QueryResolveError::JoinError)?.map(|file|FileHint{file,hint:ScriptHint::ModuleScript}), module.map_err(|e|QueryResolveError::JoinError(e))?.map(|file|FileHint{file,hint:ScriptHint::ModuleScript}),
server.map_err(QueryResolveError::JoinError)?.map(|file|FileHint{file,hint:ScriptHint::Script}), server.map_err(|e|QueryResolveError::JoinError(e))?.map(|file|FileHint{file,hint:ScriptHint::Script}),
client.map_err(QueryResolveError::JoinError)?.map(|file|FileHint{file,hint:ScriptHint::LocalScript}), client.map_err(|e|QueryResolveError::JoinError(e))?.map(|file|FileHint{file,hint:ScriptHint::LocalScript}),
)) ))
} }
} }
@ -159,7 +159,7 @@ struct QueryQuad{
client:QueryHandle, client:QueryHandle,
} }
impl QueryQuad{ impl QueryQuad{
fn rox_rojo(search_path:&Path,search_name:&str)->Self{ fn rox_rojo(search_path:&PathBuf,search_name:&str)->Self{
let fill=QueryTriple::rox_rojo(search_path,search_name,true); let fill=QueryTriple::rox_rojo(search_path,search_name,true);
Self{ Self{
module_implicit:QuerySingle::rox(search_path,search_name).script,//Script.lua module_implicit:QuerySingle::rox(search_path,search_name).script,//Script.lua
@ -173,10 +173,10 @@ impl Query for QueryQuad{
async fn resolve(self)->QueryHintResult{ async fn resolve(self)->QueryHintResult{
let (module_implicit,module_explicit,server,client)=tokio::join!(self.module_implicit,self.module_explicit,self.server,self.client); let (module_implicit,module_explicit,server,client)=tokio::join!(self.module_implicit,self.module_explicit,self.server,self.client);
mega_quadruple_join(( mega_quadruple_join((
module_implicit.map_err(QueryResolveError::JoinError)?.map(|file|FileHint{file,hint:ScriptHint::ModuleScript}), module_implicit.map_err(|e|QueryResolveError::JoinError(e))?.map(|file|FileHint{file,hint:ScriptHint::ModuleScript}),
module_explicit.map_err(QueryResolveError::JoinError)?.map(|file|FileHint{file,hint:ScriptHint::ModuleScript}), module_explicit.map_err(|e|QueryResolveError::JoinError(e))?.map(|file|FileHint{file,hint:ScriptHint::ModuleScript}),
server.map_err(QueryResolveError::JoinError)?.map(|file|FileHint{file,hint:ScriptHint::Script}), server.map_err(|e|QueryResolveError::JoinError(e))?.map(|file|FileHint{file,hint:ScriptHint::Script}),
client.map_err(QueryResolveError::JoinError)?.map(|file|FileHint{file,hint:ScriptHint::LocalScript}), client.map_err(|e|QueryResolveError::JoinError(e))?.map(|file|FileHint{file,hint:ScriptHint::LocalScript}),
)) ))
} }
} }
@ -338,7 +338,10 @@ impl CompileNode{
.into_string() .into_string()
.map_err(CompileNodeError::FileName)?; .map_err(CompileNodeError::FileName)?;
//reject goobers //reject goobers
let is_goober=matches!(style,Some(Style::Rojo)); let is_goober=match style{
Some(Style::Rojo)=>true,
_=>false,
};
let (ext_len,file_discernment)={ let (ext_len,file_discernment)={
if let Some(captures)=lazy_regex::regex!(r"^.*(\.module\.lua|\.client\.lua|\.server\.lua)$") if let Some(captures)=lazy_regex::regex!(r"^.*(\.module\.lua|\.client\.lua|\.server\.lua)$")
.captures(file_name.as_str()){ .captures(file_name.as_str()){
@ -436,7 +439,7 @@ impl std::error::Error for CompileError{}
pub async fn compile(config:CompileConfig,mut dom:&mut rbx_dom_weak::WeakDom)->Result<(),CompileError>{ pub async fn compile(config:CompileConfig,mut dom:&mut rbx_dom_weak::WeakDom)->Result<(),CompileError>{
//hack to traverse root folder as the root object //hack to traverse root folder as the root object
"src".clone_into(&mut dom.root_mut().name); dom.root_mut().name="src".to_owned();
//add in scripts and models //add in scripts and models
let mut folder=config.input_folder.clone(); let mut folder=config.input_folder.clone();
let mut stack:Vec<CompileStackInstruction>=vec![CompileStackInstruction::TraverseReferent(dom.root_ref(),None)]; let mut stack:Vec<CompileStackInstruction>=vec![CompileStackInstruction::TraverseReferent(dom.root_ref(),None)];
@ -456,9 +459,9 @@ pub async fn compile(config:CompileConfig,mut dom:&mut rbx_dom_weak::WeakDom)->R
let mut exist_names:std::collections::HashSet<String>={ let mut exist_names:std::collections::HashSet<String>={
let item=dom.get_by_ref(item_ref).ok_or(CompileError::NullChildRef)?; let item=dom.get_by_ref(item_ref).ok_or(CompileError::NullChildRef)?;
//push existing dom children objects onto stack (unrelated to exist_names) //push existing dom children objects onto stack (unrelated to exist_names)
stack.extend(item.children().iter().map(|&referent|CompileStackInstruction::TraverseReferent(referent,None))); stack.extend(item.children().into_iter().map(|&referent|CompileStackInstruction::TraverseReferent(referent,None)));
//get names of existing objects //get names of existing objects
item.children().iter().map(|&child_ref|{ item.children().into_iter().map(|&child_ref|{
let child=dom.get_by_ref(child_ref).ok_or(CompileError::NullChildRef)?; let child=dom.get_by_ref(child_ref).ok_or(CompileError::NullChildRef)?;
Ok::<_,CompileError>(sanitize(child.name.as_str()).to_string()) Ok::<_,CompileError>(sanitize(child.name.as_str()).to_string())
}).collect::<Result<_,CompileError>>()? }).collect::<Result<_,CompileError>>()?
@ -475,7 +478,7 @@ pub async fn compile(config:CompileConfig,mut dom:&mut rbx_dom_weak::WeakDom)->R
let ret1={ let ret1={
//capture a scoped mutable reference so we can forward dir to the next call even on an error //capture a scoped mutable reference so we can forward dir to the next call even on an error
let dir2=&mut dir1; let dir2=&mut dir1;
async move{//error catcher so I can use ? (||async move{//error catcher so I can use ?
let ret2=if let Some(entry)=dir2.next_entry().await?{ let ret2=if let Some(entry)=dir2.next_entry().await?{
//cull early even if supporting things with identical names is possible //cull early even if supporting things with identical names is possible
if exist_names.contains(entry.file_name().to_str().unwrap()){ if exist_names.contains(entry.file_name().to_str().unwrap()){
@ -487,7 +490,7 @@ pub async fn compile(config:CompileConfig,mut dom:&mut rbx_dom_weak::WeakDom)->R
TooComplicated::Stop TooComplicated::Stop
}; };
Ok(ret2) Ok(ret2)
}.await })().await
}; };
match ret1{ match ret1{
Ok(TooComplicated::Stop)=>None, Ok(TooComplicated::Stop)=>None,

@ -147,7 +147,10 @@ impl DecompiledContext{
"Model"=>Class::Model, "Model"=>Class::Model,
_=>Class::Folder, _=>Class::Folder,
}; };
let skip=class==Class::Model; let skip=match class{
Class::Model=>true,
_=>false,
};
if let Some(parent_node)=tree_refs.get_mut(&item.parent()){ if let Some(parent_node)=tree_refs.get_mut(&item.parent()){
let referent=item.referent(); let referent=item.referent();
let node=TreeNode::new(item.name.clone(),referent,parent_node.referent,class); let node=TreeNode::new(item.name.clone(),referent,parent_node.referent,class);
@ -179,14 +182,14 @@ impl DecompiledContext{
if node.class==Class::Folder&&script_count!=0{ if node.class==Class::Folder&&script_count!=0{
node.class=Class::Model node.class=Class::Model
} }
if node.class==Class::Folder&&node.children.is_empty(){ if node.class==Class::Folder&&node.children.len()==0{
delete=Some(node.parent); delete=Some(node.parent);
}else{ }else{
//how the hell do I do this better without recursion //how the hell do I do this better without recursion
let is_script=matches!( let is_script=match node.class{
node.class, Class::ModuleScript|Class::LocalScript|Class::Script=>true,
Class::ModuleScript|Class::LocalScript|Class::Script _=>false,
); };
//stack is popped from back //stack is popped from back
if is_script{ if is_script{
stack.push(TrimStackInstruction::DecrementScript); stack.push(TrimStackInstruction::DecrementScript);
@ -234,7 +237,7 @@ impl DecompiledContext{
WriteStackInstruction::Node(node,name_count)=>{ WriteStackInstruction::Node(node,name_count)=>{
//track properties that must be overriden to compile folder structure back into a place file //track properties that must be overriden to compile folder structure back into a place file
let mut properties=PropertiesOverride::default(); let mut properties=PropertiesOverride::default();
let has_children=node.children.is_empty(); let has_children=node.children.len()!=0;
match node.class{ match node.class{
Class::Folder=>(), Class::Folder=>(),
Class::ModuleScript=>(),//.lua files are ModuleScript by default Class::ModuleScript=>(),//.lua files are ModuleScript by default
@ -294,7 +297,7 @@ impl DecompiledContext{
let write_models=config.write_models; let write_models=config.write_models;
let write_scripts=config.write_scripts; let write_scripts=config.write_scripts;
let results:Vec<Result<(),WriteError>>=rayon::iter::ParallelIterator::collect(rayon::iter::ParallelIterator::map(rayon::iter::IntoParallelIterator::into_par_iter(write_queue),|(write_path,node,node_name_override,properties,style)|{ let results:Vec<Result<(),WriteError>>=rayon::iter::ParallelIterator::collect(rayon::iter::ParallelIterator::map(rayon::iter::IntoParallelIterator::into_par_iter(write_queue),|(write_path,node,node_name_override,properties,style)|{
write_item(dom,write_path,node,node_name_override,properties,style,write_models,write_scripts) write_item(&dom,write_path,node,node_name_override,properties,style,write_models,write_scripts)
})); }));
for result in results{ for result in results{
result?; result?;

@ -1,6 +1,6 @@
use std::{io::Read,path::PathBuf}; use std::{io::Read,path::PathBuf};
use clap::{Args,Parser,Subcommand}; use clap::{Args,Parser,Subcommand};
use anyhow::{anyhow,Result as AResult}; use anyhow::Result as AResult;
use futures::StreamExt; use futures::StreamExt;
use rbx_asset::cloud::{ApiKey,CloudContext}; use rbx_asset::cloud::{ApiKey,CloudContext};
use rbx_asset::cookie::{Cookie,CookieContext,AssetVersion,InventoryItem}; use rbx_asset::cookie::{Cookie,CookieContext,AssetVersion,InventoryItem};
@ -20,15 +20,13 @@ struct Cli{
#[derive(Subcommand)] #[derive(Subcommand)]
enum Commands{ enum Commands{
Info(InfoSubcommand),
DownloadHistory(DownloadHistorySubcommand), DownloadHistory(DownloadHistorySubcommand),
Download(DownloadSubcommand), Download(DownloadSubcommand),
DownloadDecompile(DownloadDecompileSubcommand), DownloadDecompile(DownloadDecompileSubcommand),
DownloadGroupInventoryJson(DownloadGroupInventoryJsonSubcommand), DownloadGroupInventoryJson(DownloadGroupInventoryJsonSubcommand),
CreateAsset(CreateAssetSubcommand), CreateAsset(CreateAssetSubcommand),
CreateAssetMedia(CreateAssetMediaSubcommand),
CreateAssetMedias(CreateAssetMediasSubcommand),
UploadAsset(UpdateAssetSubcommand), UploadAsset(UpdateAssetSubcommand),
UploadAssetMedia(UpdateAssetMediaSubcommand),
UploadPlace(UpdatePlaceSubcommand), UploadPlace(UpdatePlaceSubcommand),
Compile(CompileSubcommand), Compile(CompileSubcommand),
CompileUploadAsset(CompileUploadAssetSubcommand), CompileUploadAsset(CompileUploadAssetSubcommand),
@ -37,6 +35,11 @@ enum Commands{
DecompileHistoryIntoGit(DecompileHistoryIntoGitSubcommand), DecompileHistoryIntoGit(DecompileHistoryIntoGitSubcommand),
DownloadAndDecompileHistoryIntoGit(DownloadAndDecompileHistoryIntoGitSubcommand), DownloadAndDecompileHistoryIntoGit(DownloadAndDecompileHistoryIntoGitSubcommand),
} }
#[derive(Args)]
struct InfoSubcommand{
#[arg(long)]
path:PathBuf,
}
#[derive(Args)] #[derive(Args)]
struct DownloadHistorySubcommand{ struct DownloadHistorySubcommand{
@ -59,12 +62,12 @@ struct DownloadHistorySubcommand{
} }
#[derive(Args)] #[derive(Args)]
struct DownloadSubcommand{ struct DownloadSubcommand{
#[arg(long,group="cookie",required=true)] #[arg(long,group="api_key",required=true)]
cookie_literal:Option<String>, api_key_literal:Option<String>,
#[arg(long,group="cookie",required=true)] #[arg(long,group="api_key",required=true)]
cookie_envvar:Option<String>, api_key_envvar:Option<String>,
#[arg(long,group="cookie",required=true)] #[arg(long,group="api_key",required=true)]
cookie_file:Option<PathBuf>, api_key_file:Option<PathBuf>,
#[arg(long)] #[arg(long)]
output_folder:Option<PathBuf>, output_folder:Option<PathBuf>,
#[arg(required=true)] #[arg(required=true)]
@ -85,27 +88,6 @@ struct DownloadGroupInventoryJsonSubcommand{
} }
#[derive(Args)] #[derive(Args)]
struct CreateAssetSubcommand{ struct CreateAssetSubcommand{
#[arg(long,group="cookie",required=true)]
cookie_literal:Option<String>,
#[arg(long,group="cookie",required=true)]
cookie_envvar:Option<String>,
#[arg(long,group="cookie",required=true)]
cookie_file:Option<PathBuf>,
#[arg(long)]
group_id:Option<u64>,
#[arg(long)]
input_file:PathBuf,
#[arg(long)]
model_name:String,
#[arg(long)]
description:Option<String>,
#[arg(long)]
free_model:Option<bool>,
#[arg(long)]
allow_comments:Option<bool>,
}
#[derive(Args)]
struct CreateAssetMediaSubcommand{
#[arg(long,group="api_key",required=true)] #[arg(long,group="api_key",required=true)]
api_key_literal:Option<String>, api_key_literal:Option<String>,
#[arg(long,group="api_key",required=true)] #[arg(long,group="api_key",required=true)]
@ -119,66 +101,12 @@ struct CreateAssetMediaSubcommand{
#[arg(long)] #[arg(long)]
input_file:PathBuf, input_file:PathBuf,
#[arg(long)] #[arg(long)]
asset_type:AssetType, creator_user_id:u64,
#[arg(long,group="creator",required=true)] #[arg(long)]
creator_user_id:Option<u64>,
#[arg(long,group="creator",required=true)]
creator_group_id:Option<u64>, creator_group_id:Option<u64>,
/// Expected price limits how much robux can be spent to create the asset (defaults to 0)
#[arg(long)]
expected_price:Option<u64>,
}
#[derive(Args)]
/// Automatically detect the media type from file extension and generate asset name and description
struct CreateAssetMediasSubcommand{
#[arg(long,group="api_key",required=true)]
api_key_literal:Option<String>,
#[arg(long,group="api_key",required=true)]
api_key_envvar:Option<String>,
#[arg(long,group="api_key",required=true)]
api_key_file:Option<PathBuf>,
#[arg(long,group="cookie",required=true)]
cookie_literal:Option<String>,
#[arg(long,group="cookie",required=true)]
cookie_envvar:Option<String>,
#[arg(long,group="cookie",required=true)]
cookie_file:Option<PathBuf>,
#[arg(long)]
description:Option<String>,
#[arg(long,group="creator",required=true)]
creator_user_id:Option<u64>,
#[arg(long,group="creator",required=true)]
creator_group_id:Option<u64>,
/// Expected price limits how much robux can be spent to create the asset (defaults to 0)
#[arg(long)]
expected_price:Option<u64>,
input_files:Vec<PathBuf>,
} }
#[derive(Args)] #[derive(Args)]
struct UpdateAssetSubcommand{ struct UpdateAssetSubcommand{
#[arg(long)]
asset_id:AssetID,
#[arg(long,group="cookie",required=true)]
cookie_literal:Option<String>,
#[arg(long,group="cookie",required=true)]
cookie_envvar:Option<String>,
#[arg(long,group="cookie",required=true)]
cookie_file:Option<PathBuf>,
#[arg(long)]
group_id:Option<u64>,
#[arg(long)]
input_file:PathBuf,
#[arg(long)]
change_name:Option<String>,
#[arg(long)]
change_description:Option<String>,
#[arg(long)]
change_free_model:Option<bool>,
#[arg(long)]
change_allow_comments:Option<bool>,
}
#[derive(Args)]
struct UpdateAssetMediaSubcommand{
#[arg(long)] #[arg(long)]
asset_id:AssetID, asset_id:AssetID,
#[arg(long,group="api_key",required=true)] #[arg(long,group="api_key",required=true)]
@ -220,14 +148,12 @@ struct CompileSubcommand{
struct CompileUploadAssetSubcommand{ struct CompileUploadAssetSubcommand{
#[arg(long)] #[arg(long)]
asset_id:AssetID, asset_id:AssetID,
#[arg(long,group="cookie",required=true)] #[arg(long,group="api_key",required=true)]
cookie_literal:Option<String>, api_key_literal:Option<String>,
#[arg(long,group="cookie",required=true)] #[arg(long,group="api_key",required=true)]
cookie_envvar:Option<String>, api_key_envvar:Option<String>,
#[arg(long,group="cookie",required=true)] #[arg(long,group="api_key",required=true)]
cookie_file:Option<PathBuf>, api_key_file:Option<PathBuf>,
#[arg(long)]
group_id:Option<u64>,
#[arg(long)] #[arg(long)]
input_folder:Option<PathBuf>, input_folder:Option<PathBuf>,
#[arg(long)] #[arg(long)]
@ -350,26 +276,23 @@ impl Style{
} }
} }
} }
#[derive(Clone,Copy,Debug,clap::ValueEnum)]
enum AssetType{ async fn info(path:PathBuf)->AResult<()>{
Audio, let dir=std::env::current_dir().unwrap();
Decal, println!("pwd={:?}",dir);
Model, println!("path={path:?}");
} let mut read_dir=tokio::fs::read_dir(path).await?;
impl AssetType{ while let Some(entry)=read_dir.next_entry().await?{
fn cloud(&self)->rbx_asset::cloud::AssetType{ println!("{:?}",entry);
match self{
AssetType::Audio=>rbx_asset::cloud::AssetType::Audio,
AssetType::Decal=>rbx_asset::cloud::AssetType::Decal,
AssetType::Model=>rbx_asset::cloud::AssetType::Model,
}
} }
Ok(())
} }
#[tokio::main] #[tokio::main]
async fn main()->AResult<()>{ async fn main()->AResult<()>{
let cli=Cli::parse(); let cli=Cli::parse();
match cli.command{ match cli.command{
Commands::Info(subcommand)=>info(subcommand.path).await,
Commands::DownloadHistory(subcommand)=>download_history(DownloadHistoryConfig{ Commands::DownloadHistory(subcommand)=>download_history(DownloadHistoryConfig{
continue_from_versions:subcommand.continue_from_versions.unwrap_or(false), continue_from_versions:subcommand.continue_from_versions.unwrap_or(false),
end_version:subcommand.end_version, end_version:subcommand.end_version,
@ -385,10 +308,10 @@ async fn main()->AResult<()>{
Commands::Download(subcommand)=>{ Commands::Download(subcommand)=>{
let output_folder=subcommand.output_folder.unwrap_or_else(||std::env::current_dir().unwrap()); let output_folder=subcommand.output_folder.unwrap_or_else(||std::env::current_dir().unwrap());
download_list( download_list(
cookie_from_args( api_key_from_args(
subcommand.cookie_literal, subcommand.api_key_literal,
subcommand.cookie_envvar, subcommand.api_key_envvar,
subcommand.cookie_file, subcommand.api_key_file,
).await?, ).await?,
subcommand.asset_ids.into_iter().map(|asset_id|{ subcommand.asset_ids.into_iter().map(|asset_id|{
let mut path=output_folder.clone(); let mut path=output_folder.clone();
@ -421,71 +344,19 @@ async fn main()->AResult<()>{
subcommand.group, subcommand.group,
subcommand.output_folder.unwrap_or_else(||std::env::current_dir().unwrap()), subcommand.output_folder.unwrap_or_else(||std::env::current_dir().unwrap()),
).await, ).await,
Commands::CreateAsset(subcommand)=>create_asset(CreateAssetConfig{ Commands::CreateAsset(subcommand)=>create(CreateConfig{
cookie:cookie_from_args(
subcommand.cookie_literal,
subcommand.cookie_envvar,
subcommand.cookie_file,
).await?,
group:subcommand.group_id,
input_file:subcommand.input_file,
model_name:subcommand.model_name,
description:subcommand.description.unwrap_or_else(||String::with_capacity(0)),
free_model:subcommand.free_model.unwrap_or(false),
allow_comments:subcommand.allow_comments.unwrap_or(false),
}).await,
Commands::CreateAssetMedia(subcommand)=>create_asset_media(CreateAssetMediaConfig{
api_key:api_key_from_args( api_key:api_key_from_args(
subcommand.api_key_literal, subcommand.api_key_literal,
subcommand.api_key_envvar, subcommand.api_key_envvar,
subcommand.api_key_file, subcommand.api_key_file,
).await?, ).await?,
creator:match (subcommand.creator_user_id,subcommand.creator_group_id){ creator_user_id:subcommand.creator_user_id,
(Some(user_id),None)=>rbx_asset::cloud::Creator::userId(user_id.to_string()), creator_group_id:subcommand.creator_group_id,
(None,Some(group_id))=>rbx_asset::cloud::Creator::groupId(group_id.to_string()),
other=>Err(anyhow!("Invalid creator {other:?}"))?,
},
input_file:subcommand.input_file, input_file:subcommand.input_file,
asset_type:subcommand.asset_type.cloud(),
model_name:subcommand.model_name, model_name:subcommand.model_name,
description:subcommand.description.unwrap_or_else(||String::with_capacity(0)), description:subcommand.description.unwrap_or_else(||String::with_capacity(0)),
expected_price:subcommand.expected_price,
}).await,
Commands::CreateAssetMedias(subcommand)=>create_asset_medias(CreateAssetMediasConfig{
api_key:api_key_from_args(
subcommand.api_key_literal,
subcommand.api_key_envvar,
subcommand.api_key_file,
).await?,
cookie:cookie_from_args(
subcommand.cookie_literal,
subcommand.cookie_envvar,
subcommand.cookie_file,
).await?,
creator:match (subcommand.creator_user_id,subcommand.creator_group_id){
(Some(user_id),None)=>rbx_asset::cloud::Creator::userId(user_id.to_string()),
(None,Some(group_id))=>rbx_asset::cloud::Creator::groupId(group_id.to_string()),
other=>Err(anyhow!("Invalid creator {other:?}"))?,
},
description:subcommand.description.unwrap_or_else(||String::with_capacity(0)),
input_files:subcommand.input_files,
expected_price:subcommand.expected_price,
}).await, }).await,
Commands::UploadAsset(subcommand)=>upload_asset(UploadAssetConfig{ Commands::UploadAsset(subcommand)=>upload_asset(UploadAssetConfig{
cookie:cookie_from_args(
subcommand.cookie_literal,
subcommand.cookie_envvar,
subcommand.cookie_file,
).await?,
asset_id:subcommand.asset_id,
group_id:subcommand.group_id,
input_file:subcommand.input_file,
change_name:subcommand.change_name,
change_description:subcommand.change_description,
change_free_model:subcommand.change_free_model,
change_allow_comments:subcommand.change_allow_comments,
}).await,
Commands::UploadAssetMedia(subcommand)=>upload_asset_media(UploadAssetMediaConfig{
api_key:api_key_from_args( api_key:api_key_from_args(
subcommand.api_key_literal, subcommand.api_key_literal,
subcommand.api_key_envvar, subcommand.api_key_envvar,
@ -514,13 +385,12 @@ async fn main()->AResult<()>{
input_folder:subcommand.input_folder.unwrap_or_else(||std::env::current_dir().unwrap()), input_folder:subcommand.input_folder.unwrap_or_else(||std::env::current_dir().unwrap()),
template:subcommand.template, template:subcommand.template,
style:subcommand.style.map(|s|s.rox()), style:subcommand.style.map(|s|s.rox()),
cookie:cookie_from_args( api_key:api_key_from_args(
subcommand.cookie_literal, subcommand.api_key_literal,
subcommand.cookie_envvar, subcommand.api_key_envvar,
subcommand.cookie_file, subcommand.api_key_file,
).await?, ).await?,
asset_id:subcommand.asset_id, asset_id:subcommand.asset_id,
group_id:subcommand.group_id,
}).await, }).await,
Commands::CompileUploadPlace(subcommand)=>compile_upload_place(CompileUploadPlaceConfig{ Commands::CompileUploadPlace(subcommand)=>compile_upload_place(CompileUploadPlaceConfig{
input_folder:subcommand.input_folder.unwrap_or_else(||std::env::current_dir().unwrap()), input_folder:subcommand.input_folder.unwrap_or_else(||std::env::current_dir().unwrap()),
@ -575,7 +445,7 @@ async fn cookie_from_args(literal:Option<String>,environment:Option<String>,file
(Some(cookie_literal),None,None)=>cookie_literal, (Some(cookie_literal),None,None)=>cookie_literal,
(None,Some(cookie_environment),None)=>std::env::var(cookie_environment)?, (None,Some(cookie_environment),None)=>std::env::var(cookie_environment)?,
(None,None,Some(cookie_file))=>tokio::fs::read_to_string(cookie_file).await?, (None,None,Some(cookie_file))=>tokio::fs::read_to_string(cookie_file).await?,
_=>Err(anyhow::Error::msg("Illegal cookie argument triple"))?, _=>Err(anyhow::Error::msg("Illegal api key argument triple"))?,
}; };
Ok(Cookie::new(format!(".ROBLOSECURITY={cookie}"))) Ok(Cookie::new(format!(".ROBLOSECURITY={cookie}")))
} }
@ -589,235 +459,40 @@ async fn api_key_from_args(literal:Option<String>,environment:Option<String>,fil
Ok(ApiKey::new(api_key)) Ok(ApiKey::new(api_key))
} }
struct CreateAssetConfig{ struct CreateConfig{
cookie:Cookie,
model_name:String,
description:String,
input_file:PathBuf,
group:Option<u64>,
free_model:bool,
allow_comments:bool,
}
async fn create_asset(config:CreateAssetConfig)->AResult<()>{
let resp=CookieContext::new(config.cookie)
.create(rbx_asset::cookie::CreateRequest{
name:config.model_name,
description:config.description,
ispublic:config.free_model,
allowComments:config.allow_comments,
groupId:config.group,
},tokio::fs::read(config.input_file).await?).await?;
println!("UploadResponse={:?}",resp);
Ok(())
}
struct CreateAssetMediaConfig{
api_key:ApiKey, api_key:ApiKey,
asset_type:rbx_asset::cloud::AssetType,
model_name:String, model_name:String,
description:String, description:String,
input_file:PathBuf, input_file:PathBuf,
creator:rbx_asset::cloud::Creator, creator_user_id:u64,
expected_price:Option<u64>, creator_group_id:Option<u64>,
} }
async fn get_asset_exp_backoff( ///This is hardcoded to create models atm
context:&CloudContext, async fn create(config:CreateConfig)->AResult<()>{
create_asset_response:&rbx_asset::cloud::CreateAssetResponse let resp=CloudContext::new(config.api_key)
)->Result<rbx_asset::cloud::AssetResponse,rbx_asset::cloud::CreateAssetResponseGetAssetError>{
let mut backoff:u64=0;
loop{
match create_asset_response.try_get_asset(&context).await{
//try again when the operation is not done
Err(rbx_asset::cloud::CreateAssetResponseGetAssetError::Operation(rbx_asset::cloud::OperationError::NotDone))=>(),
//return all other results
other_result=>return other_result,
}
let wait=f32::exp(backoff as f32/3.0)*1000f32;
println!("Operation not complete; waiting {:.0}ms...",wait);
tokio::time::sleep(std::time::Duration::from_millis(wait as u64)).await;
backoff+=1;
}
}
async fn create_asset_media(config:CreateAssetMediaConfig)->AResult<()>{
let context=CloudContext::new(config.api_key);
let asset_response=context
.create_asset(rbx_asset::cloud::CreateAssetRequest{ .create_asset(rbx_asset::cloud::CreateAssetRequest{
assetType:config.asset_type, assetType:rbx_asset::cloud::AssetType::Model,
displayName:config.model_name, displayName:config.model_name,
description:config.description, description:config.description,
creationContext:rbx_asset::cloud::CreationContext{ creationContext:rbx_asset::cloud::CreationContext{
creator:config.creator, creator:rbx_asset::cloud::Creator{
expectedPrice:Some(config.expected_price.unwrap_or(0)), userId:config.creator_user_id,
groupId:config.creator_group_id.unwrap_or(0),
},
expectedPrice:0,
} }
},tokio::fs::read(config.input_file).await?).await?; },tokio::fs::read(config.input_file).await?).await?;
//hardcode a 2 second sleep because roblox be slow println!("CreateResponse={:?}",resp);
println!("Asset submitted, waiting 2s...");
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
let asset=get_asset_exp_backoff(&context,&asset_response).await?;
println!("CreateResponse={:?}",asset);
Ok(())
}
// complex operation requires both api key and cookie! how horrible! roblox please fix!
struct CreateAssetMediasConfig{
api_key:ApiKey,
cookie:Cookie,
description:String,
input_files:Vec<PathBuf>,
creator:rbx_asset::cloud::Creator,
expected_price:Option<u64>,
}
#[derive(Debug)]
enum CreateAssetMediasError{
NoFileStem(PathBuf),
UnknownFourCC(Option<Vec<u8>>),
}
impl std::fmt::Display for CreateAssetMediasError{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for CreateAssetMediasError{}
#[derive(Debug)]
enum DownloadDecalError{
ParseInt(std::num::ParseIntError),
Get(rbx_asset::cookie::GetError),
LoadDom(LoadDomError),
NoFirstInstance,
NoTextureProperty,
TexturePropertyInvalid,
}
impl std::fmt::Display for DownloadDecalError{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for DownloadDecalError{}
async fn create_asset_medias(config:CreateAssetMediasConfig)->AResult<()>{
let context=CloudContext::new(config.api_key);
let cookie_context=CookieContext::new(config.cookie);
let expected_price=Some(config.expected_price.unwrap_or(0));
let asset_id_list=futures::stream::iter(config.input_files.into_iter()
//step 1: read file, make create request
.map(|path|{
let description=&config.description;
let creator=&config.creator;
let context=&context;
async move{
let model_name=path.file_stem()
.and_then(std::ffi::OsStr::to_str)
.ok_or(CreateAssetMediasError::NoFileStem(path.clone()))?
.to_owned();
let file=tokio::fs::read(path).await?;
let asset_type=match file.get(0..4){
//png
Some(b"\x89PNG")=>rbx_asset::cloud::AssetType::Decal,
//jpeg
Some(b"\xFF\xD8\xFF\xE0")=>rbx_asset::cloud::AssetType::Decal,
//Some("fbx")=>rbx_asset::cloud::AssetType::Model,
//Some("ogg")=>rbx_asset::cloud::AssetType::Audio,
fourcc=>Err(CreateAssetMediasError::UnknownFourCC(fourcc.map(<[u8]>::to_owned)))?,
};
Ok(context.create_asset(rbx_asset::cloud::CreateAssetRequest{
assetType:asset_type,
displayName:model_name,
description:description.clone(),
creationContext:rbx_asset::cloud::CreationContext{
creator:creator.clone(),
expectedPrice:expected_price,
}
},file).await?)
}
}))
//parallel requests
.buffer_unordered(CONCURRENT_REQUESTS)
//step 2: poll operation until it completes (as fast as possible no exp backoff or anything just hammer roblox)
.filter_map(|create_result:AResult<_>|{
let context=&context;
async{
match create_result{
Ok(create_asset_response)=>match get_asset_exp_backoff(context,&create_asset_response).await{
Ok(asset_response)=>Some(asset_response),
Err(e)=>{
eprintln!("operation error: {}",e);
None
},
},
Err(e)=>{
eprintln!("create_asset error: {}",e);
None
},
}
}
})
//step 3: read decal id from operation and download it
.filter_map(|asset_response|{
let parse_result=asset_response.assetId.parse();
async{
match async{
let file=cookie_context.get_asset(rbx_asset::cookie::GetAssetRequest{
asset_id:parse_result.map_err(DownloadDecalError::ParseInt)?,
version:None,
}).await.map_err(DownloadDecalError::Get)?;
let dom=load_dom(std::io::Cursor::new(file)).map_err(DownloadDecalError::LoadDom)?;
let instance=dom.get_by_ref(
*dom.root().children().first().ok_or(DownloadDecalError::NoFirstInstance)?
).ok_or(DownloadDecalError::NoFirstInstance)?;
match instance.properties.get("Texture").ok_or(DownloadDecalError::NoTextureProperty)?{
rbx_dom_weak::types::Variant::Content(url)=>Ok(url.clone().into_string()),
_=>Err(DownloadDecalError::TexturePropertyInvalid),
}
}.await{
Ok(asset_url)=>Some((asset_response.displayName,asset_url)),
Err(e)=>{
eprintln!("get_asset error: {}",e);
None
},
}
}
}).collect::<Vec<(String,String)>>().await;
for (file_name,asset_url) in asset_id_list{
println!("{}={}",file_name,asset_url);
}
Ok(()) Ok(())
} }
struct UploadAssetConfig{ struct UploadAssetConfig{
cookie:Cookie,
asset_id:AssetID,
change_name:Option<String>,
change_description:Option<String>,
change_free_model:Option<bool>,
change_allow_comments:Option<bool>,
group_id:Option<u64>,
input_file:PathBuf,
}
async fn upload_asset(config:UploadAssetConfig)->AResult<()>{
let context=CookieContext::new(config.cookie);
let resp=context.upload(rbx_asset::cookie::UploadRequest{
assetid:config.asset_id,
name:config.change_name,
description:config.change_description,
ispublic:config.change_free_model,
allowComments:config.change_allow_comments,
groupId:config.group_id,
},tokio::fs::read(config.input_file).await?).await?;
println!("UploadResponse={:?}",resp);
Ok(())
}
struct UploadAssetMediaConfig{
api_key:ApiKey, api_key:ApiKey,
asset_id:u64, asset_id:u64,
input_file:PathBuf, input_file:PathBuf,
} }
async fn upload_asset_media(config:UploadAssetMediaConfig)->AResult<()>{ async fn upload_asset(config:UploadAssetConfig)->AResult<()>{
let context=CloudContext::new(config.api_key); let context=CloudContext::new(config.api_key);
let resp=context.update_asset(rbx_asset::cloud::UpdateAssetRequest{ let resp=context.update_asset(rbx_asset::cloud::UpdateAssetRequest{
assetId:config.asset_id, assetId:config.asset_id,
@ -843,20 +518,23 @@ async fn upload_place(config:UploadPlaceConfig)->AResult<()>{
Ok(()) Ok(())
} }
async fn download_list(cookie:Cookie,asset_id_file_map:AssetIDFileMap)->AResult<()>{ async fn download_list(api_key:ApiKey,asset_id_file_map:AssetIDFileMap)->AResult<()>{
let context=CookieContext::new(cookie); let context=CloudContext::new(api_key);
futures::stream::iter(asset_id_file_map.into_iter() futures::stream::iter(asset_id_file_map.into_iter()
.map(|(asset_id,file)|{ .map(|(asset_id,file)|{
let context=&context; let context=&context;
async move{ async move{
Ok((file,context.get_asset(rbx_asset::cookie::GetAssetRequest{asset_id,version:None}).await?)) Ok((file,context.get_asset(rbx_asset::cloud::GetAssetRequest{asset_id,version:None}).await?))
} }
})) }))
.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,data))=>{
eprintln!("fs error: {}",e); match tokio::fs::write(dest,data).await{
Err(e)=>eprintln!("fs error: {}",e),
_=>(),
}
}, },
Err(e)=>eprintln!("dl error: {}",e), Err(e)=>eprintln!("dl error: {}",e),
} }
@ -1033,33 +711,18 @@ async fn download_history(mut config:DownloadHistoryConfig)->AResult<()>{
Ok(()) Ok(())
} }
#[derive(Debug)] fn load_dom<R:Read>(input:R)->AResult<rbx_dom_weak::WeakDom>{
enum LoadDomError{
IO(std::io::Error),
RbxBinary(rbx_binary::DecodeError),
RbxXml(rbx_xml::DecodeError),
UnknownRobloxFile([u8;4]),
UnsupportedFile,
}
impl std::fmt::Display for LoadDomError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for LoadDomError{}
fn load_dom<R:Read>(input:R)->Result<rbx_dom_weak::WeakDom,LoadDomError>{
let mut buf=std::io::BufReader::new(input); let mut buf=std::io::BufReader::new(input);
let peek=std::io::BufRead::fill_buf(&mut buf).map_err(LoadDomError::IO)?; let peek=std::io::BufRead::fill_buf(&mut buf)?;
match &peek[0..4]{ match &peek[0..4]{
b"<rob"=>{ b"<rob"=>{
match &peek[4..8]{ match &peek[4..8]{
b"lox!"=>rbx_binary::from_reader(buf).map_err(LoadDomError::RbxBinary), b"lox!"=>rbx_binary::from_reader(buf).map_err(anyhow::Error::msg),
b"lox "=>rbx_xml::from_reader_default(buf).map_err(LoadDomError::RbxXml), b"lox "=>rbx_xml::from_reader_default(buf).map_err(anyhow::Error::msg),
other=>Err(LoadDomError::UnknownRobloxFile(other.try_into().unwrap())), other=>Err(anyhow::Error::msg(format!("Unknown Roblox file type {:?}",other))),
} }
}, },
_=>Err(LoadDomError::UnsupportedFile), _=>Err(anyhow::Error::msg("unsupported file type")),
} }
} }
@ -1349,8 +1012,7 @@ struct CompileUploadAssetConfig{
input_folder:PathBuf, input_folder:PathBuf,
template:Option<PathBuf>, template:Option<PathBuf>,
style:Option<rox_compiler::Style>, style:Option<rox_compiler::Style>,
cookie:Cookie, api_key:ApiKey,
group_id:Option<u64>,
asset_id:AssetID, asset_id:AssetID,
} }
async fn compile_upload_asset(config:CompileUploadAssetConfig)->AResult<()>{ async fn compile_upload_asset(config:CompileUploadAssetConfig)->AResult<()>{
@ -1370,14 +1032,11 @@ async fn compile_upload_asset(config:CompileUploadAssetConfig)->AResult<()>{
rbx_binary::to_writer(std::io::Cursor::new(&mut data),&dom,dom.root().children())?; rbx_binary::to_writer(std::io::Cursor::new(&mut data),&dom,dom.root().children())?;
//upload it //upload it
let context=CookieContext::new(config.cookie); let context=CloudContext::new(config.api_key);
let resp=context.upload(rbx_asset::cookie::UploadRequest{ let resp=context.update_asset(rbx_asset::cloud::UpdateAssetRequest{
groupId:config.group_id, assetId:config.asset_id,
assetid:config.asset_id, displayName:None,
name:None,
description:None, description:None,
ispublic:None,
allowComments:None,
},data).await?; },data).await?;
println!("UploadResponse={:?}",resp); println!("UploadResponse={:?}",resp);
Ok(()) Ok(())
@ -1409,10 +1068,9 @@ async fn compile_upload_place(config:CompileUploadPlaceConfig)->AResult<()>{
//upload it //upload it
let context=CloudContext::new(config.api_key); let context=CloudContext::new(config.api_key);
let resp=context.update_place(rbx_asset::cloud::UpdatePlaceRequest{ context.update_place(rbx_asset::cloud::UpdatePlaceRequest{
universeId:config.universe_id, universeId:config.universe_id,
placeId:config.place_id, placeId:config.place_id,
},data).await?; },data).await?;
println!("UploadResponse={:?}",resp);
Ok(()) Ok(())
} }