1use std::path::PathBuf;
2
3use anyhow::{bail, Context};
4use clap::Parser;
5use memofs::Vfs;
6use reqwest::{
7 header::{ACCEPT, CONTENT_TYPE, COOKIE, USER_AGENT},
8 StatusCode,
9};
10
11use crate::{auth_cookie::get_auth_cookie, serve_session::ServeSession};
12
13use super::resolve_path;
14
15#[derive(Debug, Parser)]
17pub struct UploadCommand {
18 #[clap(default_value = "")]
20 pub project: PathBuf,
21
22 #[clap(long)]
24 pub cookie: Option<String>,
25
26 #[clap(long = "api_key")]
28 pub api_key: Option<String>,
29
30 #[clap(long = "universe_id")]
32 pub universe_id: Option<u64>,
33
34 #[clap(long = "asset_id")]
36 pub asset_id: u64,
37}
38
39impl UploadCommand {
40 pub fn run(self) -> Result<(), anyhow::Error> {
41 let project_path = resolve_path(&self.project);
42
43 let vfs = Vfs::new_default();
44
45 let session = ServeSession::new(vfs, project_path)?;
46
47 let tree = session.tree();
48 let inner_tree = tree.inner();
49 let root = inner_tree.root();
50
51 let encode_ids = match root.class.as_str() {
52 "DataModel" => root.children().to_vec(),
53 _ => vec![root.referent()],
54 };
55
56 let mut buffer = Vec::new();
57
58 log::trace!("Encoding binary model");
59 rbx_binary::to_writer(&mut buffer, tree.inner(), &encode_ids)?;
60
61 match (self.cookie, self.api_key, self.universe_id) {
62 (cookie, None, universe) => {
63 if universe.is_some() {
65 log::warn!(
66 "--universe_id was provided but is ignored when using legacy upload"
67 );
68 }
69
70 let cookie = cookie.or_else(get_auth_cookie).context(
71 "Rojo could not find your Roblox auth cookie. Please pass one via --cookie.",
72 )?;
73 do_upload(buffer, self.asset_id, &cookie)
74 }
75
76 (cookie, Some(api_key), Some(universe_id)) => {
77 if cookie.is_some() {
79 log::warn!("--cookie was provided but is ignored when using Open Cloud API");
80 }
81
82 do_upload_open_cloud(buffer, universe_id, self.asset_id, &api_key)
83 }
84
85 (_, Some(_), None) => {
86 bail!("--universe_id must be provided to use the Open Cloud API");
88 }
89 }
90 }
91}
92
93fn do_upload(buffer: Vec<u8>, asset_id: u64, cookie: &str) -> anyhow::Result<()> {
94 let url = format!(
95 "https://data.roblox.com/Data/Upload.ashx?assetid={}",
96 asset_id
97 );
98
99 let client = reqwest::blocking::Client::new();
100
101 let build_request = move || {
102 client
103 .post(&url)
104 .header(COOKIE, format!(".ROBLOSECURITY={}", cookie))
105 .header(USER_AGENT, "Roblox/WinInet")
106 .header(CONTENT_TYPE, "application/xml")
107 .header(ACCEPT, "application/json")
108 .body(buffer.clone())
109 };
110
111 log::debug!("Uploading to Roblox...");
112 let mut response = build_request().send()?;
113
114 if response.status() == StatusCode::FORBIDDEN {
118 if let Some(csrf_token) = response.headers().get("X-CSRF-Token") {
119 log::debug!("Received CSRF challenge, retrying with token...");
120 response = build_request().header("X-CSRF-Token", csrf_token).send()?;
121 }
122 }
123
124 let status = response.status();
125 if !status.is_success() {
126 bail!(
127 "The Roblox API returned an unexpected error: {}",
128 response.text()?
129 );
130 }
131
132 Ok(())
133}
134
135fn do_upload_open_cloud(
138 buffer: Vec<u8>,
139 universe_id: u64,
140 asset_id: u64,
141 api_key: &str,
142) -> anyhow::Result<()> {
143 let url = format!(
144 "https://apis.roblox.com/universes/v1/{}/places/{}/versions?versionType=Published",
145 universe_id, asset_id
146 );
147
148 let client = reqwest::blocking::Client::new();
149
150 log::debug!("Uploading to Roblox...");
151 let response = client
152 .post(url)
153 .header("x-api-key", api_key)
154 .header(CONTENT_TYPE, "application/xml")
155 .header(ACCEPT, "application/json")
156 .body(buffer)
157 .send()?;
158
159 let status = response.status();
160 if !status.is_success() {
161 bail!(
162 "The Roblox API returned an unexpected error: {}",
163 response.text()?
164 );
165 }
166
167 Ok(())
168}