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