#[derive(Debug,serde::Deserialize,serde::Serialize)] #[allow(nonstandard_style,dead_code)] pub enum AssetType{ Audio, Decal, Model, } #[derive(Debug,serde::Deserialize,serde::Serialize)] #[allow(nonstandard_style,dead_code)] pub struct CreateAssetRequest{ pub assetType:AssetType, pub creationContext:CreationContext, pub description:String, pub displayName:String, } #[derive(Debug)] pub enum CreateError{ ParseError(url::ParseError), SerializeError(serde_json::Error), Reqwest(reqwest::Error), } impl std::fmt::Display for CreateError{ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f,"{self:?}") } } impl std::error::Error for CreateError{} #[derive(Debug,serde::Deserialize,serde::Serialize)] #[allow(nonstandard_style,dead_code)] pub struct UpdateAssetRequest{ pub assetId:u64, pub displayName:Option, pub description:Option, } //woo nested roblox stuff #[derive(Debug,serde::Deserialize,serde::Serialize)] #[allow(nonstandard_style,dead_code)] pub enum Creator{ userId(String),//u64 string groupId(String),//u64 string } #[derive(Debug,serde::Deserialize,serde::Serialize)] #[allow(nonstandard_style,dead_code)] pub struct CreationContext{ pub creator:Creator, pub expectedPrice:Option, } #[derive(Debug,serde::Deserialize,serde::Serialize)] #[allow(nonstandard_style,dead_code)] pub enum ModerationState{ Reviewing, Rejected, Approved, } #[derive(Debug,serde::Deserialize,serde::Serialize)] #[allow(nonstandard_style,dead_code)] pub struct ModerationResult{ pub moderationState:ModerationState, } #[derive(Debug,serde::Deserialize,serde::Serialize)] #[allow(nonstandard_style,dead_code)] pub struct Preview{ pub asset:String, pub altText:String, } #[allow(nonstandard_style,dead_code)] pub struct UpdatePlaceRequest{ pub universeId:u64, pub placeId:u64, } #[derive(Debug,serde::Deserialize,serde::Serialize)] #[allow(nonstandard_style,dead_code)] pub struct UpdatePlaceResponse{ pub versionNumber:u64, } #[derive(Debug)] pub enum UpdateError{ ParseError(url::ParseError), SerializeError(serde_json::Error), Reqwest(reqwest::Error), } impl std::fmt::Display for UpdateError{ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f,"{self:?}") } } impl std::error::Error for UpdateError{} #[allow(nonstandard_style,dead_code)] 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, pub revisionId:String,//u64 pub moderationResult:ModerationResult, pub icon:Option, pub previews:Option>, } #[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, } #[derive(Debug)] pub enum GetError{ ParseError(url::ParseError), Reqwest(reqwest::Error), IO(std::io::Error) } impl std::fmt::Display for GetError{ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f,"{self:?}") } } impl std::error::Error for GetError{} pub struct AssetVersionsRequest{ pub asset_id:u64, pub cursor:Option, } #[derive(Debug,serde::Deserialize,serde::Serialize)] #[allow(nonstandard_style,dead_code)] pub struct AssetVersion{ pub Id:u64, pub assetId:u64, pub assetVersionNumber:u64, pub creatorType:String, pub creatorTargetId:u64, pub creatingUniverseId:Option, pub created:chrono::DateTime, pub isPublished:bool, } #[derive(Debug,serde::Deserialize)] #[allow(nonstandard_style,dead_code)] pub struct AssetVersionsResponse{ pub previousPageCursor:Option, pub nextPageCursor:Option, pub data:Vec, } #[derive(Debug)] pub enum AssetVersionsError{ ParseError(url::ParseError), Reqwest(reqwest::Error), } impl std::fmt::Display for AssetVersionsError{ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f,"{self:?}") } } impl std::error::Error for AssetVersionsError{} pub struct InventoryPageRequest{ pub group:u64, pub cursor:Option, } #[derive(Debug,serde::Deserialize,serde::Serialize)] #[allow(nonstandard_style,dead_code)] pub struct InventoryItem{ pub id:u64, pub name:String, } #[derive(Debug,serde::Deserialize,serde::Serialize)] #[allow(nonstandard_style,dead_code)] pub struct InventoryPageResponse{ pub totalResults:u64,//up to 50 pub filteredKeyword:Option,//"" pub searchDebugInfo:Option,//null pub spellCheckerResult:Option,//null pub queryFacets:Option,//null pub imageSearchStatus:Option,//null pub previousPageCursor:Option, pub nextPageCursor:Option, pub data:Vec, } #[derive(Debug)] pub enum InventoryPageError{ ParseError(url::ParseError), Reqwest(reqwest::Error), } impl std::fmt::Display for InventoryPageError{ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f,"{self:?}") } } impl std::error::Error for InventoryPageError{} #[derive(Debug,serde::Deserialize,serde::Serialize)] #[allow(nonstandard_style,dead_code)] pub struct RobloxOperation{ pub path:Option, pub metadata:Option, pub done:Option, pub error:Option, pub response:Option, } //idk how to do this better enum ReaderType{ GZip(flate2::read::GzDecoder>), Raw(std::io::BufReader), } fn maybe_gzip_decode(input:R)->std::io::Result>{ 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>{ let mut contents=Vec::new(); readable.read_to_end(&mut contents)?; Ok(contents) } #[derive(Clone)] pub struct ApiKey(String); impl ApiKey{ pub fn new(api_key:String)->Self{ Self(api_key) } pub fn get(self)->String{ self.0 } } #[derive(Clone)] pub struct CloudContext{ pub api_key:String, pub client:reqwest::Client, } impl CloudContext{ pub fn new(api_key:ApiKey)->Self{ Self{ api_key:api_key.get(), client:reqwest::Client::new(), } } async fn get(&self,url:impl reqwest::IntoUrl)->Result{ self.client.get(url) .header("x-api-key",self.api_key.as_str()) .send().await } async fn post(&self,url:url::Url,body:impl Into+Clone)->Result{ self.client.post(url) .header("x-api-key",self.api_key.as_str()) .body(body) .send().await } async fn patch_form(&self,url:url::Url,form:reqwest::multipart::Form)->Result{ self.client.patch(url) .header("x-api-key",self.api_key.as_str()) .multipart(form) .send().await } async fn post_form(&self,url:url::Url,form:reqwest::multipart::Form)->Result{ self.client.post(url) .header("x-api-key",self.api_key.as_str()) .multipart(form) .send().await } pub async fn create_asset(&self,config:CreateAssetRequest,body:impl Into>)->Result{ 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::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() .text("request",request_config) .part("fileContent",part); self.post_form(url,form).await.map_err(CreateError::Reqwest)? .error_for_status().map_err(CreateError::Reqwest)? .json::().await.map_err(CreateError::Reqwest) } pub async fn update_asset(&self,config:UpdateAssetRequest,body:impl Into>)->Result{ let raw_url=format!("https://apis.roblox.com/assets/v1/assets/{}",config.assetId); let url=reqwest::Url::parse(raw_url.as_str()).map_err(UpdateError::ParseError)?; let request_config=serde_json::to_string(&config).map_err(UpdateError::SerializeError)?; let form=reqwest::multipart::Form::new() .text("request",request_config) .part("fileContent",reqwest::multipart::Part::bytes(body)); self.patch_form(url,form).await .map_err(UpdateError::Reqwest)? //roblox api documentation is very poor, just give the status code and drop the json .error_for_status().map_err(UpdateError::Reqwest)? .json::().await.map_err(UpdateError::Reqwest) } pub async fn get_asset_info(&self,config:GetAssetInfoRequest)->Result{ 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)?; self.get(url).await.map_err(GetError::Reqwest)? .error_for_status().map_err(GetError::Reqwest)? .json::().await.map_err(GetError::Reqwest) } pub async fn get_asset_version(&self,config:GetAssetVersionRequest)->Result,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)){ 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,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{ 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)?; self.get(url).await.map_err(AssetVersionsError::Reqwest)? .error_for_status().map_err(AssetVersionsError::Reqwest)? .json::().await.map_err(AssetVersionsError::Reqwest) } pub async fn inventory_page(&self,config:InventoryPageRequest)->Result{ 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)?; //url borrow scope { let mut query=url.query_pairs_mut();//borrow here if let Some(cursor)=config.cursor.as_deref(){ query.append_pair("cursor",cursor); } } self.get(url).await.map_err(InventoryPageError::Reqwest)? .error_for_status().map_err(InventoryPageError::Reqwest)? .json::().await.map_err(InventoryPageError::Reqwest) } pub async fn update_place(&self,config:UpdatePlaceRequest,body:impl Into+Clone)->Result{ let raw_url=format!("https://apis.roblox.com/universes/v1/{}/places/{}/versions",config.universeId,config.placeId); let mut url=reqwest::Url::parse(raw_url.as_str()).map_err(UpdateError::ParseError)?; //url borrow scope { let mut query=url.query_pairs_mut();//borrow here query.append_pair("versionType","Published"); } self.post(url,body).await.map_err(UpdateError::Reqwest)? .error_for_status().map_err(UpdateError::Reqwest)? .json::().await.map_err(UpdateError::Reqwest) } }