1use std::{
5 collections::{HashMap, HashSet},
6 fs,
7 path::PathBuf,
8 str::FromStr,
9 sync::Arc,
10};
11
12use hyper::{body, Body, Method, Request, Response, StatusCode};
13use opener::OpenError;
14use rbx_dom_weak::{
15 types::{Ref, Variant},
16 InstanceBuilder, UstrMap, WeakDom,
17};
18
19use crate::{
20 json,
21 serve_session::ServeSession,
22 snapshot::{InstanceWithMeta, PatchSet, PatchUpdate},
23 web::{
24 interface::{
25 ErrorResponse, Instance, OpenResponse, ReadResponse, ServerInfoResponse,
26 SubscribeMessage, SubscribeResponse, WriteRequest, WriteResponse, PROTOCOL_VERSION,
27 SERVER_VERSION,
28 },
29 util::{json, json_ok},
30 },
31 web_api::{BufferEncode, InstanceUpdate, RefPatchResponse, SerializeResponse},
32};
33
34pub async fn call(serve_session: Arc<ServeSession>, request: Request<Body>) -> Response<Body> {
35 let service = ApiService::new(serve_session);
36
37 match (request.method(), request.uri().path()) {
38 (&Method::GET, "/api/rojo") => service.handle_api_rojo().await,
39 (&Method::GET, path) if path.starts_with("/api/read/") => {
40 service.handle_api_read(request).await
41 }
42 (&Method::GET, path) if path.starts_with("/api/subscribe/") => {
43 service.handle_api_subscribe(request).await
44 }
45 (&Method::GET, path) if path.starts_with("/api/serialize/") => {
46 service.handle_api_serialize(request).await
47 }
48 (&Method::GET, path) if path.starts_with("/api/ref-patch/") => {
49 service.handle_api_ref_patch(request).await
50 }
51
52 (&Method::POST, path) if path.starts_with("/api/open/") => {
53 service.handle_api_open(request).await
54 }
55 (&Method::POST, "/api/write") => service.handle_api_write(request).await,
56
57 (_method, path) => json(
58 ErrorResponse::not_found(format!("Route not found: {}", path)),
59 StatusCode::NOT_FOUND,
60 ),
61 }
62}
63
64pub struct ApiService {
65 serve_session: Arc<ServeSession>,
66}
67
68impl ApiService {
69 pub fn new(serve_session: Arc<ServeSession>) -> Self {
70 ApiService { serve_session }
71 }
72
73 async fn handle_api_rojo(&self) -> Response<Body> {
75 let tree = self.serve_session.tree();
76 let root_instance_id = tree.get_root_id();
77
78 json_ok(&ServerInfoResponse {
79 server_version: SERVER_VERSION.to_owned(),
80 protocol_version: PROTOCOL_VERSION,
81 session_id: self.serve_session.session_id(),
82 project_name: self.serve_session.project_name().to_owned(),
83 expected_place_ids: self.serve_session.serve_place_ids().cloned(),
84 unexpected_place_ids: self.serve_session.blocked_place_ids().cloned(),
85 place_id: self.serve_session.place_id(),
86 game_id: self.serve_session.game_id(),
87 root_instance_id,
88 })
89 }
90
91 async fn handle_api_subscribe(&self, request: Request<Body>) -> Response<Body> {
94 let argument = &request.uri().path()["/api/subscribe/".len()..];
95 let input_cursor: u32 = match argument.parse() {
96 Ok(v) => v,
97 Err(err) => {
98 return json(
99 ErrorResponse::bad_request(format!("Malformed message cursor: {}", err)),
100 StatusCode::BAD_REQUEST,
101 );
102 }
103 };
104
105 let session_id = self.serve_session.session_id();
106
107 let result = self
108 .serve_session
109 .message_queue()
110 .subscribe(input_cursor)
111 .await;
112
113 let tree_handle = self.serve_session.tree_handle();
114
115 match result {
116 Ok((message_cursor, messages)) => {
117 let tree = tree_handle.lock().unwrap();
118
119 let api_messages = messages
120 .into_iter()
121 .map(|patch| SubscribeMessage::from_patch_update(&tree, patch))
122 .collect();
123
124 json_ok(SubscribeResponse {
125 session_id,
126 message_cursor,
127 messages: api_messages,
128 })
129 }
130 Err(_) => json(
131 ErrorResponse::internal_error("Message queue disconnected sender"),
132 StatusCode::INTERNAL_SERVER_ERROR,
133 ),
134 }
135 }
136
137 async fn handle_api_write(&self, request: Request<Body>) -> Response<Body> {
138 let session_id = self.serve_session.session_id();
139 let tree_mutation_sender = self.serve_session.tree_mutation_sender();
140
141 let body = body::to_bytes(request.into_body()).await.unwrap();
142
143 let request: WriteRequest = match json::from_slice(&body) {
144 Ok(request) => request,
145 Err(err) => {
146 return json(
147 ErrorResponse::bad_request(format!("Invalid body: {}", err)),
148 StatusCode::BAD_REQUEST,
149 );
150 }
151 };
152
153 if request.session_id != session_id {
154 return json(
155 ErrorResponse::bad_request("Wrong session ID"),
156 StatusCode::BAD_REQUEST,
157 );
158 }
159
160 let updated_instances = request
161 .updated
162 .into_iter()
163 .map(|update| PatchUpdate {
164 id: update.id,
165 changed_class_name: update.changed_class_name,
166 changed_name: update.changed_name,
167 changed_properties: update.changed_properties,
168 changed_metadata: None,
169 })
170 .collect();
171
172 tree_mutation_sender
173 .send(PatchSet {
174 removed_instances: Vec::new(),
175 added_instances: Vec::new(),
176 updated_instances,
177 })
178 .unwrap();
179
180 json_ok(WriteResponse { session_id })
181 }
182
183 async fn handle_api_read(&self, request: Request<Body>) -> Response<Body> {
184 let argument = &request.uri().path()["/api/read/".len()..];
185 let requested_ids: Result<Vec<Ref>, _> = argument.split(',').map(Ref::from_str).collect();
186
187 let requested_ids = match requested_ids {
188 Ok(ids) => ids,
189 Err(_) => {
190 return json(
191 ErrorResponse::bad_request("Malformed ID list"),
192 StatusCode::BAD_REQUEST,
193 );
194 }
195 };
196
197 let message_queue = self.serve_session.message_queue();
198 let message_cursor = message_queue.cursor();
199
200 let tree = self.serve_session.tree();
201
202 let mut instances = HashMap::new();
203
204 for id in requested_ids {
205 if let Some(instance) = tree.get_instance(id) {
206 instances.insert(id, Instance::from_rojo_instance(instance));
207
208 for descendant in tree.descendants(id) {
209 instances.insert(descendant.id(), Instance::from_rojo_instance(descendant));
210 }
211 }
212 }
213
214 json_ok(ReadResponse {
215 session_id: self.serve_session.session_id(),
216 message_cursor,
217 instances,
218 })
219 }
220
221 async fn handle_api_serialize(&self, request: Request<Body>) -> Response<Body> {
229 let argument = &request.uri().path()["/api/serialize/".len()..];
230 let requested_ids: Result<Vec<Ref>, _> = argument.split(',').map(Ref::from_str).collect();
231
232 let requested_ids = match requested_ids {
233 Ok(ids) => ids,
234 Err(_) => {
235 return json(
236 ErrorResponse::bad_request("Malformed ID list"),
237 StatusCode::BAD_REQUEST,
238 );
239 }
240 };
241 let mut response_dom = WeakDom::new(InstanceBuilder::new("Folder"));
242
243 let tree = self.serve_session.tree();
244 for id in &requested_ids {
245 if let Some(instance) = tree.get_instance(*id) {
246 let clone = response_dom.insert(
247 Ref::none(),
248 InstanceBuilder::new(instance.class_name())
249 .with_name(instance.name())
250 .with_properties(instance.properties().clone()),
251 );
252 let object_value = response_dom.insert(
253 response_dom.root_ref(),
254 InstanceBuilder::new("ObjectValue")
255 .with_name(id.to_string())
256 .with_property("Value", clone),
257 );
258
259 let mut child_ref = clone;
260 if let Some(parent_class) = parent_requirements(&instance.class_name()) {
261 child_ref =
262 response_dom.insert(object_value, InstanceBuilder::new(parent_class));
263 response_dom.transfer_within(clone, child_ref);
264 }
265
266 response_dom.transfer_within(child_ref, object_value);
267 } else {
268 json(
269 ErrorResponse::bad_request(format!("provided id {id} is not in the tree")),
270 StatusCode::BAD_REQUEST,
271 );
272 }
273 }
274 drop(tree);
275
276 let mut source = Vec::new();
277 rbx_binary::to_writer(&mut source, &response_dom, &[response_dom.root_ref()]).unwrap();
278
279 json_ok(SerializeResponse {
280 session_id: self.serve_session.session_id(),
281 model_contents: BufferEncode::new(source),
282 })
283 }
284
285 async fn handle_api_ref_patch(self, request: Request<Body>) -> Response<Body> {
290 let argument = &request.uri().path()["/api/ref-patch/".len()..];
291 let requested_ids: Result<HashSet<Ref>, _> =
292 argument.split(',').map(Ref::from_str).collect();
293
294 let requested_ids = match requested_ids {
295 Ok(ids) => ids,
296 Err(_) => {
297 return json(
298 ErrorResponse::bad_request("Malformed ID list"),
299 StatusCode::BAD_REQUEST,
300 );
301 }
302 };
303
304 let mut instance_updates: HashMap<Ref, InstanceUpdate> = HashMap::new();
305
306 let tree = self.serve_session.tree();
307 for instance in tree.descendants(tree.get_root_id()) {
308 for (prop_name, prop_value) in instance.properties() {
309 let Variant::Ref(prop_value) = prop_value else {
310 continue;
311 };
312 if let Some(target_id) = requested_ids.get(prop_value) {
313 let instance_id = instance.id();
314 let update =
315 instance_updates
316 .entry(instance_id)
317 .or_insert_with(|| InstanceUpdate {
318 id: instance_id,
319 changed_class_name: None,
320 changed_name: None,
321 changed_metadata: None,
322 changed_properties: UstrMap::default(),
323 });
324 update
325 .changed_properties
326 .insert(*prop_name, Some(Variant::Ref(*target_id)));
327 }
328 }
329 }
330
331 json_ok(RefPatchResponse {
332 session_id: self.serve_session.session_id(),
333 patch: SubscribeMessage {
334 added: HashMap::new(),
335 removed: Vec::new(),
336 updated: instance_updates.into_values().collect(),
337 },
338 })
339 }
340
341 async fn handle_api_open(&self, request: Request<Body>) -> Response<Body> {
343 let argument = &request.uri().path()["/api/open/".len()..];
344 let requested_id = match Ref::from_str(argument) {
345 Ok(id) => id,
346 Err(_) => {
347 return json(
348 ErrorResponse::bad_request("Invalid instance ID"),
349 StatusCode::BAD_REQUEST,
350 );
351 }
352 };
353
354 let tree = self.serve_session.tree();
355
356 let instance = match tree.get_instance(requested_id) {
357 Some(instance) => instance,
358 None => {
359 return json(
360 ErrorResponse::bad_request("Instance not found"),
361 StatusCode::NOT_FOUND,
362 );
363 }
364 };
365
366 let script_path = match pick_script_path(instance) {
367 Some(path) => path,
368 None => {
369 return json(
370 ErrorResponse::bad_request(
371 "No appropriate file could be found to open this script",
372 ),
373 StatusCode::NOT_FOUND,
374 );
375 }
376 };
377
378 match opener::open(&script_path) {
379 Ok(()) => {}
380 Err(error) => match error {
381 OpenError::Io(io_error) => {
382 return json(
383 ErrorResponse::internal_error(format!(
384 "Attempting to open {} failed because of the following io error: {}",
385 script_path.display(),
386 io_error
387 )),
388 StatusCode::INTERNAL_SERVER_ERROR,
389 )
390 }
391 OpenError::ExitStatus {
392 cmd,
393 status,
394 stderr,
395 } => {
396 return json(
397 ErrorResponse::internal_error(format!(
398 r#"The command '{}' to open '{}' failed with the error code '{}'.
399 Error logs:
400 {}"#,
401 cmd,
402 script_path.display(),
403 status,
404 stderr
405 )),
406 StatusCode::INTERNAL_SERVER_ERROR,
407 )
408 }
409 },
410 };
411
412 json_ok(OpenResponse {
413 session_id: self.serve_session.session_id(),
414 })
415 }
416}
417
418fn pick_script_path(instance: InstanceWithMeta<'_>) -> Option<PathBuf> {
421 match instance.class_name().as_str() {
422 "Script" | "LocalScript" | "ModuleScript" => {}
423 _ => return None,
424 }
425
426 instance
429 .metadata()
430 .relevant_paths
431 .iter()
432 .find(|path| {
433 match path.extension().and_then(|ext| ext.to_str()) {
435 Some("lua") => {}
436 Some("luau") => {}
437 _ => return false,
438 }
439
440 fs::metadata(path)
441 .map(|meta| meta.is_file())
442 .unwrap_or(false)
443 })
444 .map(|path| path.to_owned())
445}
446
447fn parent_requirements(class: &str) -> Option<&str> {
453 Some(match class {
454 "Attachment" | "Bone" => "Part",
455 "Animator" => "Humanoid",
456 "BaseWrap" | "WrapLayer" | "WrapTarget" | "WrapDeformer" => "MeshPart",
457 _ => return None,
458 })
459}