diff --git a/Cargo.lock b/Cargo.lock index 463aa92..3f06925 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,7 +124,7 @@ dependencies = [ "rbx_dom_weak", "rbx_reflection_database", "rbx_xml", - "reqwest", + "reqwest 0.11.27", "serde", "serde_json", "tokio", @@ -163,6 +163,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" + [[package]] name = "bitflags" version = "1.3.2" @@ -416,9 +422,9 @@ checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" [[package]] name = "flate2" -version = "1.0.28" +version = "1.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "4556222738635b7a3417ae6130d8f52201e45a0c4d1a907f0826383adb5f85e7" dependencies = [ "crc32fast", "miniz_oxide", @@ -586,7 +592,26 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 1.1.0", "indexmap", "slab", "tokio", @@ -623,6 +648,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http-body" version = "0.4.6" @@ -630,7 +666,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +dependencies = [ + "bytes", + "futures-core", + "http 1.1.0", + "http-body 1.0.0", "pin-project-lite", ] @@ -656,9 +715,9 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", - "http", - "http-body", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", "httparse", "httpdate", "itoa", @@ -670,6 +729,26 @@ dependencies = [ "want", ] +[[package]] +name = "hyper" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.4", + "http 1.1.0", + "http-body 1.0.0", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + [[package]] name = "hyper-tls" version = "0.5.0" @@ -677,12 +756,48 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper", + "hyper 0.14.28", "native-tls", "tokio", "tokio-native-tls", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.3.1", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "hyper 1.3.1", + "pin-project-lite", + "socket2", + "tokio", + "tower", + "tower-service", + "tracing", +] + [[package]] name = "iana-time-zone" version = "0.1.60" @@ -1019,6 +1134,26 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.14" @@ -1158,6 +1293,17 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rbx_asset" +version = "0.1.0" +dependencies = [ + "chrono", + "flate2", + "reqwest 0.12.4", + "serde", + "url", +] + [[package]] name = "rbx_binary" version = "0.7.4" @@ -1277,11 +1423,11 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2", - "http", - "http-body", - "hyper", - "hyper-tls", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.28", + "hyper-tls 0.5.0", "ipnet", "js-sys", "log", @@ -1290,7 +1436,7 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile", + "rustls-pemfile 1.0.4", "serde", "serde_json", "serde_urlencoded", @@ -1303,7 +1449,49 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "winreg", + "winreg 0.50.0", +] + +[[package]] +name = "reqwest" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" +dependencies = [ + "base64 0.22.0", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.4.4", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.3.1", + "hyper-tls 0.6.0", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile 2.1.2", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg 0.52.0", ] [[package]] @@ -1356,6 +1544,22 @@ dependencies = [ "base64 0.21.7", ] +[[package]] +name = "rustls-pemfile" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +dependencies = [ + "base64 0.22.0", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "beb461507cee2c2ff151784c52762cf4d9ff6a61f3e80968600ed24fa837fa54" + [[package]] name = "ryu" version = "1.0.17" @@ -1396,18 +1600,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.198" +version = "1.0.199" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" +checksum = "0c9f6e76df036c77cd94996771fb40db98187f096dd0b9af39c6c6e452ba966a" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.198" +version = "1.0.199" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" +checksum = "11bd257a6541e141e42ca6d24ae26f7714887b47e89aa739099104c7e4d3b7fc" dependencies = [ "proc-macro2", "quote", @@ -1446,6 +1650,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + [[package]] name = "socket2" version = "0.5.6" @@ -1630,6 +1840,28 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + [[package]] name = "tower-service" version = "0.3.2" @@ -1642,6 +1874,7 @@ version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ + "log", "pin-project-lite", "tracing-core", ] @@ -1960,6 +2193,16 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "xml-rs" version = "0.8.20" diff --git a/Cargo.toml b/Cargo.toml index e3bc714..5c8fb07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,4 @@ +workspace = { members = ["rbx_asset"] } [package] name = "asset-tool" version = "0.3.0" diff --git a/rbx_asset/Cargo.toml b/rbx_asset/Cargo.toml new file mode 100644 index 0000000..64325a2 --- /dev/null +++ b/rbx_asset/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "rbx_asset" +version = "0.1.0" +edition = "2021" +publish = ["strafesnet"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +chrono = { version = "0.4.38", features = ["serde"] } +flate2 = "1.0.29" +reqwest = { version = "0.12.4", features = ["json"] } +serde = { version = "1.0.199", features = ["derive"] } +url = "2.5.0" diff --git a/rbx_asset/src/context.rs b/rbx_asset/src/context.rs new file mode 100644 index 0000000..134ec3b --- /dev/null +++ b/rbx_asset/src/context.rs @@ -0,0 +1,311 @@ +#[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 DownloadRequest{ + pub asset_id:u64, + pub version:Option, +} +#[derive(Debug)] +pub enum DownloadError{ + ParseError(url::ParseError), + Reqwest(reqwest::Error), + IO(std::io::Error) +} +impl std::fmt::Display for DownloadError{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f,"{self:?}") + } +} +impl std::error::Error for DownloadError{} + +pub struct HistoryPageRequest{ + 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 HistoryPageResponse{ + pub previousPageCursor:Option, + pub nextPageCursor:Option, + pub data:Vec, +} +#[derive(Debug)] +pub enum HistoryPageError{ + ParseError(url::ParseError), + Reqwest(reqwest::Error), +} +impl std::fmt::Display for HistoryPageError{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f,"{self:?}") + } +} +impl std::error::Error for HistoryPageError{} + +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 RobloxContext{ + pub cookie:String, + pub client:reqwest::Client, +} + +impl RobloxContext{ + pub fn new(cookie:String)->Self{ + Self{ + cookie, + 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 download(&self,config:DownloadRequest)->Result,DownloadError>{ + let mut url=reqwest::Url::parse("https://assetdelivery.roblox.com/v1/asset/").map_err(DownloadError::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(DownloadError::Reqwest)?; + + let body=resp.bytes().await.map_err(DownloadError::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(DownloadError::IO) + } + pub async fn history_page(&self,config:HistoryPageRequest)->Result{ + let mut url=reqwest::Url::parse(format!("https://develop.roblox.com/v1/assets/{}/saved-versions",config.asset_id).as_str()).map_err(HistoryPageError::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(HistoryPageError::Reqwest)? + .json::().await.map_err(HistoryPageError::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)?) + } +} \ No newline at end of file diff --git a/rbx_asset/src/lib.rs b/rbx_asset/src/lib.rs new file mode 100644 index 0000000..9efb2ab --- /dev/null +++ b/rbx_asset/src/lib.rs @@ -0,0 +1 @@ +pub mod context;