diff --git a/rbx_asset/src/cookie.rs b/rbx_asset/src/cookie.rs new file mode 100644 index 0000000..005d36f --- /dev/null +++ b/rbx_asset/src/cookie.rs @@ -0,0 +1,321 @@ +#[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)?) + } +} \ No newline at end of file diff --git a/rbx_asset/src/lib.rs b/rbx_asset/src/lib.rs index 1ede322..7baa94b 100644 --- a/rbx_asset/src/lib.rs +++ b/rbx_asset/src/lib.rs @@ -1 +1,2 @@ pub mod cloud; +pub mod cookie;