#[derive(Debug)] pub enum PostError{ Reqwest(reqwest::Error), CSRF, } impl std::fmt::Display for PostError{ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f,"{self:?}") } } impl std::error::Error for PostError{} #[derive(Debug,serde::Deserialize,serde::Serialize)] #[allow(nonstandard_style,dead_code)] pub struct CreateRequest{ pub name:String, pub description:String, pub ispublic:bool, pub allowComments:bool, pub groupId:Option, } #[derive(Debug)] pub enum CreateError{ ParseError(url::ParseError), PostError(PostError), 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{} #[allow(nonstandard_style,dead_code)] pub struct UploadRequest{ pub assetid:u64, pub name:Option, pub description:Option, pub ispublic:Option, pub allowComments:Option, pub groupId:Option, } #[derive(Debug)] pub enum UploadError{ ParseError(url::ParseError), PostError(PostError), Reqwest(reqwest::Error), AssetIdIsZero, } impl std::fmt::Display for UploadError{ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f,"{self:?}") } } impl std::error::Error for UploadError{} #[derive(Debug,serde::Deserialize,serde::Serialize)] #[allow(nonstandard_style,dead_code)] pub struct UploadResponse{ pub AssetId:u64, pub AssetVersionId: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 AssetVersionsPageRequest{ pub asset_id:u64, pub cursor:Option, } #[derive(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(serde::Deserialize)] #[allow(nonstandard_style,dead_code)] pub struct AssetVersionsPageResponse{ pub previousPageCursor:Option, pub nextPageCursor:Option, pub data:Vec, } #[derive(Debug)] pub enum AssetVersionsPageError{ ParseError(url::ParseError), Reqwest(reqwest::Error), } impl std::fmt::Display for AssetVersionsPageError{ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f,"{self:?}") } } impl std::error::Error for AssetVersionsPageError{} pub struct InventoryPageRequest{ pub group:u64, pub cursor:Option, } #[derive(serde::Deserialize,serde::Serialize)] #[allow(nonstandard_style,dead_code)] pub struct InventoryItem{ pub id:u64, pub name:String, } #[derive(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{} //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 Cookie(String); impl Cookie{ pub fn new(cookie:String)->Self{ Self(cookie) } pub fn get(self)->String{ self.0 } } #[derive(Clone)] pub struct CookieContext{ pub cookie:String, pub client:reqwest::Client, } impl CookieContext{ pub fn new(cookie:Cookie)->Self{ Self{ cookie:cookie.get(), client:reqwest::Client::new(), } } async fn get(&self,url:impl reqwest::IntoUrl)->Result{ self.client.get(url) .header("Cookie",self.cookie.as_str()) .send().await } async fn post(&self,url:url::Url,body:impl Into+Clone)->Result{ let mut resp=self.client.post(url.clone()) .header("Cookie",self.cookie.as_str()) .body(body.clone()) .send().await.map_err(PostError::Reqwest)?; //This is called a CSRF challenge apparently if resp.status()==reqwest::StatusCode::FORBIDDEN{ if let Some(csrf_token)=resp.headers().get("X-CSRF-Token"){ resp=self.client.post(url) .header("X-CSRF-Token",csrf_token) .header("Cookie",self.cookie.as_str()) .body(body) .send().await.map_err(PostError::Reqwest)?; }else{ Err(PostError::CSRF)?; } } Ok(resp) } pub async fn create(&self,config:CreateRequest,body:impl Into+Clone)->Result{ let mut url=reqwest::Url::parse("https://data.roblox.com/Data/Upload.ashx?json=1&type=Model&genreTypeId=1").map_err(CreateError::ParseError)?; //url borrow scope { let mut query=url.query_pairs_mut();//borrow here //archaic roblox api uses 0 for new asset query.append_pair("assetid","0"); query.append_pair("name",config.name.as_str()); query.append_pair("description",config.description.as_str()); query.append_pair("ispublic",if config.ispublic{"True"}else{"False"}); query.append_pair("allowComments",if config.allowComments{"True"}else{"False"}); match config.groupId{ Some(group_id)=>{query.append_pair("groupId",group_id.to_string().as_str());}, None=>(), } } let resp=self.post(url,body).await.map_err(CreateError::PostError)?; Ok(resp.json::().await.map_err(CreateError::Reqwest)?) } pub async fn upload(&self,config:UploadRequest,body:impl Into+Clone)->Result{ let mut url=reqwest::Url::parse("https://data.roblox.com/Data/Upload.ashx?json=1&type=Model&genreTypeId=1").map_err(UploadError::ParseError)?; //url borrow scope { let mut query=url.query_pairs_mut();//borrow here //archaic roblox api uses 0 for new asset match config.assetid{ 0=>return Err(UploadError::AssetIdIsZero), assetid=>{query.append_pair("assetid",assetid.to_string().as_str());}, } if let Some(name)=config.name.as_deref(){ query.append_pair("name",name); } if let Some(description)=config.description.as_deref(){ query.append_pair("description",description); } if let Some(ispublic)=config.ispublic{ query.append_pair("ispublic",if ispublic{"True"}else{"False"}); } if let Some(allow_comments)=config.allowComments{ query.append_pair("allowComments",if allow_comments{"True"}else{"False"}); } if let Some(group_id)=config.groupId{ query.append_pair("groupId",group_id.to_string().as_str()); } } let resp=self.post(url,body).await.map_err(UploadError::PostError)?; Ok(resp.json::().await.map_err(UploadError::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_page(&self,config:AssetVersionsPageRequest)->Result{ let mut url=reqwest::Url::parse(format!("https://develop.roblox.com/v1/assets/{}/saved-versions",config.asset_id).as_str()).map_err(AssetVersionsPageError::ParseError)?; //url borrow scope { let mut query=url.query_pairs_mut();//borrow here //query.append_pair("sortOrder","Asc"); //query.append_pair("limit","100"); //query.append_pair("count","100"); if let Some(cursor)=config.cursor.as_deref(){ query.append_pair("cursor",cursor); } } Ok(self.get(url).await.map_err(AssetVersionsPageError::Reqwest)? .json::().await.map_err(AssetVersionsPageError::Reqwest)?) } pub async fn get_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)?) } }