116 Commits

Author SHA1 Message Date
2088256e29 update deps
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-27 15:30:53 -08:00
d50e86f809 update deps
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-09 06:06:00 -08:00
b37b08d564 drop lazy_regex dep 2025-11-09 06:04:36 -08:00
d66f786aca fix deploy
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-28 04:06:18 -07:00
e05c1ddabf put git behind feature flag to prevent openssl woes
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-25 21:03:38 -07:00
a2b4980bf3 asset-tool: use rustls
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-25 20:59:05 -07:00
ad3c446b43 fix deploy
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-25 20:54:56 -07:00
6cc9ded572 fix deploy
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-25 20:53:04 -07:00
2faddb741f asset-tool v0.5.0 new commands + minor breaking changes
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-25 20:43:10 -07:00
f59a2e0b6a rbx_asset v0.5.0 minor braking changes 2025-08-25 20:41:29 -07:00
e5e75ef3cb DownloadCreationsHistory
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-25 19:47:16 -07:00
fdf0c14309 detect resume files correctly in download_creations_pages_from_checkpoint 2025-08-25 19:47:16 -07:00
8885eb744b propagate error in download_creations_pages_from_checkpoint 2025-08-25 19:47:14 -07:00
c0ded31a6a delete cursor when completed 2025-08-25 19:47:12 -07:00
d9bf50e1c4 fix get_creations_pages 2025-08-25 19:47:10 -07:00
55d5f97f0b rbx_asset: update GetAssetV2Info
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-25 17:41:58 -07:00
7665ccc5d1 rbx_asset: accept reference in get_asset_versions_page
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-25 16:04:56 -07:00
24f50de2ae use sort_by_key
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-25 15:47:26 -07:00
afd8a74e34 add continue from cursor option to DownloadCreationsJson 2025-08-25 15:47:26 -07:00
ddf294c6f9 rename cursor.json to just cursor 2025-08-25 15:35:25 -07:00
a9a8c01cb1 do creations pages like user 2025-08-25 15:35:13 -07:00
287969b7d5 update UserInventoryItemOwner.buildersClubMembershipType type
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-23 21:34:50 -07:00
b80868e722 v0.4.10 Luau Execution API
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-09 00:06:13 -07:00
e1503ba898 v0.4.10-pre2
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-08 20:35:33 -07:00
97086e351f rbx_asset: rename LuauSessionLatestRequest 2025-08-08 20:35:33 -07:00
89d96db03c rbx_asset: unversioned request 2025-08-08 20:32:52 -07:00
6b9ae59e7f v0.4.10-pre1 Luau Execution API
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-08 20:27:46 -07:00
dd4344f514 Luau Execution API (#18)
All checks were successful
continuous-integration/drone/push Build is passing
Tested to some extent

Reviewed-on: #18
Co-authored-by: Rhys Lloyd <krakow20@gmail.com>
Co-committed-by: Rhys Lloyd <krakow20@gmail.com>
2025-08-09 03:26:03 +00:00
8a400faae2 rbx_asset: rename GetError variant
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-08 17:33:26 -07:00
6dff5f5145 rbx_asset: relax operation allocation requirement
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-08 16:59:24 -07:00
f3f048e293 v0.4.9 rustls passthru
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-05 21:20:31 -07:00
10c9ddd696 goofy feature stuff
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-05 21:17:01 -07:00
d73567050c update deps 2025-08-05 21:14:30 -07:00
c1e53e42bc remove attributes OBE 2025-08-05 21:09:47 -07:00
b3defd31fc replace allow with expect 2025-08-05 21:08:23 -07:00
bf3b429c66 rbx_asset v0.4.8 default field
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-01 04:56:23 -07:00
20899a3fae rbx_asset: default field
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-01 04:55:56 -07:00
d60cedf430 rbx_asset: v0.4.7 roblox api changed
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-01 01:12:43 -07:00
ad435fb8c9 update deps 2025-07-01 01:12:43 -07:00
9f1bdd6a1f rbx_asset: roblox api changed
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-01 01:08:51 -07:00
0bf0b92efb asset location commands
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-13 20:04:18 -07:00
41cd60c459 untab 2025-06-13 20:04:07 -07:00
52a0bf221b rbx_asset: visibility mistake
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-11 23:34:21 -07:00
f2bd298cd1 rbx_asset: try out ref for funsies 2025-06-11 23:34:21 -07:00
89bbe00e3d Set Universe Asset Permissions (#17)
All checks were successful
continuous-integration/drone/push Build is passing
This implements an endpoint to set universe asset permissions.

Reviewed-on: #17
Co-authored-by: Quaternions <krakow20@gmail.com>
Co-committed-by: Quaternions <krakow20@gmail.com>
2025-06-12 02:33:12 +00:00
369f19452c rbx_asset: omit Cursor
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-13 23:44:11 -07:00
9e78be3d09 rbx_asset v0.4.5 fix error type
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-13 23:28:00 -07:00
70414d94ae clippy fixes
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-13 23:26:15 -07:00
819eea1b4a rbx_asset: error type is too damn big 2025-05-13 23:23:58 -07:00
9eabb0197c asset-tool v0.4.12 update rbx-dom
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-30 17:23:36 -07:00
fa9d42fc1f update deps
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-30 17:13:02 -07:00
8f754f0bca update rbx-dom 2025-04-30 17:12:49 -07:00
450b6a0829 update deps
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-10 17:26:47 -07:00
091a2a92f1 rbx_asset: v0.4.4 parse string ints + save intermediate allocation
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-10 17:24:49 -07:00
31aae80cc5 rbx_asset: change api to save intermediate allocation 2025-04-10 17:24:49 -07:00
041cc75015 rbx_asset: move code into util, types 2025-04-10 17:24:49 -07:00
d77312309f rbx_asset: helpers for integers within a string 2025-04-10 17:24:49 -07:00
50145460b9 rbx_asset: simplify gzip logic
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-10 00:25:34 -07:00
df2a5bb9ce rbx_asset: v0.4.3 optional AssetLocation
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-08 16:54:59 -07:00
e40041a894 rbx_asset: change api for asset location again 2025-04-08 16:54:14 -07:00
45c1e52c0f rbx_asset: v0.4.2
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-05 14:53:47 -07:00
0196f47374 rbx_asset: description is optional
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-05 14:53:32 -07:00
a8163014ad rbx_asset: v0.4.1
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-05 14:23:26 -07:00
4d26e7ad19 rbx_asset: discover asset location
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-05 14:21:57 -07:00
99077cf467 rbx_asset: v0.4.0
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-05 13:51:16 -07:00
302603998e rbx_asset: cloud: implement new asset-delivery-api 2025-04-05 13:09:59 -07:00
71cae5c089 rbx_asset: cloud: tweak asset info requests, remove get_asset 2025-04-05 13:09:25 -07:00
fb9dd8660d rbx_asset: deduplicate common code 2025-04-05 12:59:44 -07:00
64e4887b83 rbx_asset: use #[serde(default)] instead of Option<Vec<_>> 2025-04-05 12:59:24 -07:00
d125829a00 rbx_asset: rename CloudContext & CookieContext to Context 2025-04-05 12:23:39 -07:00
5509fd2166 rbx_asset: context fields should be private 2025-04-05 12:23:13 -07:00
e17db96e86 asset-tool v0.4.11 continue feature for DownloadUserInventoryJson
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-04 13:53:19 -07:00
875b059074 asset-tool: tweak get_user_inventory_pages 2025-04-04 13:53:19 -07:00
9b1c709e7c asset-tool: add continue to DownloadUserInventoryJson
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-04 13:36:18 -07:00
ebd48269b8 rbx_asset: v0.3.4 UploadResponse.AssetVersion
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-03 16:14:33 -07:00
5a4ac0e7f2 rbx_asset: include asset version in UploadResponse
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-03 16:13:45 -07:00
68ebbad7a7 rbx_asset: v0.3.3 optional field
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-03 13:56:10 -07:00
c99b752738 rbx_asset: optional field on AssetDetails 2025-04-03 13:56:02 -07:00
4f798e5f07 rbx_asset: v0.3.1 fix missing field 2025-04-03 13:55:56 -07:00
39fa74d44a rbx_asset: fix missing field on GetAssetV2Location 2025-04-03 13:55:56 -07:00
edb5ea7648 rbx_asset: v0.3.1 CreatorType traits
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-03 12:57:17 -07:00
504ff40385 rbx_asset: derive additional traits for CreatorType 2025-04-03 12:56:42 -07:00
89c0ee2cc2 update deps
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-03 12:53:09 -07:00
f7e565bd0b asset-tool: v0.4.10
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-03 12:47:02 -07:00
fe8062d8e0 rbx_asset: v0.3.0 add CreatorType + get_asset_details + get_asset_v2 2025-04-03 12:47:02 -07:00
e6fe04aa73 asset-tool: add AssetDetails 2025-04-03 12:47:02 -07:00
656de62bdc asset-tool: add DownloadVersionV2 2025-04-03 12:47:02 -07:00
4d90a74a82 rbx_asset: get_asset_v2 2025-04-03 12:47:02 -07:00
fb6fb67954 rbx_asset: tweak get_asset 2025-04-03 12:45:18 -07:00
b0f1e964a6 rbx_asset: add get_asset_details 2025-04-03 12:45:18 -07:00
aea777ecd3 rbx_asset: add CreatorType 2025-04-03 12:43:16 -07:00
6eca84a08a DownloadVersion command
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-12 14:55:19 -08:00
b7bab46e04 v0.4.9 roblox error message in body
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-11 13:53:16 -08:00
0413767ef9 v0.2.6 roblox error message in body
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-11 13:50:31 -08:00
b69698fd8e rbx_asset: special error for special roblox
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-11 13:43:11 -08:00
309fc2494d update deps 2025-02-11 13:31:14 -08:00
b352707b99 rbx_asset v0.2.5 better error info
All checks were successful
continuous-integration/drone/push Build is passing
2024-12-13 19:55:45 -08:00
81f411272f rbx_asset keep typed UploadResponse 2024-12-13 19:55:19 -08:00
e45f3c2cf9 cloud: use response_ok helper function
All checks were successful
continuous-integration/drone/push Build is passing
2024-12-13 19:35:31 -08:00
3cd65158a0 update deps
All checks were successful
continuous-integration/drone/push Build is passing
2024-12-13 19:19:04 -08:00
38d78ff2c5 cookie: refactor http errors to include more useful information 2024-12-13 19:18:35 -08:00
c49f9e4dd3 cookie: prepend in library 2024-12-13 19:01:37 -08:00
a99b5a2666 update Roblox API
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-23 10:43:59 -07:00
a693da9cb2 asset-tool v0.4.7 user inventory + git fix
All checks were successful
continuous-integration/drone/push Build is passing
2024-10-01 13:08:35 -07:00
a572d10447 support output-folder option for git-related features
Repository-based logic seems to automatically be made relative to the top-level path the repo was initialised with, so we can actually support an output folder with no issues here
2024-10-01 13:08:26 -07:00
1b2e13b4c7 fix decompiling history into git
Why does this work and the version with the directory doesn't? yeah idk. But hey, this one successfully stages the files, so ill take it
2024-10-01 13:08:23 -07:00
321ef9b51e rbx_asset v0.2.3 user inventory 2024-10-01 11:40:29 -07:00
28bcf8005b download_user_inventory_json 2024-10-01 11:39:16 -07:00
ea0239c78b cookie.get_user_inventory_page 2024-10-01 11:38:43 -07:00
94792ebf02 unite PageErrors 2024-10-01 11:38:43 -07:00
ac674778f6 enable getting creations pages from user or group 2024-10-01 11:38:43 -07:00
d3d58ff358 crate info and licenses
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-25 15:51:22 -07:00
bc11997e88 v0.4.6 documentation + update deps
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-16 21:44:10 -07:00
4b5ceef5d4 update deps
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-16 21:40:13 -07:00
00f8cddde0 write some documentation
All checks were successful
continuous-integration/drone/push Build is passing
2024-08-24 12:22:21 -07:00
87993d0f52 improve directions
All checks were successful
continuous-integration/drone/push Build is passing
2024-08-17 11:31:14 -07:00
21 changed files with 3127 additions and 804 deletions

View File

@@ -7,6 +7,17 @@ platform:
arch: amd64
steps:
- name: build
image: clux/muslrust:1.89.0-stable
commands:
- cargo build --release --target x86_64-unknown-linux-musl
when:
branch:
- master
event:
- push
- pull_request
- name: image
image: plugins/docker
settings:
@@ -19,6 +30,15 @@ steps:
password:
from_secret: GIT_PASS
dockerfile: Containerfile
depends_on:
- build
when:
branch:
- master
- master
event:
- push
---
kind: signature
hmac: 52507904dfaada892c05a61422dc5e147c1438419ed841d0f1e3e3ec2b193540
...

1698
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
workspace = { members = ["rbx_asset", "rox_compiler"] }
[package]
name = "asset-tool"
version = "0.4.5"
version = "0.5.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -10,18 +10,20 @@ edition = "2021"
anyhow = "1.0.75"
clap = { version = "4.4.2", features = ["derive"] }
futures = "0.3.30"
git2 = "0.18.1"
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"
git2 = { version = "0.20.0", optional = true }
rbx_asset = { path = "rbx_asset", features = ["gzip", "rustls-tls"], default-features = false }
rbx_binary = "2.0.0"
rbx_dom_weak = "4.0.0"
rbx_reflection_database = "2.0.1"
rbx_xml = "2.0.0"
rox_compiler = { path = "rox_compiler" }
serde_json = "1.0.111"
tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread", "fs"] }
[features]
default = []
git = ["dep:git2"]
[profile.release]
#lto = true
strip = true

View File

@@ -1,23 +1,3 @@
# Using the `rust-musl-builder` as base image, instead of
# the official Rust toolchain
FROM docker.io/clux/muslrust:stable AS chef
USER root
RUN cargo install cargo-chef
WORKDIR /app
FROM chef AS planner
COPY . .
RUN cargo chef prepare --recipe-path recipe.json
FROM chef AS builder
COPY --from=planner /app/recipe.json recipe.json
# Notice that we are specifying the --target flag!
RUN cargo chef cook --release --target x86_64-unknown-linux-musl --recipe-path recipe.json
COPY . .
RUN cargo build --release --target x86_64-unknown-linux-musl --bin asset-tool
FROM docker.io/alpine:latest AS runtime
RUN addgroup -S myuser && adduser -S myuser -G myuser
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/asset-tool /usr/local/bin/
USER myuser
ENTRYPOINT ["/usr/local/bin/asset-tool"]
FROM alpine:3.22 AS runtime
COPY /target/x86_64-unknown-linux-musl/release/asset-tool /usr/local/bin/
ENTRYPOINT ["/usr/local/bin/asset-tool"]

View File

@@ -2,4 +2,20 @@
[![Build Status](https://ci.itzana.me/api/badges/StrafesNET/asset-tool/status.svg?ref=refs/heads/master)](https://ci.itzana.me/StrafesNET/asset-tool)
For uploading and downloading roblox assets.
For uploading and downloading roblox assets.
See [releases](https://git.itzana.me/StrafesNET/asset-tool/releases) for downloads.
To get started, you will need an api key and/or cookie depending on which command you use. Api keys can be created from the open cloud section on the creator hub, cookies must be extracted from a browser session. Do not share your cookie file with anyone or use it with tools that you do not trust, as it gives unrestricted permissions to do any account actions on your behalf. The api keys are significantly more safe because you can choose exactly what permissions the given key has, and which ip addresses can use it.
The help text lists available commands:
`asset-tool --help`
For help with a specific command:
`asset-tool download --help`
Example incantation to download a list of models:
`asset-tool download --cookie-file Documents\mycookie.txt 255299419 1213190363`

View File

@@ -1,15 +1,31 @@
[package]
name = "rbx_asset"
version = "0.2.2"
version = "0.5.0"
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", "default-tls"]
gzip = ["dep:flate2"]
default-tls = ["reqwest/default-tls"]
rustls-tls = ["reqwest/rustls-tls"]
[dependencies]
bytes = "1.10.1"
chrono = { version = "0.4.38", features = ["serde"] }
flate2 = "1.0.29"
reqwest = { version = "0.12.4", features = ["json","multipart"] }
flate2 = { version = "1.0.29", optional = true }
reqwest = { version = "0.12.4", features = [
"json", "multipart",
# default features
"charset", "http2", "system-proxy"
], default-features = false }
serde = { version = "1.0.199", features = ["derive"] }
serde_json = "1.0.111"
url = "2.5.0"

176
rbx_asset/LICENSE-APACHE Normal file
View 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
View 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.

44
rbx_asset/src/body.rs Normal file
View File

@@ -0,0 +1,44 @@
use reqwest::Body;
pub trait ContentType:Into<Body>{
fn content_type(&self)->&'static str;
}
#[derive(Clone,Copy,Debug)]
pub struct Json<T>(pub(crate)T);
impl<T:Into<Body>> From<Json<T>> for Body{
fn from(Json(value):Json<T>)->Self{
value.into()
}
}
impl<T:Into<Body>> ContentType for Json<T>{
fn content_type(&self)->&'static str{
"application/json"
}
}
#[derive(Clone,Copy,Debug)]
pub struct Text<T>(pub(crate)T);
impl<T:Into<Body>> From<Text<T>> for Body{
fn from(Text(value):Text<T>)->Self{
value.into()
}
}
impl<T:Into<Body>> ContentType for Text<T>{
fn content_type(&self)->&'static str{
"text/plain"
}
}
#[derive(Clone,Copy,Debug)]
pub struct Binary<T>(pub(crate)T);
impl<T:Into<Body>> From<Binary<T>> for Body{
fn from(Binary(value):Binary<T>)->Self{
value.into()
}
}
impl<T:Into<Body>> ContentType for Binary<T>{
fn content_type(&self)->&'static str{
"application/octet-stream"
}
}

View File

@@ -1,3 +1,7 @@
use crate::body::{Binary,ContentType,Json};
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 +33,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 +44,7 @@ impl AssetOperation{
#[derive(Debug)]
pub enum CreateError{
Parse(url::ParseError),
Response(ResponseError),
Serialize(serde_json::Error),
Reqwest(reqwest::Error),
}
@@ -62,8 +67,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 +107,7 @@ pub struct UpdatePlaceResponse{
#[derive(Debug)]
pub enum UpdateError{
ParseError(url::ParseError),
Response(ResponseError),
SerializeError(serde_json::Error),
Reqwest(reqwest::Error),
}
@@ -112,10 +118,10 @@ impl std::fmt::Display for UpdateError{
}
impl std::error::Error for UpdateError{}
struct GetAssetOperationRequest{
operation_id:String,
struct GetAssetOperationRequest<'a>{
operation_id:&'a str,
}
pub struct GetAssetInfoRequest{
pub struct GetAssetLatestRequest{
pub asset_id:u64,
}
/*
@@ -142,33 +148,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),
Parse(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 +184,36 @@ 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 AssetMetadata{
pub metadataType:u32,
pub value:String,
}
#[derive(Debug,serde::Deserialize)]
#[allow(nonstandard_style,dead_code)]
pub struct AssetLocationInfo{
pub location:Option<AssetLocation>,
pub requestId:String,
pub isArchived:bool,
pub assetTypeId:u32,
#[serde(default)]
pub assetMetadatas:Vec<AssetMetadata>,
pub isRecordable:bool,
}
pub struct AssetVersionsRequest{
pub asset_id:u64,
pub cursor:Option<String>,
@@ -203,6 +240,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 +276,7 @@ pub struct InventoryPageResponse{
#[derive(Debug)]
pub enum InventoryPageError{
ParseError(url::ParseError),
Response(ResponseError),
Reqwest(reqwest::Error),
}
impl std::fmt::Display for InventoryPageError{
@@ -279,33 +318,120 @@ 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)?
.to_owned(),
.ok_or(OperationError::NoOperationId)?,
}).await.map_err(OperationError::Get)?
.response.ok_or(OperationError::NotDone)
}
}
//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>),
#[derive(Debug)]
pub enum LuauSessionError{
Get(GetError),
Unspecified,
NotDone,
NoOutput,
NoError,
}
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)),
impl std::fmt::Display for LuauSessionError{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f,"{self:?}")
}
}
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)
impl std::error::Error for LuauSessionError{}
#[derive(Debug,serde::Serialize)]
#[expect(nonstandard_style)]
pub struct LuauSessionCreate<'a>{
pub script:&'a str,
#[serde(skip_serializing_if="Option::is_none")]
pub user:Option<&'a str>,
#[serde(skip_serializing_if="Option::is_none")]
pub timeout:Option<&'a str>,
#[serde(skip_serializing_if="Option::is_none")]
pub binaryInput:Option<&'a str>,
#[serde(skip_serializing_if="Option::is_none")]
pub enableBinaryOutput:Option<bool>,
#[serde(skip_serializing_if="Option::is_none")]
pub binaryOutputUri:Option<&'a str>,
}
#[derive(Debug,serde::Deserialize)]
#[expect(nonstandard_style)]
pub enum LuauSessionState{
STATE_UNSPECIFIED,
PROCESSING,
COMPLETE,
FAILED,
}
#[derive(Debug,serde::Deserialize)]
pub struct LuauError{
pub code:String,
pub message:String,
}
#[derive(Debug,serde::Deserialize)]
pub struct LuauResults{
pub results:Vec<serde_json::Value>,
}
#[derive(Debug,serde::Deserialize)]
#[expect(nonstandard_style)]
pub struct LuauSessionResponse{
path:String,
#[serde(deserialize_with="deserialize_u64")]
pub user:u64,
pub state:LuauSessionState,
pub script:String,
pub error:Option<LuauError>,
pub output:Option<LuauResults>,
pub binaryInput:String,
pub enableBinaryOutput:bool,
pub binaryOutputUri:String,
}
impl LuauSessionResponse{
pub fn path(&self)->&str{
&self.path
}
pub async fn try_get_result(&self,context:&Context)->Result<Result<LuauResults,LuauError>,LuauSessionError>{
let response=context.get_luau_session(self).await.map_err(LuauSessionError::Get)?;
match response.state{
LuauSessionState::STATE_UNSPECIFIED=>Err(LuauSessionError::Unspecified),
LuauSessionState::PROCESSING=>Err(LuauSessionError::NotDone),
LuauSessionState::COMPLETE=>Ok(Ok(response.output.ok_or(LuauSessionError::NoOutput)?)),
LuauSessionState::FAILED=>Ok(Err(response.error.ok_or(LuauSessionError::NoError)?)),
}
}
}
pub trait AsSessionPath{
fn into_session_path(&self)->impl AsRef<str>;
}
impl AsSessionPath for LuauSessionResponse{
fn into_session_path(&self)->impl AsRef<str>{
&self.path
}
}
pub struct LuauSessionLatestRequest{
pub universe_id:u64,
pub place_id:u64,
}
impl AsSessionPath for LuauSessionLatestRequest{
fn into_session_path(&self)->impl AsRef<str>{
let universe_id=self.universe_id;
let place_id=self.place_id;
format!("universes/{universe_id}/places/{place_id}/luau-execution-session-tasks")
}
}
pub struct LuauSessionVersionRequest{
pub universe_id:u64,
pub place_id:u64,
pub version_id:u64,
}
impl AsSessionPath for LuauSessionVersionRequest{
fn into_session_path(&self)->impl AsRef<str>{
let universe_id=self.universe_id;
let place_id=self.place_id;
let version_id=self.version_id;
format!("universes/{universe_id}/places/{place_id}/versions/{version_id}/luau-execution-session-tasks")
}
}
#[derive(Clone)]
@@ -320,12 +446,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(),
@@ -337,9 +463,10 @@ impl CloudContext{
.header("x-api-key",self.api_key.as_str())
.send().await
}
async fn post(&self,url:url::Url,body:impl Into<reqwest::Body>+Clone)->Result<reqwest::Response,reqwest::Error>{
async fn post(&self,url:url::Url,body:impl ContentType)->Result<reqwest::Response,reqwest::Error>{
self.client.post(url)
.header("x-api-key",self.api_key.as_str())
.header("Content-Type",body.content_type())
.body(body)
.send().await
}
@@ -368,9 +495,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,62 +514,97 @@ 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{
operation,
})
}
async fn get_asset_operation(&self,config:GetAssetOperationRequest)->Result<RobloxOperation,GetError>{
async fn get_asset_operation(&self,config:GetAssetOperationRequest<'_>)->Result<RobloxOperation,GetError>{
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)?;
let url=reqwest::Url::parse(raw_url.as_str()).map_err(GetError::Parse)?;
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>{
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)?;
pub async fn create_luau_session(&self,config:&impl AsSessionPath,session:LuauSessionCreate<'_>)->Result<LuauSessionResponse,CreateError>{
let raw_url=format!("https://apis.roblox.com/cloud/v2/{}",config.into_session_path().as_ref());
let url=reqwest::Url::parse(raw_url.as_str()).map_err(CreateError::Parse)?;
self.get(url).await.map_err(GetError::Reqwest)?
.error_for_status().map_err(GetError::Reqwest)?
let body=serde_json::to_string(&session).map_err(CreateError::Serialize)?;
response_ok(
self.post(url,Json(body)).await.map_err(CreateError::Reqwest)?
).await.map_err(CreateError::Response)?
.json::<LuauSessionResponse>().await.map_err(CreateError::Reqwest)
}
pub async fn get_luau_session(&self,config:&impl AsSessionPath)->Result<LuauSessionResponse,GetError>{
let raw_url=format!("https://apis.roblox.com/cloud/v2/{}",config.into_session_path().as_ref());
let url=reqwest::Url::parse(raw_url.as_str()).map_err(GetError::Parse)?;
response_ok(
self.get(url).await.map_err(GetError::Reqwest)?
).await.map_err(GetError::Response)?
.json::<LuauSessionResponse>().await.map_err(GetError::Reqwest)
}
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::Parse)?;
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 url=reqwest::Url::parse(raw_url.as_str()).map_err(GetError::Parse)?;
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::Parse)?;
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::Parse)?;
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::Parse)?;
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 +617,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 +631,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,Binary(body)).await.map_err(UpdateError::Reqwest)?
).await.map_err(UpdateError::Response)?
.json::<UpdatePlaceResponse>().await.map_err(UpdateError::Reqwest)
}
}

View File

@@ -1,3 +1,7 @@
use crate::body::{ContentType,Json};
use crate::util::response_ok;
use crate::types::{ResponseError,MaybeGzippedBytes};
#[derive(Debug)]
pub enum PostError{
Reqwest(reqwest::Error),
@@ -23,7 +27,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 +57,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 +76,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 +91,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 +101,109 @@ 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 isArchived:bool,
pub assetTypeId:u32,
pub isRecordable:Option<bool>,
pub IsHashDynamic:Option<bool>,
pub IsCopyrightProtected:Option<bool>,
}
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 +214,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 +228,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 +273,111 @@ pub struct InventoryPageResponse{
pub imageSearchStatus:Option<String>,//null
pub previousPageCursor:Option<String>,
pub nextPageCursor:Option<String>,
pub data:Vec<InventoryItem>,
pub data:Vec<CreationsItem>,
}
pub struct UserInventoryPageRequest{
pub user_id:u64,
pub cursor:Option<String>,
}
#[derive(serde::Deserialize,serde::Serialize)]
#[allow(nonstandard_style,dead_code)]
pub struct UserInventoryItemOwner{
pub userId:u64,
pub username:String,
pub buildersClubMembershipType:String,
}
#[derive(serde::Deserialize,serde::Serialize)]
#[allow(nonstandard_style,dead_code)]
pub struct UserInventoryItem{
pub userAssetId:u64,
pub assetId:u64,
pub assetName:String,
pub collectibleItemId:Option<String>,
pub collectibleItemInstanceId:Option<String>,
pub owner:UserInventoryItemOwner,
pub created:chrono::DateTime<chrono::Utc>,
pub 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(Debug)]
pub enum InventoryPageError{
ParseError(url::ParseError),
pub enum SetAssetsPermissionsError{
Parse(url::ParseError),
JSONEncode(serde_json::Error),
Patch(PostError),
Response(ResponseError),
Reqwest(reqwest::Error),
}
impl std::fmt::Display for InventoryPageError{
impl std::fmt::Display for SetAssetsPermissionsError{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f,"{self:?}")
}
}
impl std::error::Error for InventoryPageError{}
impl std::error::Error for SetAssetsPermissionsError{}
//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>),
#[derive(serde::Serialize)]
#[allow(nonstandard_style)]
struct AssetPermissions{
assetId:u64,
grantToDependencies:bool,//true
}
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::Serialize)]
#[allow(nonstandard_style)]
struct SetAssetsPermissions<'a>{
subjectType:&'a str,// "Universe"
subjectId:&'a str,// "4422715291"
action:&'a str,// "Use",
enableDeepAccessCheck:bool,//true,
requests:&'a [AssetPermissions],
}
pub struct SetAssetsPermissionsRequest<'a>{
pub universe_id:u64,
pub asset_ids:&'a [u64],
}
impl SetAssetsPermissionsRequest<'_>{
fn serialize(&self)->Result<String,serde_json::Error>{
let ref requests:Vec<_>=self.asset_ids.iter().map(|&asset_id|AssetPermissions{
assetId:asset_id,
grantToDependencies:true,
}).collect();
let ref subject_id=self.universe_id.to_string();
let ref permissions=SetAssetsPermissions{
subjectType:"Universe",
subjectId:subject_id,
action:"Use",
enableDeepAccessCheck:true,
requests,
};
serde_json::to_string(permissions)
}
}
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 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(),
@@ -217,6 +410,29 @@ impl CookieContext{
Ok(resp)
}
async fn patch(&self,url:url::Url,body:impl ContentType+Clone)->Result<reqwest::Response,PostError>{
let mut resp=self.client.patch(url.clone())
.header("Cookie",self.cookie.as_str())
.header("Content-Type",body.content_type())
.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.patch(url)
.header("X-CSRF-Token",csrf_token)
.header("Cookie",self.cookie.as_str())
.header("Content-Type",body.content_type())
.body(body)
.send().await.map_err(PostError::Reqwest)?;
}else{
Err(PostError::CSRF)?;
}
}
Ok(resp)
}
pub async fn create(&self,config:CreateRequest,body:impl Into<reqwest::Body>+Clone)->Result<UploadResponse,CreateError>{
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
@@ -232,10 +448,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 +500,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 +536,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 +603,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 +618,35 @@ impl CookieContext{
query.append_pair("cursor",cursor);
}
}
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)
}
/// Used to enable an asset to be loaded onto a group game.
pub async fn set_assets_permissions(&self,config:SetAssetsPermissionsRequest<'_>)->Result<(),SetAssetsPermissionsError>{
let url=reqwest::Url::parse("https://apis.roblox.com/asset-permissions-api/v1/assets/permissions").map_err(SetAssetsPermissionsError::Parse)?;
self.get(url).await.map_err(InventoryPageError::Reqwest)?
.error_for_status().map_err(InventoryPageError::Reqwest)?
.json::<InventoryPageResponse>().await.map_err(InventoryPageError::Reqwest)
let body=config.serialize().map_err(SetAssetsPermissionsError::JSONEncode)?;
response_ok(
self.patch(url,Json(body)).await.map_err(SetAssetsPermissionsError::Patch)?
).await.map_err(SetAssetsPermissionsError::Response)?;
Ok(())
}
}

View File

@@ -1,2 +1,5 @@
pub mod cloud;
pub mod cookie;
pub mod types;
mod body;
mod util;

67
rbx_asset/src/types.rs Normal file
View File

@@ -0,0 +1,67 @@
#[derive(Debug)]
pub struct UrlAndBody{
pub url:url::Url,
pub body:String,
}
#[derive(Debug)]
pub enum ResponseError{
Reqwest(reqwest::Error),
Details{
status_code:reqwest::StatusCode,
url_and_body:Box<UrlAndBody>,
},
}
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 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(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<&'a [u8]>)->T,
ReadRaw:Fn(&'a [u8])->T,
{
match self.bytes.get(0..2){
Some(b"\x1f\x8b")=>read_gzip(GzDecoder::new(self.bytes.as_ref())),
_=>read_raw(self.bytes.as_ref())
}
}
}

39
rbx_asset/src/util.rs Normal file
View File

@@ -0,0 +1,39 @@
use crate::types::{ResponseError,UrlAndBody};
// 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::Details{
status_code,
url_and_body:Box::new(UrlAndBody{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())
}

View File

@@ -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"
regex = { version = "1.11.3", default-features = false }
rayon = "1.8.0"
rbx_dom_weak = "2.7.0"
rbx_xml = "0.13.3"
rbx_dom_weak = "4.0.0"
rbx_xml = "2.0.0"
tokio = { version = "1.35.1", features = ["fs"] }

176
rox_compiler/LICENSE-APACHE Normal file
View 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
View 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.

View File

@@ -28,6 +28,16 @@ impl std::fmt::Display for PropertiesOverride{
}
}
pub(crate) fn sanitize(s:&str)->std::borrow::Cow<'_,str>{
lazy_regex::regex!(r"[^A-Za-z0-9.-]").replace_all(s,"_")
#[macro_export]
macro_rules! lazy_regex{
($r:literal)=>{{
use regex::Regex;
use std::sync::LazyLock;
static RE:LazyLock<Regex>=LazyLock::new(||Regex::new($r).unwrap());
&RE
}};
}
pub(crate) fn sanitize(s:&str)->std::borrow::Cow<'_,str>{
lazy_regex!(r"[^A-Za-z0-9.-]").replace_all(s,"_")
}

View File

@@ -2,6 +2,7 @@ use std::path::{Path,PathBuf};
use futures::{StreamExt, TryStreamExt};
use tokio::io::AsyncReadExt;
use crate::lazy_regex;
use crate::common::{sanitize,Style,PropertiesOverride};
//holy smokes what am I doing lmao
@@ -16,7 +17,6 @@ use crate::common::{sanitize,Style,PropertiesOverride};
//I could use a function!
//eventually:
#[derive(Debug)]
#[allow(dead_code)]//idk why this thinks it's dead code, the errors are printed out in various places
pub enum QueryResolveError{
NotFound,//0 results
Ambiguous,//>1 results
@@ -203,7 +203,7 @@ impl ScriptWithOverrides{
let mut count=0;
for line in source.lines(){
//only string type properties are supported atm
if let Some(captures)=lazy_regex::regex!(r#"^\-\-\s*Properties\.([A-Za-z]\w*)\s*\=\s*"(\w+)"$"#)
if let Some(captures)=lazy_regex!(r#"^\-\-\s*Properties\.([A-Za-z]\w*)\s*\=\s*"(\w+)"$"#)
.captures(line){
count+=line.len();
match &captures[1]{
@@ -340,7 +340,7 @@ impl CompileNode{
//reject goobers
let is_goober=matches!(style,Some(Style::Rojo));
let (ext_len,file_discernment)={
if let Some(captures)=lazy_regex::regex!(r"^.*(\.module\.lua|\.client\.lua|\.server\.lua)$")
if let Some(captures)=lazy_regex!(r"^.*(\.module\.lua|\.client\.lua|\.server\.lua)$")
.captures(file_name.as_str()){
let ext=&captures[1];
(ext.len(),match ext{
@@ -354,7 +354,7 @@ impl CompileNode{
".server.lua"=>FileDiscernment::Script(ScriptHint::Script),
_=>panic!("Regex failed"),
})
}else if let Some(captures)=lazy_regex::regex!(r"^.*(\.rbxmx|\.lua)$")
}else if let Some(captures)=lazy_regex!(r"^.*(\.rbxmx|\.lua)$")
.captures(file_name.as_str()){
let ext=&captures[1];
(ext.len(),match ext{

View File

@@ -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();

File diff suppressed because it is too large Load Diff