#[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 struct Creator{ pub userId:u64, pub groupId:u64, } #[derive(Debug,serde::Deserialize,serde::Serialize)] #[allow(nonstandard_style,dead_code)] pub struct CreationContext{ pub creator:Creator, pub expectedPrice:u64, } #[derive(Debug,serde::Deserialize,serde::Serialize)] #[allow(nonstandard_style,dead_code)] pub enum ModerationResult{ MODERATION_STATE_REVIEWING, MODERATION_STATE_REJECTED, MODERATION_STATE_APPROVED, } #[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 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 form=reqwest::multipart::Form::new() .text("request",request_config) .part("fileContent",reqwest::multipart::Part::bytes(body)); let resp=self.post_form(url,form).await.map_err(CreateError::Reqwest)? .error_for_status().map_err(CreateError::Reqwest)?; Ok(resp.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)); let resp=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)?; Ok(resp.json::().await.map_err(UpdateError::Reqwest)?) } pub async fn get_asset(&self,config:GetAssetRequest)->Result,GetError>{ let mut url=reqwest::Url::parse("https://assetdelivery.roblox.com/v1/asset/").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)?; let body=resp.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_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)?; Ok(self.get(url).await.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); } } Ok(self.get(url).await.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"); } let resp=self.post(url,body).await.map_err(UpdateError::Reqwest)? .error_for_status().map_err(UpdateError::Reqwest)?; Ok(resp.json::().await.map_err(UpdateError::Reqwest)?) } }