forked from StrafesNET/asset-tool
Compare commits
71 Commits
Author | SHA1 | Date | |
---|---|---|---|
10a50948fc | |||
9eabb0197c | |||
fa9d42fc1f | |||
8f754f0bca | |||
2987a6b321 | |||
450b6a0829 | |||
091a2a92f1 | |||
31aae80cc5 | |||
041cc75015 | |||
d77312309f | |||
50145460b9 | |||
df2a5bb9ce | |||
e40041a894 | |||
ad8e1865f3 | |||
45c1e52c0f | |||
0196f47374 | |||
a8163014ad | |||
4d26e7ad19 | |||
99077cf467 | |||
302603998e | |||
71cae5c089 | |||
fb9dd8660d | |||
64e4887b83 | |||
d125829a00 | |||
5509fd2166 | |||
27deef3dd6 | |||
e17db96e86 | |||
875b059074 | |||
9b1c709e7c | |||
ebd48269b8 | |||
5a4ac0e7f2 | |||
89a478eaac | |||
68ebbad7a7 | |||
c99b752738 | |||
4f798e5f07 | |||
39fa74d44a | |||
edb5ea7648 | |||
504ff40385 | |||
89c0ee2cc2 | |||
f7e565bd0b | |||
fe8062d8e0 | |||
e6fe04aa73 | |||
656de62bdc | |||
4d90a74a82 | |||
fb6fb67954 | |||
b0f1e964a6 | |||
aea777ecd3 | |||
6eca84a08a | |||
d6adc1da45 | |||
b7bab46e04 | |||
0413767ef9 | |||
b69698fd8e | |||
309fc2494d | |||
e89edf287f | |||
b352707b99 | |||
81f411272f | |||
e45f3c2cf9 | |||
3cd65158a0 | |||
38d78ff2c5 | |||
c49f9e4dd3 | |||
a99b5a2666 | |||
4ffeaa5784 | |||
a693da9cb2 | |||
a572d10447 | |||
1b2e13b4c7 | |||
321ef9b51e | |||
28bcf8005b | |||
ea0239c78b | |||
94792ebf02 | |||
ac674778f6 | |||
d3d58ff358 |
1226
Cargo.lock
generated
1226
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
13
Cargo.toml
13
Cargo.toml
@ -1,7 +1,7 @@
|
||||
workspace = { members = ["rbx_asset", "rox_compiler"] }
|
||||
[package]
|
||||
name = "asset-tool"
|
||||
version = "0.4.6"
|
||||
version = "0.4.12"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
@ -10,14 +10,13 @@ edition = "2021"
|
||||
anyhow = "1.0.75"
|
||||
clap = { version = "4.4.2", features = ["derive"] }
|
||||
futures = "0.3.30"
|
||||
git2 = "0.18.1"
|
||||
git2 = "0.20.0"
|
||||
lazy-regex = "3.1.0"
|
||||
pollster = "0.3.0"
|
||||
rbx_asset = { path = "rbx_asset" }
|
||||
rbx_binary = "0.7.4"
|
||||
rbx_dom_weak = "2.7.0"
|
||||
rbx_reflection_database = "0.2.10"
|
||||
rbx_xml = "0.13.3"
|
||||
rbx_binary = "1.0.0"
|
||||
rbx_dom_weak = "3.0.0"
|
||||
rbx_reflection_database = "1.0.3"
|
||||
rbx_xml = "1.0.0"
|
||||
rox_compiler = { path = "rox_compiler" }
|
||||
serde_json = "1.0.111"
|
||||
tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread", "fs"] }
|
||||
|
@ -1,14 +1,23 @@
|
||||
[package]
|
||||
name = "rbx_asset"
|
||||
version = "0.2.2"
|
||||
version = "0.4.4"
|
||||
edition = "2021"
|
||||
publish = ["strafesnet"]
|
||||
repository = "https://git.itzana.me/StrafesNET/asset-tool"
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "Strongly typed interface to the Roblox API using a Cookie or an API key."
|
||||
authors = ["Rhys Lloyd <krakow20@gmail.com>"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
default = ["gzip"]
|
||||
gzip = ["dep:flate2"]
|
||||
|
||||
[dependencies]
|
||||
bytes = "1.10.1"
|
||||
chrono = { version = "0.4.38", features = ["serde"] }
|
||||
flate2 = "1.0.29"
|
||||
flate2 = { version = "1.0.29", optional = true }
|
||||
reqwest = { version = "0.12.4", features = ["json","multipart"] }
|
||||
serde = { version = "1.0.199", features = ["derive"] }
|
||||
serde_json = "1.0.111"
|
||||
|
176
rbx_asset/LICENSE-APACHE
Normal file
176
rbx_asset/LICENSE-APACHE
Normal file
@ -0,0 +1,176 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
23
rbx_asset/LICENSE-MIT
Normal file
23
rbx_asset/LICENSE-MIT
Normal file
@ -0,0 +1,23 @@
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
@ -1,3 +1,6 @@
|
||||
use crate::util::{serialize_u64,deserialize_u64,response_ok};
|
||||
use crate::types::{ResponseError,MaybeGzippedBytes};
|
||||
|
||||
#[derive(Debug,serde::Deserialize,serde::Serialize)]
|
||||
#[allow(nonstandard_style,dead_code)]
|
||||
pub enum AssetType{
|
||||
@ -29,7 +32,7 @@ pub struct AssetOperation{
|
||||
operation:RobloxOperation,
|
||||
}
|
||||
impl AssetOperation{
|
||||
pub async fn try_get_asset(&self,context:&CloudContext)->Result<AssetResponse,AssetOperationError>{
|
||||
pub async fn try_get_asset(&self,context:&Context)->Result<AssetResponse,AssetOperationError>{
|
||||
serde_json::from_value(
|
||||
self.operation
|
||||
.try_get_reponse(context).await
|
||||
@ -40,6 +43,7 @@ impl AssetOperation{
|
||||
#[derive(Debug)]
|
||||
pub enum CreateError{
|
||||
Parse(url::ParseError),
|
||||
Response(ResponseError),
|
||||
Serialize(serde_json::Error),
|
||||
Reqwest(reqwest::Error),
|
||||
}
|
||||
@ -62,8 +66,8 @@ pub struct UpdateAssetRequest{
|
||||
#[derive(Clone,Debug,serde::Deserialize,serde::Serialize)]
|
||||
#[allow(nonstandard_style,dead_code)]
|
||||
pub enum Creator{
|
||||
userId(String),//u64 string
|
||||
groupId(String),//u64 string
|
||||
userId(#[serde(deserialize_with="deserialize_u64",serialize_with="serialize_u64")]u64),
|
||||
groupId(#[serde(deserialize_with="deserialize_u64",serialize_with="serialize_u64")]u64),
|
||||
}
|
||||
#[derive(Debug,serde::Deserialize,serde::Serialize)]
|
||||
#[allow(nonstandard_style,dead_code)]
|
||||
@ -102,6 +106,7 @@ pub struct UpdatePlaceResponse{
|
||||
#[derive(Debug)]
|
||||
pub enum UpdateError{
|
||||
ParseError(url::ParseError),
|
||||
Response(ResponseError),
|
||||
SerializeError(serde_json::Error),
|
||||
Reqwest(reqwest::Error),
|
||||
}
|
||||
@ -115,7 +120,7 @@ impl std::error::Error for UpdateError{}
|
||||
struct GetAssetOperationRequest{
|
||||
operation_id:String,
|
||||
}
|
||||
pub struct GetAssetInfoRequest{
|
||||
pub struct GetAssetLatestRequest{
|
||||
pub asset_id:u64,
|
||||
}
|
||||
/*
|
||||
@ -142,33 +147,34 @@ pub struct GetAssetInfoRequest{
|
||||
#[derive(Debug,serde::Deserialize,serde::Serialize)]
|
||||
#[allow(nonstandard_style,dead_code)]
|
||||
pub struct AssetResponse{
|
||||
pub assetId:String,//u64 wrapped in quotes wohoo!!
|
||||
//u64 wrapped in quotes wohoo!!
|
||||
#[serde(deserialize_with="deserialize_u64")]
|
||||
#[serde(serialize_with="serialize_u64")]
|
||||
pub assetId:u64,
|
||||
pub assetType:AssetType,
|
||||
pub creationContext:CreationContext,
|
||||
pub description:String,
|
||||
pub description:Option<String>,
|
||||
pub displayName:String,
|
||||
pub path:String,
|
||||
pub revisionCreateTime:chrono::DateTime<chrono::Utc>,
|
||||
pub revisionId:String,//u64
|
||||
#[serde(deserialize_with="deserialize_u64")]
|
||||
#[serde(serialize_with="serialize_u64")]
|
||||
pub revisionId:u64,
|
||||
pub moderationResult:ModerationResult,
|
||||
pub icon:Option<String>,
|
||||
pub previews:Option<Vec<Preview>>,
|
||||
#[serde(default)]
|
||||
pub previews:Vec<Preview>,
|
||||
}
|
||||
#[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<u64>,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub enum GetError{
|
||||
ParseError(url::ParseError),
|
||||
Response(ResponseError),
|
||||
Reqwest(reqwest::Error),
|
||||
IO(std::io::Error)
|
||||
}
|
||||
impl std::fmt::Display for GetError{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
@ -177,6 +183,29 @@ impl std::fmt::Display for GetError{
|
||||
}
|
||||
impl std::error::Error for GetError{}
|
||||
|
||||
#[derive(Debug,serde::Deserialize,serde::Serialize)]
|
||||
pub struct AssetLocation(
|
||||
// the location is private so users cannot mutate it
|
||||
String
|
||||
);
|
||||
impl AssetLocation{
|
||||
pub fn location(&self)->&str{
|
||||
let Self(location)=self;
|
||||
location
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug,serde::Deserialize)]
|
||||
#[allow(nonstandard_style,dead_code)]
|
||||
pub struct AssetLocationInfo{
|
||||
pub location:Option<AssetLocation>,
|
||||
pub requestId:String,
|
||||
pub IsHashDynamic:bool,
|
||||
pub IsCopyrightProtected:bool,
|
||||
pub isArchived:bool,
|
||||
pub assetTypeId:u32,
|
||||
}
|
||||
|
||||
pub struct AssetVersionsRequest{
|
||||
pub asset_id:u64,
|
||||
pub cursor:Option<String>,
|
||||
@ -203,6 +232,7 @@ pub struct AssetVersionsResponse{
|
||||
#[derive(Debug)]
|
||||
pub enum AssetVersionsError{
|
||||
ParseError(url::ParseError),
|
||||
Response(ResponseError),
|
||||
Reqwest(reqwest::Error),
|
||||
}
|
||||
impl std::fmt::Display for AssetVersionsError{
|
||||
@ -238,6 +268,7 @@ pub struct InventoryPageResponse{
|
||||
#[derive(Debug)]
|
||||
pub enum InventoryPageError{
|
||||
ParseError(url::ParseError),
|
||||
Response(ResponseError),
|
||||
Reqwest(reqwest::Error),
|
||||
}
|
||||
impl std::fmt::Display for InventoryPageError{
|
||||
@ -279,7 +310,7 @@ impl RobloxOperation{
|
||||
None=>self.path.as_deref()?.get(11..),
|
||||
}
|
||||
}
|
||||
pub async fn try_get_reponse(&self,context:&CloudContext)->Result<serde_json::Value,OperationError>{
|
||||
pub async fn try_get_reponse(&self,context:&Context)->Result<serde_json::Value,OperationError>{
|
||||
context.get_asset_operation(GetAssetOperationRequest{
|
||||
operation_id:self.operation_id()
|
||||
.ok_or(OperationError::NoOperationId)?
|
||||
@ -289,25 +320,6 @@ impl RobloxOperation{
|
||||
}
|
||||
}
|
||||
|
||||
//idk how to do this better
|
||||
enum ReaderType<R:std::io::Read>{
|
||||
GZip(flate2::read::GzDecoder<std::io::BufReader<R>>),
|
||||
Raw(std::io::BufReader<R>),
|
||||
}
|
||||
fn maybe_gzip_decode<R:std::io::Read>(input:R)->std::io::Result<ReaderType<R>>{
|
||||
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<Vec<u8>>{
|
||||
let mut contents=Vec::new();
|
||||
readable.read_to_end(&mut contents)?;
|
||||
Ok(contents)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ApiKey(String);
|
||||
impl ApiKey{
|
||||
@ -320,12 +332,12 @@ impl ApiKey{
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CloudContext{
|
||||
pub api_key:String,
|
||||
pub client:reqwest::Client,
|
||||
pub struct Context{
|
||||
api_key:String,
|
||||
client:reqwest::Client,
|
||||
}
|
||||
|
||||
impl CloudContext{
|
||||
impl Context{
|
||||
pub fn new(api_key:ApiKey)->Self{
|
||||
Self{
|
||||
api_key:api_key.get(),
|
||||
@ -368,9 +380,9 @@ impl CloudContext{
|
||||
.text("request",request_config)
|
||||
.part("fileContent",part);
|
||||
|
||||
let operation=self.post_form(url,form).await
|
||||
.map_err(CreateError::Reqwest)?
|
||||
.error_for_status().map_err(CreateError::Reqwest)?
|
||||
let operation=response_ok(
|
||||
self.post_form(url,form).await.map_err(CreateError::Reqwest)?
|
||||
).await.map_err(CreateError::Response)?
|
||||
.json::<RobloxOperation>().await.map_err(CreateError::Reqwest)?;
|
||||
|
||||
Ok(AssetOperation{
|
||||
@ -387,10 +399,9 @@ impl CloudContext{
|
||||
.text("request",request_config)
|
||||
.part("fileContent",reqwest::multipart::Part::bytes(body));
|
||||
|
||||
let operation=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)?
|
||||
let operation=response_ok(
|
||||
self.patch_form(url,form).await.map_err(UpdateError::Reqwest)?
|
||||
).await.map_err(UpdateError::Response)?
|
||||
.json::<RobloxOperation>().await.map_err(UpdateError::Reqwest)?;
|
||||
|
||||
Ok(AssetOperation{
|
||||
@ -401,48 +412,64 @@ impl CloudContext{
|
||||
let raw_url=format!("https://apis.roblox.com/assets/v1/operations/{}",config.operation_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)?
|
||||
response_ok(
|
||||
self.get(url).await.map_err(GetError::Reqwest)?
|
||||
).await.map_err(GetError::Response)?
|
||||
.json::<RobloxOperation>().await.map_err(GetError::Reqwest)
|
||||
}
|
||||
pub async fn get_asset_info(&self,config:GetAssetInfoRequest)->Result<AssetResponse,GetError>{
|
||||
pub async fn get_asset_info(&self,config:GetAssetLatestRequest)->Result<AssetResponse,GetError>{
|
||||
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)?
|
||||
response_ok(
|
||||
self.get(url).await.map_err(GetError::Reqwest)?
|
||||
).await.map_err(GetError::Response)?
|
||||
.json::<AssetResponse>().await.map_err(GetError::Reqwest)
|
||||
}
|
||||
pub async fn get_asset_version(&self,config:GetAssetVersionRequest)->Result<Vec<u8>,GetError>{
|
||||
pub async fn get_asset_version_info(&self,config:GetAssetVersionRequest)->Result<AssetResponse,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)?
|
||||
response_ok(
|
||||
self.get(url).await.map_err(GetError::Reqwest)?
|
||||
).await.map_err(GetError::Response)?
|
||||
.json::<AssetResponse>().await.map_err(GetError::Reqwest)
|
||||
}
|
||||
pub async fn get_asset_location(&self,config:GetAssetLatestRequest)->Result<AssetLocationInfo,GetError>{
|
||||
let raw_url=format!("https://apis.roblox.com/asset-delivery-api/v1/assetId/{}",config.asset_id);
|
||||
let url=reqwest::Url::parse(raw_url.as_str()).map_err(GetError::ParseError)?;
|
||||
|
||||
response_ok(
|
||||
self.get(url).await.map_err(GetError::Reqwest)?
|
||||
).await.map_err(GetError::Response)?
|
||||
.json().await.map_err(GetError::Reqwest)
|
||||
}
|
||||
pub async fn get_asset_version_location(&self,config:GetAssetVersionRequest)->Result<AssetLocationInfo,GetError>{
|
||||
let raw_url=format!("https://apis.roblox.com/asset-delivery-api/v1/assetId/{}/version/{}",config.asset_id,config.version);
|
||||
let url=reqwest::Url::parse(raw_url.as_str()).map_err(GetError::ParseError)?;
|
||||
|
||||
response_ok(
|
||||
self.get(url).await.map_err(GetError::Reqwest)?
|
||||
).await.map_err(GetError::Response)?
|
||||
.json().await.map_err(GetError::Reqwest)
|
||||
}
|
||||
pub async fn get_asset(&self,config:&AssetLocation)->Result<MaybeGzippedBytes,GetError>{
|
||||
let url=reqwest::Url::parse(config.location()).map_err(GetError::ParseError)?;
|
||||
|
||||
let bytes=response_ok(
|
||||
self.get(url).await.map_err(GetError::Reqwest)?
|
||||
).await.map_err(GetError::Response)?
|
||||
.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<Vec<u8>,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
|
||||
Ok(MaybeGzippedBytes::new(bytes))
|
||||
}
|
||||
pub async fn get_asset_versions(&self,config:AssetVersionsRequest)->Result<AssetVersionsResponse,AssetVersionsError>{
|
||||
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)?
|
||||
response_ok(
|
||||
self.get(url).await.map_err(AssetVersionsError::Reqwest)?
|
||||
).await.map_err(AssetVersionsError::Response)?
|
||||
.json::<AssetVersionsResponse>().await.map_err(AssetVersionsError::Reqwest)
|
||||
}
|
||||
pub async fn inventory_page(&self,config:InventoryPageRequest)->Result<InventoryPageResponse,InventoryPageError>{
|
||||
@ -455,8 +482,9 @@ impl CloudContext{
|
||||
}
|
||||
}
|
||||
|
||||
self.get(url).await.map_err(InventoryPageError::Reqwest)?
|
||||
.error_for_status().map_err(InventoryPageError::Reqwest)?
|
||||
response_ok(
|
||||
self.get(url).await.map_err(InventoryPageError::Reqwest)?
|
||||
).await.map_err(InventoryPageError::Response)?
|
||||
.json::<InventoryPageResponse>().await.map_err(InventoryPageError::Reqwest)
|
||||
}
|
||||
pub async fn update_place(&self,config:UpdatePlaceRequest,body:impl Into<reqwest::Body>+Clone)->Result<UpdatePlaceResponse,UpdateError>{
|
||||
@ -468,8 +496,9 @@ impl CloudContext{
|
||||
query.append_pair("versionType","Published");
|
||||
}
|
||||
|
||||
self.post(url,body).await.map_err(UpdateError::Reqwest)?
|
||||
.error_for_status().map_err(UpdateError::Reqwest)?
|
||||
response_ok(
|
||||
self.post(url,body).await.map_err(UpdateError::Reqwest)?
|
||||
).await.map_err(UpdateError::Response)?
|
||||
.json::<UpdatePlaceResponse>().await.map_err(UpdateError::Reqwest)
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,6 @@
|
||||
use crate::util::response_ok;
|
||||
use crate::types::{ResponseError,MaybeGzippedBytes};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PostError{
|
||||
Reqwest(reqwest::Error),
|
||||
@ -23,7 +26,14 @@ pub struct CreateRequest{
|
||||
pub enum CreateError{
|
||||
ParseError(url::ParseError),
|
||||
PostError(PostError),
|
||||
Response(ResponseError),
|
||||
Reqwest(reqwest::Error),
|
||||
ParseInt{
|
||||
response:String,
|
||||
err:std::num::ParseIntError,
|
||||
},
|
||||
VersionHeaderMissing,
|
||||
ToStr(reqwest::header::ToStrError),
|
||||
}
|
||||
impl std::fmt::Display for CreateError{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
@ -46,7 +56,14 @@ pub enum UploadError{
|
||||
ParseError(url::ParseError),
|
||||
PostError(PostError),
|
||||
Reqwest(reqwest::Error),
|
||||
Response(ResponseError),
|
||||
AssetIdIsZero,
|
||||
ParseInt{
|
||||
response:String,
|
||||
err:std::num::ParseIntError,
|
||||
},
|
||||
VersionHeaderMissing,
|
||||
ToStr(reqwest::header::ToStrError),
|
||||
}
|
||||
impl std::fmt::Display for UploadError{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
@ -58,9 +75,13 @@ impl std::error::Error for UploadError{}
|
||||
#[allow(nonstandard_style,dead_code)]
|
||||
pub struct UploadResponse{
|
||||
pub AssetId:u64,
|
||||
pub AssetVersionId:u64,
|
||||
pub AssetVersion:u64,
|
||||
}
|
||||
|
||||
#[allow(nonstandard_style,dead_code)]
|
||||
pub struct GetAssetDetailsRequest{
|
||||
pub asset_id:u64,
|
||||
}
|
||||
#[allow(nonstandard_style,dead_code)]
|
||||
pub struct GetAssetRequest{
|
||||
pub asset_id:u64,
|
||||
@ -69,8 +90,8 @@ pub struct GetAssetRequest{
|
||||
#[derive(Debug)]
|
||||
pub enum GetError{
|
||||
ParseError(url::ParseError),
|
||||
Response(ResponseError),
|
||||
Reqwest(reqwest::Error),
|
||||
IO(std::io::Error)
|
||||
}
|
||||
impl std::fmt::Display for GetError{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
@ -79,6 +100,108 @@ impl std::fmt::Display for GetError{
|
||||
}
|
||||
impl std::error::Error for GetError{}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum GetAssetV2Error{
|
||||
ParseError(url::ParseError),
|
||||
Response(ResponseError),
|
||||
VersionHeaderMissing,
|
||||
ToStr(reqwest::header::ToStrError),
|
||||
ParseInt(std::num::ParseIntError),
|
||||
Reqwest(reqwest::Error),
|
||||
}
|
||||
impl std::fmt::Display for GetAssetV2Error{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f,"{self:?}")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for GetAssetV2Error{}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[allow(nonstandard_style,dead_code)]
|
||||
pub struct GetAssetV2AssetMetadata{
|
||||
pub metadataType:u32,
|
||||
pub value:String,
|
||||
}
|
||||
#[derive(serde::Deserialize)]
|
||||
#[allow(nonstandard_style,dead_code)]
|
||||
pub struct GetAssetV2Location{
|
||||
pub assetFormat:String,// "source"
|
||||
location:String,// this value is private so users cannot mutate it
|
||||
#[serde(default)]
|
||||
pub assetMetadatas:Vec<GetAssetV2AssetMetadata>,
|
||||
}
|
||||
impl GetAssetV2Location{
|
||||
pub fn location(&self)->&str{
|
||||
&self.location
|
||||
}
|
||||
}
|
||||
#[derive(serde::Deserialize)]
|
||||
#[allow(nonstandard_style,dead_code)]
|
||||
pub struct GetAssetV2Info{
|
||||
pub locations:Vec<GetAssetV2Location>,
|
||||
pub requestId:String,
|
||||
pub IsHashDynamic:bool,
|
||||
pub IsCopyrightProtected:bool,
|
||||
pub isArchived:bool,
|
||||
pub assetTypeId:u32,
|
||||
}
|
||||
|
||||
pub struct GetAssetV2{
|
||||
pub version:u64,
|
||||
pub info:GetAssetV2Info,
|
||||
}
|
||||
|
||||
#[derive(Clone,Copy,Debug,Eq,PartialEq,Hash)]
|
||||
#[derive(serde::Deserialize,serde::Serialize)]
|
||||
pub enum CreatorType{
|
||||
User,
|
||||
Group,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(serde::Deserialize)]
|
||||
#[allow(nonstandard_style,dead_code)]
|
||||
pub struct Creator{
|
||||
pub Id:u64,
|
||||
pub Name:String,
|
||||
pub CreatorType:CreatorType,
|
||||
pub CreatorTargetId:u64,
|
||||
pub HasVerifiedBadge:bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(serde::Deserialize)]
|
||||
#[allow(nonstandard_style,dead_code)]
|
||||
pub struct AssetDetails{
|
||||
pub TargetId:u64,
|
||||
pub ProductType:Option<String>,
|
||||
pub AssetId:u64,
|
||||
pub ProductId:u64,
|
||||
pub Name:String,
|
||||
pub Description:String,
|
||||
pub AssetTypeId:u32,
|
||||
pub Creator:Creator,
|
||||
pub IconImageAssetId:u64,
|
||||
pub Created:chrono::DateTime<chrono::Utc>,
|
||||
pub Updated:chrono::DateTime<chrono::Utc>,
|
||||
pub PriceInRobux:Option<u32>,
|
||||
pub PriceInTickets:Option<u32>,
|
||||
pub Sales:u32,
|
||||
pub IsNew:bool,
|
||||
pub IsForSale:bool,
|
||||
pub IsPublicDomain:bool,
|
||||
pub IsLimited:bool,
|
||||
pub IsLimitedUnique:bool,
|
||||
pub Remaining:Option<u32>,
|
||||
pub MinimumMembershipLevel:u32,
|
||||
pub ContentRatingTypeId:u32,
|
||||
pub SaleAvailabilityLocations:Option<String>,
|
||||
pub SaleLocation:Option<String>,
|
||||
pub CollectibleItemId:Option<u64>,
|
||||
pub CollectibleProductId:Option<u64>,
|
||||
pub CollectiblesItemDetails:Option<String>,
|
||||
}
|
||||
|
||||
pub struct AssetVersionsPageRequest{
|
||||
pub asset_id:u64,
|
||||
pub cursor:Option<String>,
|
||||
@ -89,7 +212,7 @@ pub struct AssetVersion{
|
||||
pub Id:u64,
|
||||
pub assetId:u64,
|
||||
pub assetVersionNumber:u64,
|
||||
pub creatorType:String,
|
||||
pub creatorType:CreatorType,
|
||||
pub creatorTargetId:u64,
|
||||
pub creatingUniverseId:Option<u64>,
|
||||
pub created:chrono::DateTime<chrono::Utc>,
|
||||
@ -103,30 +226,43 @@ pub struct AssetVersionsPageResponse{
|
||||
pub data:Vec<AssetVersion>,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub enum AssetVersionsPageError{
|
||||
pub enum PageError{
|
||||
ParseError(url::ParseError),
|
||||
Response(ResponseError),
|
||||
Reqwest(reqwest::Error),
|
||||
}
|
||||
impl std::fmt::Display for AssetVersionsPageError{
|
||||
impl std::fmt::Display for PageError{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f,"{self:?}")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for AssetVersionsPageError{}
|
||||
impl std::error::Error for PageError{}
|
||||
|
||||
pub struct InventoryPageRequest{
|
||||
pub group:u64,
|
||||
pub enum Owner{
|
||||
User(u64),
|
||||
Group(u64),
|
||||
}
|
||||
impl Owner{
|
||||
fn get_url_info(&self)->(&str,u64){
|
||||
match self{
|
||||
&Owner::User(id)=>("user",id),
|
||||
&Owner::Group(id)=>("group",id),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub struct CreationsPageRequest{
|
||||
pub owner:Owner,
|
||||
pub cursor:Option<String>,
|
||||
}
|
||||
#[derive(serde::Deserialize,serde::Serialize)]
|
||||
#[allow(nonstandard_style,dead_code)]
|
||||
pub struct InventoryItem{
|
||||
pub struct CreationsItem{
|
||||
pub id:u64,
|
||||
pub name:String,
|
||||
}
|
||||
#[derive(serde::Deserialize,serde::Serialize)]
|
||||
#[allow(nonstandard_style,dead_code)]
|
||||
pub struct InventoryPageResponse{
|
||||
pub struct CreationsPageResponse{
|
||||
pub totalResults:u64,//up to 50
|
||||
pub filteredKeyword:Option<String>,//""
|
||||
pub searchDebugInfo:Option<String>,//null
|
||||
@ -135,56 +271,59 @@ pub struct InventoryPageResponse{
|
||||
pub imageSearchStatus:Option<String>,//null
|
||||
pub previousPageCursor:Option<String>,
|
||||
pub nextPageCursor:Option<String>,
|
||||
pub data:Vec<InventoryItem>,
|
||||
pub data:Vec<CreationsItem>,
|
||||
}
|
||||
#[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<R:std::io::Read>{
|
||||
GZip(flate2::read::GzDecoder<std::io::BufReader<R>>),
|
||||
Raw(std::io::BufReader<R>),
|
||||
pub struct UserInventoryPageRequest{
|
||||
pub user_id:u64,
|
||||
pub cursor:Option<String>,
|
||||
}
|
||||
fn maybe_gzip_decode<R:std::io::Read>(input:R)->std::io::Result<ReaderType<R>>{
|
||||
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)),
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize,serde::Serialize)]
|
||||
#[allow(nonstandard_style,dead_code)]
|
||||
pub struct UserInventoryItemOwner{
|
||||
userId:u64,
|
||||
username:String,
|
||||
buildersClubMembershipType:u64,
|
||||
}
|
||||
fn read_readable(mut readable:impl std::io::Read)->std::io::Result<Vec<u8>>{
|
||||
let mut contents=Vec::new();
|
||||
readable.read_to_end(&mut contents)?;
|
||||
Ok(contents)
|
||||
#[derive(serde::Deserialize,serde::Serialize)]
|
||||
#[allow(nonstandard_style,dead_code)]
|
||||
pub struct UserInventoryItem{
|
||||
userAssetId:u64,
|
||||
assetId:u64,
|
||||
assetName:String,
|
||||
collectibleItemId:Option<String>,
|
||||
collectibleItemInstanceId:Option<String>,
|
||||
owner:UserInventoryItemOwner,
|
||||
created:chrono::DateTime<chrono::Utc>,
|
||||
updated:chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
#[derive(serde::Deserialize,serde::Serialize)]
|
||||
#[allow(nonstandard_style,dead_code)]
|
||||
pub struct UserInventoryPageResponse{
|
||||
pub previousPageCursor:Option<String>,
|
||||
pub nextPageCursor:Option<String>,
|
||||
pub data:Vec<UserInventoryItem>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Cookie(String);
|
||||
impl Cookie{
|
||||
/// cookie is prepended with ".ROBLOSECURITY=" by this function
|
||||
pub fn new(cookie:String)->Self{
|
||||
Self(cookie)
|
||||
Self(format!(".ROBLOSECURITY={cookie}"))
|
||||
}
|
||||
pub fn get(self)->String{
|
||||
self.0
|
||||
}
|
||||
}
|
||||
#[derive(Clone)]
|
||||
pub struct CookieContext{
|
||||
pub cookie:String,
|
||||
pub client:reqwest::Client,
|
||||
pub struct Context{
|
||||
cookie:String,
|
||||
client:reqwest::Client,
|
||||
}
|
||||
|
||||
impl CookieContext{
|
||||
impl Context{
|
||||
pub fn new(cookie:Cookie)->Self{
|
||||
Self{
|
||||
cookie:cookie.get(),
|
||||
@ -232,10 +371,31 @@ impl CookieContext{
|
||||
query.append_pair("groupId",group_id.to_string().as_str());
|
||||
}
|
||||
}
|
||||
let response=response_ok(
|
||||
self.post(url,body).await.map_err(CreateError::PostError)?
|
||||
).await.map_err(CreateError::Response)?;
|
||||
|
||||
self.post(url,body).await.map_err(CreateError::PostError)?
|
||||
.error_for_status().map_err(CreateError::Reqwest)?
|
||||
.json::<UploadResponse>().await.map_err(CreateError::Reqwest)
|
||||
let version_str=response
|
||||
.headers()
|
||||
.get("roblox-assetversionnumber")
|
||||
.ok_or(CreateError::VersionHeaderMissing)?
|
||||
.to_str()
|
||||
.map_err(CreateError::ToStr)?;
|
||||
let version=version_str.parse()
|
||||
.map_err(|err|CreateError::ParseInt{err,response:version_str.to_owned()})?;
|
||||
|
||||
let response=response.text().await.map_err(CreateError::Reqwest)?;
|
||||
|
||||
match response.parse(){
|
||||
Ok(asset_id)=>Ok(UploadResponse{
|
||||
AssetId:asset_id,
|
||||
AssetVersion:version,
|
||||
}),
|
||||
Err(err)=>Err(CreateError::ParseInt{
|
||||
response,
|
||||
err,
|
||||
})
|
||||
}
|
||||
}
|
||||
pub async fn upload(&self,config:UploadRequest,body:impl Into<reqwest::Body>+Clone)->Result<UploadResponse,UploadError>{
|
||||
let mut url=reqwest::Url::parse("https://data.roblox.com/Data/Upload.ashx?json=1&type=Model&genreTypeId=1").map_err(UploadError::ParseError)?;
|
||||
@ -263,12 +423,33 @@ impl CookieContext{
|
||||
query.append_pair("groupId",group_id.to_string().as_str());
|
||||
}
|
||||
}
|
||||
let response=response_ok(
|
||||
self.post(url,body).await.map_err(UploadError::PostError)?
|
||||
).await.map_err(UploadError::Response)?;
|
||||
|
||||
self.post(url,body).await.map_err(UploadError::PostError)?
|
||||
.error_for_status().map_err(UploadError::Reqwest)?
|
||||
.json::<UploadResponse>().await.map_err(UploadError::Reqwest)
|
||||
let version_str=response
|
||||
.headers()
|
||||
.get("roblox-assetversionnumber")
|
||||
.ok_or(UploadError::VersionHeaderMissing)?
|
||||
.to_str()
|
||||
.map_err(UploadError::ToStr)?;
|
||||
let version=version_str.parse()
|
||||
.map_err(|err|UploadError::ParseInt{err,response:version_str.to_owned()})?;
|
||||
|
||||
let response=response.text().await.map_err(UploadError::Reqwest)?;
|
||||
|
||||
match response.parse(){
|
||||
Ok(asset_id)=>Ok(UploadResponse{
|
||||
AssetId:asset_id,
|
||||
AssetVersion:version,
|
||||
}),
|
||||
Err(err)=>Err(UploadError::ParseInt{
|
||||
response,
|
||||
err,
|
||||
})
|
||||
}
|
||||
}
|
||||
pub async fn get_asset(&self,config:GetAssetRequest)->Result<Vec<u8>,GetError>{
|
||||
pub async fn get_asset(&self,config:GetAssetRequest)->Result<MaybeGzippedBytes,GetError>{
|
||||
let mut url=reqwest::Url::parse("https://assetdelivery.roblox.com/v1/asset/").map_err(GetError::ParseError)?;
|
||||
//url borrow scope
|
||||
{
|
||||
@ -278,18 +459,63 @@ impl CookieContext{
|
||||
query.append_pair("version",version.to_string().as_str());
|
||||
}
|
||||
}
|
||||
let body=self.get(url).await.map_err(GetError::Reqwest)?
|
||||
.error_for_status().map_err(GetError::Reqwest)?
|
||||
|
||||
let bytes=response_ok(
|
||||
self.get(url).await.map_err(GetError::Reqwest)?
|
||||
).await.map_err(GetError::Response)?
|
||||
.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)
|
||||
Ok(MaybeGzippedBytes::new(bytes))
|
||||
}
|
||||
pub async fn get_asset_versions_page(&self,config:AssetVersionsPageRequest)->Result<AssetVersionsPageResponse,AssetVersionsPageError>{
|
||||
let mut url=reqwest::Url::parse(format!("https://develop.roblox.com/v1/assets/{}/saved-versions",config.asset_id).as_str()).map_err(AssetVersionsPageError::ParseError)?;
|
||||
pub async fn get_asset_v2(&self,config:GetAssetRequest)->Result<GetAssetV2,GetAssetV2Error>{
|
||||
let mut url=reqwest::Url::parse("https://assetdelivery.roblox.com/v2/asset").map_err(GetAssetV2Error::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 response=response_ok(
|
||||
self.get(url).await.map_err(GetAssetV2Error::Reqwest)?
|
||||
).await.map_err(GetAssetV2Error::Response)?;
|
||||
|
||||
let version=response
|
||||
.headers()
|
||||
.get("roblox-assetversionnumber")
|
||||
.ok_or(GetAssetV2Error::VersionHeaderMissing)?
|
||||
.to_str()
|
||||
.map_err(GetAssetV2Error::ToStr)?
|
||||
.parse()
|
||||
.map_err(GetAssetV2Error::ParseInt)?;
|
||||
|
||||
let info=response.json().await.map_err(GetAssetV2Error::Reqwest)?;
|
||||
|
||||
Ok(GetAssetV2{
|
||||
version,
|
||||
info,
|
||||
})
|
||||
}
|
||||
pub async fn get_asset_v2_download(&self,config:&GetAssetV2Location)->Result<MaybeGzippedBytes,GetError>{
|
||||
let url=reqwest::Url::parse(config.location.as_str()).map_err(GetError::ParseError)?;
|
||||
|
||||
let bytes=response_ok(
|
||||
self.get(url).await.map_err(GetError::Reqwest)?
|
||||
).await.map_err(GetError::Response)?
|
||||
.bytes().await.map_err(GetError::Reqwest)?;
|
||||
|
||||
Ok(MaybeGzippedBytes::new(bytes))
|
||||
}
|
||||
pub async fn get_asset_details(&self,config:GetAssetDetailsRequest)->Result<AssetDetails,GetError>{
|
||||
let url=reqwest::Url::parse(format!("https://economy.roblox.com/v2/assets/{}/details",config.asset_id).as_str()).map_err(GetError::ParseError)?;
|
||||
response_ok(
|
||||
self.get(url).await.map_err(GetError::Reqwest)?
|
||||
).await.map_err(GetError::Response)?
|
||||
.json().await.map_err(GetError::Reqwest)
|
||||
}
|
||||
pub async fn get_asset_versions_page(&self,config:AssetVersionsPageRequest)->Result<AssetVersionsPageResponse,PageError>{
|
||||
let mut url=reqwest::Url::parse(format!("https://develop.roblox.com/v1/assets/{}/saved-versions",config.asset_id).as_str()).map_err(PageError::ParseError)?;
|
||||
//url borrow scope
|
||||
{
|
||||
let mut query=url.query_pairs_mut();//borrow here
|
||||
@ -300,13 +526,14 @@ impl CookieContext{
|
||||
query.append_pair("cursor",cursor);
|
||||
}
|
||||
}
|
||||
|
||||
self.get(url).await.map_err(AssetVersionsPageError::Reqwest)?
|
||||
.error_for_status().map_err(AssetVersionsPageError::Reqwest)?
|
||||
.json::<AssetVersionsPageResponse>().await.map_err(AssetVersionsPageError::Reqwest)
|
||||
response_ok(
|
||||
self.get(url).await.map_err(PageError::Reqwest)?
|
||||
).await.map_err(PageError::Response)?
|
||||
.json::<AssetVersionsPageResponse>().await.map_err(PageError::Reqwest)
|
||||
}
|
||||
pub async fn get_inventory_page(&self,config:InventoryPageRequest)->Result<InventoryPageResponse,InventoryPageError>{
|
||||
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)?;
|
||||
pub async fn get_creations_page(&self,config:&CreationsPageRequest)->Result<CreationsPageResponse,PageError>{
|
||||
let (owner,id)=config.owner.get_url_info();
|
||||
let mut url=reqwest::Url::parse(format!("https://apis.roblox.com/toolbox-service/v1/creations/{}/{}/10?limit=50",owner,id).as_str()).map_err(PageError::ParseError)?;
|
||||
//url borrow scope
|
||||
{
|
||||
let mut query=url.query_pairs_mut();//borrow here
|
||||
@ -314,9 +541,23 @@ impl CookieContext{
|
||||
query.append_pair("cursor",cursor);
|
||||
}
|
||||
}
|
||||
|
||||
self.get(url).await.map_err(InventoryPageError::Reqwest)?
|
||||
.error_for_status().map_err(InventoryPageError::Reqwest)?
|
||||
.json::<InventoryPageResponse>().await.map_err(InventoryPageError::Reqwest)
|
||||
response_ok(
|
||||
self.get(url).await.map_err(PageError::Reqwest)?
|
||||
).await.map_err(PageError::Response)?
|
||||
.json::<CreationsPageResponse>().await.map_err(PageError::Reqwest)
|
||||
}
|
||||
pub async fn get_user_inventory_page(&self,config:&UserInventoryPageRequest)->Result<UserInventoryPageResponse,PageError>{
|
||||
let mut url=reqwest::Url::parse(format!("https://inventory.roblox.com/v2/users/{}/inventory/10?limit=100&sortOrder=Desc",config.user_id).as_str()).map_err(PageError::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);
|
||||
}
|
||||
}
|
||||
response_ok(
|
||||
self.get(url).await.map_err(PageError::Reqwest)?
|
||||
).await.map_err(PageError::Response)?
|
||||
.json::<UserInventoryPageResponse>().await.map_err(PageError::Reqwest)
|
||||
}
|
||||
}
|
||||
|
@ -1,2 +1,4 @@
|
||||
pub mod cloud;
|
||||
pub mod cookie;
|
||||
pub mod types;
|
||||
mod util;
|
||||
|
68
rbx_asset/src/types.rs
Normal file
68
rbx_asset/src/types.rs
Normal file
@ -0,0 +1,68 @@
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub struct StatusCodeWithUrlAndBody{
|
||||
pub status_code:reqwest::StatusCode,
|
||||
pub url:url::Url,
|
||||
pub body:String,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub enum ResponseError{
|
||||
Reqwest(reqwest::Error),
|
||||
StatusCodeWithUrlAndBody(StatusCodeWithUrlAndBody),
|
||||
}
|
||||
impl std::fmt::Display for ResponseError{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f,"{self:?}")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for ResponseError{}
|
||||
|
||||
#[cfg(feature="gzip")]
|
||||
use std::io::Cursor;
|
||||
#[cfg(feature="gzip")]
|
||||
use flate2::read::GzDecoder;
|
||||
|
||||
/// Some bytes that might be gzipped. Use the read_with or to_vec methods to transparently decode gzip.
|
||||
pub struct MaybeGzippedBytes{
|
||||
bytes:bytes::Bytes,
|
||||
}
|
||||
impl MaybeGzippedBytes{
|
||||
pub(crate) fn new(bytes:bytes::Bytes)->Self{
|
||||
Self{bytes}
|
||||
}
|
||||
pub fn into_inner(self)->bytes::Bytes{
|
||||
self.bytes
|
||||
}
|
||||
/// get a reference to the bytes, ignoring gzip decoding
|
||||
pub fn as_raw_ref(&self)->&[u8]{
|
||||
self.bytes.as_ref()
|
||||
}
|
||||
/// Transparently decode gzip data, if present (intermediate allocation)
|
||||
#[cfg(feature="gzip")]
|
||||
pub fn to_vec(&self)->std::io::Result<Vec<u8>>{
|
||||
use std::io::Read;
|
||||
match self.bytes.get(0..2){
|
||||
Some(b"\x1f\x8b")=>{
|
||||
let mut buf=Vec::new();
|
||||
GzDecoder::new(Cursor::new(self.bytes.as_ref())).read_to_end(&mut buf)?;
|
||||
Ok(buf)
|
||||
},
|
||||
_=>Ok(self.bytes.to_vec())
|
||||
}
|
||||
}
|
||||
/// Read the bytes with the provided decoders.
|
||||
/// The idea is to make a function that is generic over std::io::Read
|
||||
/// and pass the same function to both closures.
|
||||
/// This two closure hack must be done because of the different concrete types.
|
||||
#[cfg(feature="gzip")]
|
||||
pub fn read_with<'a,ReadGzip,ReadRaw,T>(&'a self,read_gzip:ReadGzip,read_raw:ReadRaw)->T
|
||||
where
|
||||
ReadGzip:Fn(GzDecoder<Cursor<&'a [u8]>>)->T,
|
||||
ReadRaw:Fn(Cursor<&'a [u8]>)->T,
|
||||
{
|
||||
match self.bytes.get(0..2){
|
||||
Some(b"\x1f\x8b")=>read_gzip(GzDecoder::new(Cursor::new(self.bytes.as_ref()))),
|
||||
_=>read_raw(Cursor::new(self.bytes.as_ref()))
|
||||
}
|
||||
}
|
||||
}
|
40
rbx_asset/src/util.rs
Normal file
40
rbx_asset/src/util.rs
Normal file
@ -0,0 +1,40 @@
|
||||
use crate::types::{ResponseError,StatusCodeWithUrlAndBody};
|
||||
|
||||
// lazy function to draw out meaningful info from http response on failure
|
||||
pub(crate) async fn response_ok(response:reqwest::Response)->Result<reqwest::Response,ResponseError>{
|
||||
let status_code=response.status();
|
||||
if status_code.is_success(){
|
||||
Ok(response)
|
||||
}else{
|
||||
let url=response.url().to_owned();
|
||||
let bytes=response.bytes().await.map_err(ResponseError::Reqwest)?;
|
||||
let body=String::from_utf8_lossy(&bytes).to_string();
|
||||
Err(ResponseError::StatusCodeWithUrlAndBody(StatusCodeWithUrlAndBody{
|
||||
status_code,
|
||||
url,
|
||||
body,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
use serde::de::{Error,Unexpected};
|
||||
use serde::{Deserializer,Serializer};
|
||||
|
||||
struct U64StringVisitor;
|
||||
impl serde::de::Visitor<'_> for U64StringVisitor{
|
||||
type Value=u64;
|
||||
fn expecting(&self,formatter:&mut std::fmt::Formatter)->std::fmt::Result{
|
||||
write!(formatter,"string value with int")
|
||||
}
|
||||
fn visit_str<E:Error>(self,v:&str)->Result<Self::Value,E>{
|
||||
v.parse().map_err(|_|E::invalid_value(Unexpected::Str(v),&"u64"))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn deserialize_u64<'de,D:Deserializer<'de>>(deserializer:D)->Result<u64,D::Error>{
|
||||
deserializer.deserialize_any(U64StringVisitor)
|
||||
}
|
||||
|
||||
pub(crate) fn serialize_u64<S:Serializer>(v:&u64,serializer:S)->Result<S::Ok,S::Error>{
|
||||
serializer.serialize_str(v.to_string().as_str())
|
||||
}
|
@ -2,11 +2,16 @@
|
||||
name = "rox_compiler"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = ["strafesnet"]
|
||||
repository = "https://git.itzana.me/StrafesNET/asset-tool"
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "Read/write a folder structure from Roblox place/model files."
|
||||
authors = ["Rhys Lloyd <krakow20@gmail.com>"]
|
||||
|
||||
[dependencies]
|
||||
futures = "0.3.30"
|
||||
lazy-regex = "3.1.0"
|
||||
rayon = "1.8.0"
|
||||
rbx_dom_weak = "2.7.0"
|
||||
rbx_xml = "0.13.3"
|
||||
rbx_dom_weak = "3.0.0"
|
||||
rbx_xml = "1.0.0"
|
||||
tokio = { version = "1.35.1", features = ["fs"] }
|
||||
|
176
rox_compiler/LICENSE-APACHE
Normal file
176
rox_compiler/LICENSE-APACHE
Normal file
@ -0,0 +1,176 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
23
rox_compiler/LICENSE-MIT
Normal file
23
rox_compiler/LICENSE-MIT
Normal file
@ -0,0 +1,23 @@
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
@ -1,4 +1,5 @@
|
||||
use std::path::PathBuf;
|
||||
use rbx_dom_weak::ustr;
|
||||
use rbx_dom_weak::types::Ref;
|
||||
use crate::common::{sanitize,Style,PropertiesOverride};
|
||||
|
||||
@ -90,7 +91,7 @@ fn write_item(dom:&rbx_dom_weak::WeakDom,mut file:PathBuf,node:&TreeNode,node_na
|
||||
|
||||
if let Some(item)=dom.get_by_ref(node.referent){
|
||||
//TODO: delete disabled scripts
|
||||
if let Some(rbx_dom_weak::types::Variant::String(source))=item.properties.get("Source"){
|
||||
if let Some(rbx_dom_weak::types::Variant::String(source))=item.properties.get(&ustr("Source")){
|
||||
if properties.is_some(){
|
||||
//rox style
|
||||
let source=properties.to_string()+source.as_str();
|
||||
|
306
src/main.rs
306
src/main.rs
@ -2,8 +2,8 @@ use std::{io::Read,path::PathBuf};
|
||||
use clap::{Args,Parser,Subcommand};
|
||||
use anyhow::{anyhow,Result as AResult};
|
||||
use futures::StreamExt;
|
||||
use rbx_asset::cloud::{ApiKey,CloudContext};
|
||||
use rbx_asset::cookie::{Cookie,CookieContext,AssetVersion,InventoryItem};
|
||||
use rbx_asset::cloud::{ApiKey,Context as CloudContext};
|
||||
use rbx_asset::cookie::{Cookie,Context as CookieContext,AssetVersion,CreationsItem};
|
||||
|
||||
type AssetID=u64;
|
||||
type AssetIDFileMap=Vec<(AssetID,PathBuf)>;
|
||||
@ -20,10 +20,14 @@ struct Cli{
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands{
|
||||
AssetDetails(AssetDetailsSubcommand),
|
||||
DownloadHistory(DownloadHistorySubcommand),
|
||||
Download(DownloadSubcommand),
|
||||
DownloadVersion(DownloadVersionSubcommand),
|
||||
DownloadVersionV2(DownloadVersionSubcommand),
|
||||
DownloadDecompile(DownloadDecompileSubcommand),
|
||||
DownloadGroupInventoryJson(DownloadGroupInventoryJsonSubcommand),
|
||||
DownloadCreationsJson(DownloadCreationsJsonSubcommand),
|
||||
DownloadUserInventoryJson(DownloadUserInventoryJsonSubcommand),
|
||||
CreateAsset(CreateAssetSubcommand),
|
||||
CreateAssetMedia(CreateAssetMediaSubcommand),
|
||||
CreateAssetMedias(CreateAssetMediasSubcommand),
|
||||
@ -58,7 +62,19 @@ struct DownloadHistorySubcommand{
|
||||
#[arg(long)]
|
||||
end_version:Option<u64>,
|
||||
}
|
||||
/// Download a single asset by id.
|
||||
/// Print the details for an asset
|
||||
#[derive(Args)]
|
||||
struct AssetDetailsSubcommand{
|
||||
#[arg(long,group="cookie",required=true)]
|
||||
cookie_literal:Option<String>,
|
||||
#[arg(long,group="cookie",required=true)]
|
||||
cookie_envvar:Option<String>,
|
||||
#[arg(long,group="cookie",required=true)]
|
||||
cookie_file:Option<PathBuf>,
|
||||
#[arg(required=true)]
|
||||
asset_id:AssetID,
|
||||
}
|
||||
/// Download a list of assets by id.
|
||||
#[derive(Args)]
|
||||
struct DownloadSubcommand{
|
||||
#[arg(long,group="cookie",required=true)]
|
||||
@ -72,9 +88,9 @@ struct DownloadSubcommand{
|
||||
#[arg(required=true)]
|
||||
asset_ids:Vec<AssetID>,
|
||||
}
|
||||
/// Download the list of asset ids (not the assets themselves) in a group inventory. The output is written to `output_folder/versions.json`
|
||||
/// Download a single asset by id, optionally specifying the version to download.
|
||||
#[derive(Args)]
|
||||
struct DownloadGroupInventoryJsonSubcommand{
|
||||
struct DownloadVersionSubcommand{
|
||||
#[arg(long,group="cookie",required=true)]
|
||||
cookie_literal:Option<String>,
|
||||
#[arg(long,group="cookie",required=true)]
|
||||
@ -84,7 +100,41 @@ struct DownloadGroupInventoryJsonSubcommand{
|
||||
#[arg(long)]
|
||||
output_folder:Option<PathBuf>,
|
||||
#[arg(long)]
|
||||
group:u64,
|
||||
asset_id:AssetID,
|
||||
#[arg(long)]
|
||||
asset_version:Option<u64>,
|
||||
}
|
||||
/// Download the list of asset ids (not the assets themselves) created by a group or user. The output is written to `output_folder/versions.json`
|
||||
#[derive(Args)]
|
||||
struct DownloadCreationsJsonSubcommand{
|
||||
#[arg(long,group="cookie",required=true)]
|
||||
cookie_literal:Option<String>,
|
||||
#[arg(long,group="cookie",required=true)]
|
||||
cookie_envvar:Option<String>,
|
||||
#[arg(long,group="cookie",required=true)]
|
||||
cookie_file:Option<PathBuf>,
|
||||
#[arg(long)]
|
||||
output_folder:Option<PathBuf>,
|
||||
#[arg(long,group="owner",required=true)]
|
||||
group_id:Option<u64>,
|
||||
#[arg(long,group="owner",required=true)]
|
||||
user_id:Option<u64>,
|
||||
}
|
||||
/// Download the list of asset ids (not the assets themselves) in a user's inventory. The output is written to `output_folder/versions.json`
|
||||
#[derive(Args)]
|
||||
struct DownloadUserInventoryJsonSubcommand{
|
||||
#[arg(long,group="cookie",required=true)]
|
||||
cookie_literal:Option<String>,
|
||||
#[arg(long,group="cookie",required=true)]
|
||||
cookie_envvar:Option<String>,
|
||||
#[arg(long,group="cookie",required=true)]
|
||||
cookie_file:Option<PathBuf>,
|
||||
#[arg(long)]
|
||||
output_folder:Option<PathBuf>,
|
||||
#[arg(long)]
|
||||
user_id:u64,
|
||||
#[arg(long)]
|
||||
continue_from_cursor:Option<bool>,
|
||||
}
|
||||
/// Upload a (.rbxm, .rbxmx) model file, creating a new asset. Can be any type of model, including modulescripts.
|
||||
#[derive(Args)]
|
||||
@ -120,7 +170,7 @@ struct CreateAssetMediaSubcommand{
|
||||
#[arg(long)]
|
||||
model_name:String,
|
||||
#[arg(long)]
|
||||
description:Option<String>,
|
||||
description:String,
|
||||
#[arg(long)]
|
||||
input_file:PathBuf,
|
||||
#[arg(long)]
|
||||
@ -308,8 +358,8 @@ struct DownloadDecompileSubcommand{
|
||||
struct DecompileHistoryIntoGitSubcommand{
|
||||
#[arg(long)]
|
||||
input_folder:PathBuf,
|
||||
//currently output folder must be the current folder due to git2 limitations
|
||||
//output_folder:cli.output.unwrap(),
|
||||
#[arg(long)]
|
||||
output_folder:Option<PathBuf>,
|
||||
#[arg(long)]
|
||||
style:Style,
|
||||
#[arg(long)]
|
||||
@ -334,8 +384,8 @@ struct DownloadAndDecompileHistoryIntoGitSubcommand{
|
||||
cookie_envvar:Option<String>,
|
||||
#[arg(long,group="cookie",required=true)]
|
||||
cookie_file:Option<PathBuf>,
|
||||
//currently output folder must be the current folder due to git2 limitations
|
||||
//output_folder:cli.output.unwrap(),
|
||||
#[arg(long)]
|
||||
output_folder:Option<PathBuf>,
|
||||
#[arg(long)]
|
||||
style:Style,
|
||||
#[arg(long)]
|
||||
@ -385,6 +435,16 @@ impl AssetType{
|
||||
async fn main()->AResult<()>{
|
||||
let cli=Cli::parse();
|
||||
match cli.command{
|
||||
Commands::AssetDetails(subcommand)=>{
|
||||
asset_details(
|
||||
cookie_from_args(
|
||||
subcommand.cookie_literal,
|
||||
subcommand.cookie_envvar,
|
||||
subcommand.cookie_file,
|
||||
).await?,
|
||||
subcommand.asset_id
|
||||
).await
|
||||
},
|
||||
Commands::DownloadHistory(subcommand)=>download_history(DownloadHistoryConfig{
|
||||
continue_from_versions:subcommand.continue_from_versions.unwrap_or(false),
|
||||
end_version:subcommand.end_version,
|
||||
@ -412,6 +472,40 @@ async fn main()->AResult<()>{
|
||||
}).collect()
|
||||
).await
|
||||
},
|
||||
Commands::DownloadVersion(subcommand)=>{
|
||||
let output_folder=subcommand.output_folder.unwrap_or_else(||std::env::current_dir().unwrap());
|
||||
download_version(
|
||||
cookie_from_args(
|
||||
subcommand.cookie_literal,
|
||||
subcommand.cookie_envvar,
|
||||
subcommand.cookie_file,
|
||||
).await?,
|
||||
subcommand.asset_id,
|
||||
subcommand.asset_version,
|
||||
{
|
||||
let mut path=output_folder.clone();
|
||||
path.push(subcommand.asset_id.to_string());
|
||||
path
|
||||
},
|
||||
).await
|
||||
},
|
||||
Commands::DownloadVersionV2(subcommand)=>{
|
||||
let output_folder=subcommand.output_folder.unwrap_or_else(||std::env::current_dir().unwrap());
|
||||
download_version_v2(
|
||||
cookie_from_args(
|
||||
subcommand.cookie_literal,
|
||||
subcommand.cookie_envvar,
|
||||
subcommand.cookie_file,
|
||||
).await?,
|
||||
subcommand.asset_id,
|
||||
subcommand.asset_version,
|
||||
{
|
||||
let mut path=output_folder.clone();
|
||||
path.push(subcommand.asset_id.to_string());
|
||||
path
|
||||
},
|
||||
).await
|
||||
},
|
||||
Commands::DownloadDecompile(subcommand)=>{
|
||||
download_decompile(DownloadDecompileConfig{
|
||||
cookie:cookie_from_args(
|
||||
@ -427,15 +521,28 @@ async fn main()->AResult<()>{
|
||||
write_scripts:subcommand.write_scripts.unwrap_or(true),
|
||||
}).await
|
||||
},
|
||||
Commands::DownloadGroupInventoryJson(subcommand)=>download_group_inventory_json(
|
||||
Commands::DownloadCreationsJson(subcommand)=>download_creations_json(
|
||||
cookie_from_args(
|
||||
subcommand.cookie_literal,
|
||||
subcommand.cookie_envvar,
|
||||
subcommand.cookie_file,
|
||||
).await?,
|
||||
subcommand.group,
|
||||
owner_from_args(
|
||||
subcommand.user_id,
|
||||
subcommand.group_id,
|
||||
)?,
|
||||
subcommand.output_folder.unwrap_or_else(||std::env::current_dir().unwrap()),
|
||||
).await,
|
||||
Commands::DownloadUserInventoryJson(subcommand)=>download_user_inventory_json(
|
||||
cookie_from_args(
|
||||
subcommand.cookie_literal,
|
||||
subcommand.cookie_envvar,
|
||||
subcommand.cookie_file,
|
||||
).await?,
|
||||
subcommand.user_id,
|
||||
subcommand.output_folder.unwrap_or_else(||std::env::current_dir().unwrap()),
|
||||
subcommand.continue_from_cursor.unwrap_or(false),
|
||||
).await,
|
||||
Commands::CreateAsset(subcommand)=>create_asset(CreateAssetConfig{
|
||||
cookie:cookie_from_args(
|
||||
subcommand.cookie_literal,
|
||||
@ -456,14 +563,14 @@ async fn main()->AResult<()>{
|
||||
subcommand.api_key_file,
|
||||
).await?,
|
||||
creator:match (subcommand.creator_user_id,subcommand.creator_group_id){
|
||||
(Some(user_id),None)=>rbx_asset::cloud::Creator::userId(user_id.to_string()),
|
||||
(None,Some(group_id))=>rbx_asset::cloud::Creator::groupId(group_id.to_string()),
|
||||
(Some(user_id),None)=>rbx_asset::cloud::Creator::userId(user_id),
|
||||
(None,Some(group_id))=>rbx_asset::cloud::Creator::groupId(group_id),
|
||||
other=>Err(anyhow!("Invalid creator {other:?}"))?,
|
||||
},
|
||||
input_file:subcommand.input_file,
|
||||
asset_type:subcommand.asset_type.cloud(),
|
||||
model_name:subcommand.model_name,
|
||||
description:subcommand.description.unwrap_or_else(||String::with_capacity(0)),
|
||||
description:subcommand.description,
|
||||
expected_price:subcommand.expected_price,
|
||||
}).await,
|
||||
Commands::CreateAssetMedias(subcommand)=>create_asset_medias(CreateAssetMediasConfig{
|
||||
@ -478,8 +585,8 @@ async fn main()->AResult<()>{
|
||||
subcommand.cookie_file,
|
||||
).await?,
|
||||
creator:match (subcommand.creator_user_id,subcommand.creator_group_id){
|
||||
(Some(user_id),None)=>rbx_asset::cloud::Creator::userId(user_id.to_string()),
|
||||
(None,Some(group_id))=>rbx_asset::cloud::Creator::groupId(group_id.to_string()),
|
||||
(Some(user_id),None)=>rbx_asset::cloud::Creator::userId(user_id),
|
||||
(None,Some(group_id))=>rbx_asset::cloud::Creator::groupId(group_id),
|
||||
other=>Err(anyhow!("Invalid creator {other:?}"))?,
|
||||
},
|
||||
description:subcommand.description.unwrap_or_else(||String::with_capacity(0)),
|
||||
@ -561,7 +668,7 @@ async fn main()->AResult<()>{
|
||||
git_committer_name:subcommand.git_committer_name,
|
||||
git_committer_email:subcommand.git_committer_email,
|
||||
input_folder:subcommand.input_folder,
|
||||
output_folder:std::env::current_dir()?,
|
||||
output_folder:subcommand.output_folder.unwrap_or_else(||std::env::current_dir().unwrap()),
|
||||
style:subcommand.style.rox(),
|
||||
write_template:subcommand.write_template.unwrap_or(false),
|
||||
write_models:subcommand.write_models.unwrap_or(false),
|
||||
@ -576,7 +683,7 @@ async fn main()->AResult<()>{
|
||||
subcommand.cookie_file,
|
||||
).await?,
|
||||
asset_id:subcommand.asset_id,
|
||||
output_folder:std::env::current_dir()?,
|
||||
output_folder:subcommand.output_folder.unwrap_or_else(||std::env::current_dir().unwrap()),
|
||||
style:subcommand.style.rox(),
|
||||
write_template:subcommand.write_template.unwrap_or(false),
|
||||
write_models:subcommand.write_models.unwrap_or(false),
|
||||
@ -592,7 +699,7 @@ async fn cookie_from_args(literal:Option<String>,environment:Option<String>,file
|
||||
(None,None,Some(cookie_file))=>tokio::fs::read_to_string(cookie_file).await?,
|
||||
_=>Err(anyhow::Error::msg("Illegal cookie argument triple"))?,
|
||||
};
|
||||
Ok(Cookie::new(format!(".ROBLOSECURITY={cookie}")))
|
||||
Ok(Cookie::new(cookie))
|
||||
}
|
||||
async fn api_key_from_args(literal:Option<String>,environment:Option<String>,file:Option<PathBuf>)->AResult<ApiKey>{
|
||||
let api_key=match (literal,environment,file){
|
||||
@ -603,6 +710,14 @@ async fn api_key_from_args(literal:Option<String>,environment:Option<String>,fil
|
||||
};
|
||||
Ok(ApiKey::new(api_key))
|
||||
}
|
||||
fn owner_from_args(user_id:Option<u64>,group_id:Option<u64>)->AResult<rbx_asset::cookie::Owner>{
|
||||
let owner=match (user_id,group_id){
|
||||
(Some(id),None)=>rbx_asset::cookie::Owner::User(id),
|
||||
(None,Some(id))=>rbx_asset::cookie::Owner::Group(id),
|
||||
_=>Err(anyhow::Error::msg("Illegal owner argument pair"))?,
|
||||
};
|
||||
Ok(owner)
|
||||
}
|
||||
|
||||
struct CreateAssetConfig{
|
||||
cookie:Cookie,
|
||||
@ -724,6 +839,7 @@ enum DownloadDecalError{
|
||||
NoFirstInstance,
|
||||
NoTextureProperty,
|
||||
TexturePropertyInvalid,
|
||||
TextureContentNotUri,
|
||||
}
|
||||
impl std::fmt::Display for DownloadDecalError{
|
||||
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
|
||||
@ -787,20 +903,25 @@ async fn create_asset_medias(config:CreateAssetMediasConfig)->AResult<()>{
|
||||
let cookie_context=&cookie_context;
|
||||
async move{(path,
|
||||
async move{
|
||||
use rbx_dom_weak::ustr;
|
||||
let asset_response=asset_response_result.map_err(DownloadDecalError::PollOperation)?;
|
||||
let file=cookie_context.get_asset(rbx_asset::cookie::GetAssetRequest{
|
||||
asset_id:asset_response.assetId.parse().map_err(DownloadDecalError::ParseInt)?,
|
||||
let maybe_gzip=cookie_context.get_asset(rbx_asset::cookie::GetAssetRequest{
|
||||
asset_id:asset_response.assetId,
|
||||
version:None,
|
||||
}).await.map_err(DownloadDecalError::Get)?;
|
||||
let dom=load_dom(std::io::Cursor::new(file)).map_err(DownloadDecalError::LoadDom)?;
|
||||
let dom=maybe_gzip.read_with(load_dom,load_dom).map_err(DownloadDecalError::LoadDom)?;
|
||||
let instance=dom.get_by_ref(
|
||||
*dom.root().children().first().ok_or(DownloadDecalError::NoFirstInstance)?
|
||||
).ok_or(DownloadDecalError::NoFirstInstance)?;
|
||||
let texture=instance.properties.get("Texture").ok_or(DownloadDecalError::NoTextureProperty)?;
|
||||
let asset_url=match texture{
|
||||
rbx_dom_weak::types::Variant::Content(url)=>url.clone().into_string(),
|
||||
let texture=instance.properties.get(&ustr("TextureContent")).ok_or(DownloadDecalError::NoTextureProperty)?;
|
||||
let content=match texture{
|
||||
rbx_dom_weak::types::Variant::Content(content)=>content,
|
||||
_=>Err(DownloadDecalError::TexturePropertyInvalid)?,
|
||||
};
|
||||
let asset_url=match content.value(){
|
||||
rbx_dom_weak::types::ContentType::Uri(uri)=>uri.clone(),
|
||||
_=>Err(DownloadDecalError::TextureContentNotUri)?,
|
||||
};
|
||||
Ok::<_,DownloadDecalError>((asset_response.displayName,asset_url))
|
||||
}
|
||||
.await)}
|
||||
@ -869,6 +990,34 @@ async fn upload_place(config:UploadPlaceConfig)->AResult<()>{
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn asset_details(cookie:Cookie,asset_id:AssetID)->AResult<()>{
|
||||
let context=CookieContext::new(cookie);
|
||||
let details=context.get_asset_details(rbx_asset::cookie::GetAssetDetailsRequest{asset_id}).await?;
|
||||
println!("details:{details:?}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn download_version(cookie:Cookie,asset_id:AssetID,version:Option<u64>,dest:PathBuf)->AResult<()>{
|
||||
let context=CookieContext::new(cookie);
|
||||
let maybe_gzip=context.get_asset(rbx_asset::cookie::GetAssetRequest{asset_id,version}).await?;
|
||||
tokio::fs::write(dest,maybe_gzip.to_vec()?).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn download_version_v2(cookie:Cookie,asset_id:AssetID,version:Option<u64>,dest:PathBuf)->AResult<()>{
|
||||
let context=CookieContext::new(cookie);
|
||||
|
||||
// v2 has two steps
|
||||
let info=context.get_asset_v2(rbx_asset::cookie::GetAssetRequest{asset_id,version}).await?;
|
||||
println!("version:{}",info.version);
|
||||
|
||||
let location=info.info.locations.first().ok_or(anyhow::Error::msg("No locations"))?;
|
||||
let maybe_gzip=context.get_asset_v2_download(location).await?;
|
||||
|
||||
tokio::fs::write(dest,maybe_gzip.to_vec()?).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn download_list(cookie:Cookie,asset_id_file_map:AssetIDFileMap)->AResult<()>{
|
||||
let context=CookieContext::new(cookie);
|
||||
futures::stream::iter(asset_id_file_map.into_iter()
|
||||
@ -881,7 +1030,7 @@ async fn download_list(cookie:Cookie,asset_id_file_map:AssetIDFileMap)->AResult<
|
||||
.buffer_unordered(CONCURRENT_REQUESTS)
|
||||
.for_each(|b:AResult<_>|async{
|
||||
match b{
|
||||
Ok((dest,data))=>if let Err(e)=tokio::fs::write(dest,data).await{
|
||||
Ok((dest,maybe_gzip))=>if let Err(e)=(async||{tokio::fs::write(dest,maybe_gzip.to_vec()?).await})().await{
|
||||
eprintln!("fs error: {}",e);
|
||||
},
|
||||
Err(e)=>eprintln!("dl error: {}",e),
|
||||
@ -890,23 +1039,26 @@ async fn download_list(cookie:Cookie,asset_id_file_map:AssetIDFileMap)->AResult<
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_inventory_pages(context:&CookieContext,group:u64)->AResult<Vec<InventoryItem>>{
|
||||
let mut cursor:Option<String>=None;
|
||||
async fn get_creations_pages(context:&CookieContext,owner:rbx_asset::cookie::Owner)->AResult<Vec<CreationsItem>>{
|
||||
let mut config=rbx_asset::cookie::CreationsPageRequest{
|
||||
owner,
|
||||
cursor:None,
|
||||
};
|
||||
let mut asset_list=Vec::new();
|
||||
loop{
|
||||
let mut page=context.get_inventory_page(rbx_asset::cookie::InventoryPageRequest{group,cursor}).await?;
|
||||
let mut page=context.get_creations_page(&config).await?;
|
||||
asset_list.append(&mut page.data);
|
||||
if page.nextPageCursor.is_none(){
|
||||
break;
|
||||
}
|
||||
cursor=page.nextPageCursor;
|
||||
config.cursor=page.nextPageCursor;
|
||||
}
|
||||
Ok(asset_list)
|
||||
}
|
||||
|
||||
async fn download_group_inventory_json(cookie:Cookie,group:u64,output_folder:PathBuf)->AResult<()>{
|
||||
async fn download_creations_json(cookie:Cookie,owner:rbx_asset::cookie::Owner,output_folder:PathBuf)->AResult<()>{
|
||||
let context=CookieContext::new(cookie);
|
||||
let item_list=get_inventory_pages(&context,group).await?;
|
||||
let item_list=get_creations_pages(&context,owner).await?;
|
||||
|
||||
let mut path=output_folder.clone();
|
||||
path.set_file_name("versions.json");
|
||||
@ -915,6 +1067,74 @@ async fn download_group_inventory_json(cookie:Cookie,group:u64,output_folder:Pat
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_user_inventory_pages(
|
||||
context:&CookieContext,
|
||||
asset_list:&mut Vec<rbx_asset::cookie::UserInventoryItem>,
|
||||
config:&mut rbx_asset::cookie::UserInventoryPageRequest,
|
||||
)->AResult<()>{
|
||||
loop{
|
||||
let page=context.get_user_inventory_page(&config).await?;
|
||||
asset_list.extend(page.data);
|
||||
config.cursor=page.nextPageCursor;
|
||||
if config.cursor.is_none(){
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn download_user_inventory_json(cookie:Cookie,user_id:u64,output_folder:PathBuf,continue_from_cursor:bool)->AResult<()>{
|
||||
let mut versions_path=output_folder.clone();
|
||||
versions_path.set_file_name("versions.json");
|
||||
let mut cursor_path=output_folder.clone();
|
||||
cursor_path.set_file_name("cursor.json");
|
||||
|
||||
let context=CookieContext::new(cookie);
|
||||
|
||||
let (mut asset_list,mut config)=if continue_from_cursor{
|
||||
// load state from files
|
||||
let (versions,cursor)=tokio::try_join!(
|
||||
tokio::fs::read(versions_path.as_path()),
|
||||
tokio::fs::read_to_string(cursor_path.as_path()),
|
||||
)?;
|
||||
(
|
||||
serde_json::from_slice(&versions)?,
|
||||
rbx_asset::cookie::UserInventoryPageRequest{
|
||||
user_id,
|
||||
cursor:Some(cursor),
|
||||
}
|
||||
)
|
||||
}else{
|
||||
// create new state
|
||||
(
|
||||
Vec::new(),
|
||||
rbx_asset::cookie::UserInventoryPageRequest{
|
||||
user_id,
|
||||
cursor:None,
|
||||
}
|
||||
)
|
||||
};
|
||||
|
||||
match get_user_inventory_pages(&context,&mut asset_list,&mut config).await{
|
||||
Ok(())=>println!("Pages polling complete"),
|
||||
Err(e)=>println!("Error: {e}"),
|
||||
}
|
||||
|
||||
let cursor_fut=async{
|
||||
if let Some(cursor)=config.cursor{
|
||||
println!("writing cursor state...");
|
||||
// there was a problem, write out cursor
|
||||
tokio::fs::write(cursor_path,cursor).await?;
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
let versions_fut=tokio::fs::write(versions_path,serde_json::to_string(&asset_list)?);
|
||||
|
||||
tokio::try_join!(versions_fut,cursor_fut)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_version_history(context:&CookieContext,asset_id:AssetID)->AResult<Vec<AssetVersion>>{
|
||||
let mut cursor:Option<String>=None;
|
||||
let mut asset_list=Vec::new();
|
||||
@ -1014,9 +1234,9 @@ async fn download_history(mut config:DownloadHistoryConfig)->AResult<()>{
|
||||
let mut path=output_folder.clone();
|
||||
path.push(format!("{}_v{}.rbxl",config.asset_id,version_number));
|
||||
join_set.spawn(async move{
|
||||
let file=context.get_asset(rbx_asset::cookie::GetAssetRequest{asset_id:config.asset_id,version:Some(version_number)}).await?;
|
||||
let maybe_gzip=context.get_asset(rbx_asset::cookie::GetAssetRequest{asset_id:config.asset_id,version:Some(version_number)}).await?;
|
||||
|
||||
tokio::fs::write(path,file).await?;
|
||||
tokio::fs::write(path,maybe_gzip.to_vec()?).await?;
|
||||
|
||||
Ok::<_,anyhow::Error>(())
|
||||
});
|
||||
@ -1136,9 +1356,9 @@ struct DownloadDecompileConfig{
|
||||
|
||||
async fn download_decompile(config:DownloadDecompileConfig)->AResult<()>{
|
||||
let context=CookieContext::new(config.cookie);
|
||||
let file=context.get_asset(rbx_asset::cookie::GetAssetRequest{asset_id:config.asset_id,version:None}).await?;
|
||||
let maybe_gzip=context.get_asset(rbx_asset::cookie::GetAssetRequest{asset_id:config.asset_id,version:None}).await?;
|
||||
|
||||
let dom=load_dom(std::io::Cursor::new(file))?;
|
||||
let dom=maybe_gzip.read_with(load_dom,load_dom)?;
|
||||
let context=rox_compiler::DecompiledContext::from_dom(dom);
|
||||
|
||||
context.write_files(rox_compiler::WriteConfig{
|
||||
@ -1198,11 +1418,11 @@ async fn write_commit(config:WriteCommitConfig,b:Result<AResult<(AssetVersion,ro
|
||||
let sig=git2::Signature::new(config.git_committer_name.as_str(),config.git_committer_email.as_str(),&git2::Time::new(date.timestamp(),0)).unwrap();
|
||||
let tree_id={
|
||||
let mut tree_index = repo.index()?;
|
||||
match tree_index.add_all(std::iter::once(config.output_folder.as_path()),git2::IndexAddOption::DEFAULT,None){
|
||||
match tree_index.add_all(std::iter::once("*"),git2::IndexAddOption::DEFAULT,None){
|
||||
Ok(_)=>(),
|
||||
Err(e)=>println!("tree_index.add_all error: {}",e),
|
||||
}
|
||||
match tree_index.update_all(std::iter::once(config.output_folder.as_path()),None){
|
||||
match tree_index.update_all(std::iter::once("*"),None){
|
||||
Ok(_)=>(),
|
||||
Err(e)=>println!("tree_index.update_all error: {}",e),
|
||||
}
|
||||
@ -1318,8 +1538,8 @@ async fn download_and_decompile_history_into_git(config:DownloadAndDecompileHist
|
||||
.map(|asset_version|{
|
||||
let context=context.clone();
|
||||
tokio::task::spawn(async move{
|
||||
let file=context.get_asset(rbx_asset::cookie::GetAssetRequest{asset_id,version:Some(asset_version.assetVersionNumber)}).await?;
|
||||
let dom=load_dom(std::io::Cursor::new(file))?;
|
||||
let maybe_gzip=context.get_asset(rbx_asset::cookie::GetAssetRequest{asset_id,version:Some(asset_version.assetVersionNumber)}).await?;
|
||||
let dom=maybe_gzip.read_with(load_dom,load_dom)?;
|
||||
Ok::<_,anyhow::Error>((asset_version,rox_compiler::DecompiledContext::from_dom(dom)))
|
||||
})
|
||||
}))
|
||||
|
Loading…
x
Reference in New Issue
Block a user