diff --git a/CHANGELOG.md b/CHANGELOG.md index 61d10c6..5afc6ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Remodel Changelog ## Unreleased Changes +* Added APIs for uploading new models and places: + * `remodel.writeNewPlaceAsset` + * `remodel.writeNewModelAsset` ## 0.10.0 (2022-06-13) * Switched from `rlua` to `mlua`, which should improve Lua performance slightly. ([#73]) diff --git a/README.md b/README.md index b85ac51..1e889e3 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,54 @@ If the instance is a `DataModel`, this method will throw. Places should be saved Throws on error. +### `remodel.writeNewPlaceAsset` +``` +remodel.writeNewPlaceAsset(instance: DataModel, options: Options): string + +where Options: { + name: string, + description: string?, + public: boolean?, + allowComments: boolean?, + groupId: string?, +} +``` + +Uploads the given `DataModel` instance to Roblox.com as a new place and returns its id. + +`description` and `groupId` default to an empty string. `public` and `allowComments` default to `false`. + +`allowComments` does not have any function for places. + +If the instance is not a `DataModel`, this method will throw. Models should be uploaded with `writeNewModelAsset` instead. + +**This method always requires web authentication! See [Authentication](#authentication) for more information.** + +Throws on error. + +### `remodel.writeNewModelAsset` +``` +remodel.writeNewModelAsset(instance: Instance, options: Options): string + +where Options: { + name: string, + description: string?, + public: boolean?, + allowComments: boolean?, + groupId: string?, +} +``` + +Uploads the given instance to Roblox.com as a new model and returns its id. + +`description` and `groupId` default to an empty string. `public` and `allowComments` default to `false`. + +If the instance is a `DataModel`, this method will throw. Places should be uploaded with `writeNewPlaceAsset` instead. + +**This method always requires web authentication! See [Authentication](#authentication) for more information.** + +Throws on error. + ### `remodel.writeExistingPlaceAsset` (0.5.0+) ``` remodel.writeExistingPlaceAsset(instance: Instance, assetId: string) diff --git a/src/remodel_api/remodel.rs b/src/remodel_api/remodel.rs index 4c39846..f66bd41 100644 --- a/src/remodel_api/remodel.rs +++ b/src/remodel_api/remodel.rs @@ -7,12 +7,13 @@ use std::{ time::Duration, }; -use mlua::{Lua, UserData, UserDataMethods}; +use mlua::{FromLua, Lua, UserData, UserDataMethods, Value}; use rbx_dom_weak::{types::VariantType, InstanceBuilder, WeakDom}; use reqwest::{ header::{ACCEPT, CONTENT_TYPE, COOKIE, USER_AGENT}, StatusCode, }; +use serde::Serialize; use crate::{ remodel_context::RemodelContext, @@ -29,6 +30,44 @@ fn xml_decode_options() -> rbx_xml::DecodeOptions { rbx_xml::DecodeOptions::new().property_behavior(rbx_xml::DecodePropertyBehavior::ReadUnknown) } +#[derive(Clone)] +struct UploadQuery { + asset_type: String, + upload_options: UploadOptions, +} + +#[derive(Clone, Serialize)] +struct UploadOptions { + name: String, + description: String, + #[serde(rename(serialize = "isPublic"))] + public: bool, + #[serde(rename(serialize = "allowComments"))] + allow_comments: bool, + #[serde(rename(serialize = "groupId"))] + group_id: Option, +} + +impl<'lua> FromLua<'lua> for UploadOptions { + fn from_lua(lua_value: Value<'lua>, _: &'lua Lua) -> mlua::Result { + if let Value::Table(table) = lua_value { + let description: Option<_> = table.get("description")?; + let public: Option<_> = table.get("public")?; + let allow_comments: Option<_> = table.get("allowComments")?; + + Ok(UploadOptions { + name: table.get("name")?, + description: description.unwrap_or_default(), + public: public.unwrap_or_default(), + allow_comments: allow_comments.unwrap_or_default(), + group_id: table.get("groupId")?, + }) + } else { + Err(mlua::Error::external("expected table")) + } + } +} + pub struct Remodel; impl Remodel { @@ -283,11 +322,12 @@ impl Remodel { Remodel::import_tree_root(context, source_tree) } - fn write_existing_model_asset( + fn write_model_asset( context: &Lua, lua_instance: LuaInstance, asset_id: u64, - ) -> mlua::Result<()> { + upload_query: Option, + ) -> mlua::Result { let tree = lua_instance.tree.lock().unwrap(); let instance = tree .get_by_ref(lua_instance.id) @@ -303,14 +343,15 @@ impl Remodel { rbx_binary::to_writer(&mut buffer, &tree, &[lua_instance.id]) .map_err(mlua::Error::external)?; - Remodel::upload_asset(context, buffer, asset_id) + Remodel::upload_asset(context, buffer, asset_id, upload_query) } - fn write_existing_place_asset( + fn write_place_asset( context: &Lua, lua_instance: LuaInstance, asset_id: u64, - ) -> mlua::Result<()> { + upload_query: Option, + ) -> mlua::Result { let tree = lua_instance.tree.lock().unwrap(); let instance = tree .get_by_ref(lua_instance.id) @@ -326,10 +367,67 @@ impl Remodel { rbx_binary::to_writer(&mut buffer, &tree, instance.children()) .map_err(mlua::Error::external)?; - Remodel::upload_asset(context, buffer, asset_id) + Remodel::upload_asset(context, buffer, asset_id, upload_query) + } + + fn write_new_model_asset( + context: &Lua, + lua_instance: LuaInstance, + upload_options: UploadOptions, + ) -> mlua::Result { + Remodel::write_model_asset( + context, + lua_instance, + 0, + Some(UploadQuery { + asset_type: "Model".to_string(), + upload_options, + }), + ) + } + + fn write_new_place_asset( + context: &Lua, + lua_instance: LuaInstance, + upload_options: UploadOptions, + ) -> mlua::Result { + Remodel::write_place_asset( + context, + lua_instance, + 0, + Some(UploadQuery { + asset_type: "Place".to_string(), + upload_options, + }), + ) + } + + fn write_existing_model_asset( + context: &Lua, + lua_instance: LuaInstance, + asset_id: u64, + ) -> mlua::Result<()> { + Remodel::write_model_asset(context, lua_instance, asset_id, None)?; + + Ok(()) + } + + fn write_existing_place_asset( + context: &Lua, + lua_instance: LuaInstance, + asset_id: u64, + ) -> mlua::Result<()> { + Remodel::write_place_asset(context, lua_instance, asset_id, None)?; + + Ok(()) } - fn upload_asset(context: &Lua, buffer: Vec, asset_id: u64) -> mlua::Result<()> { + fn upload_asset( + context: &Lua, + buffer: Vec, + asset_id: u64, + upload_query: Option, + ) -> mlua::Result { let re_context = RemodelContext::get(context)?; let auth_cookie = re_context.auth_cookie().ok_or_else(|| { mlua::Error::external( @@ -348,13 +446,21 @@ impl Remodel { .map_err(mlua::Error::external)?; let build_request = move || { - client + let mut request = client .post(&url) .header(COOKIE, format!(".ROBLOSECURITY={}", auth_cookie)) .header(USER_AGENT, "Roblox/WinInet") .header(CONTENT_TYPE, "application/xml") .header(ACCEPT, "application/json") - .body(buffer.clone()) + .body(buffer.clone()); + + if let Some(upload_query) = upload_query.clone() { + request = request + .query(&[("type", upload_query.asset_type)]) + .query(&upload_query.upload_options); + } + + request }; log::debug!("Uploading to Roblox..."); @@ -374,7 +480,14 @@ impl Remodel { } if response.status().is_success() { - Ok(()) + match response.headers().get("roblox-assetid") { + Some(asset_id) => Ok(asset_id + .to_str() + .map_err(mlua::Error::external)? + .parse() + .map_err(mlua::Error::external)?), + None => Err(mlua::Error::external("Roblox API didn't return asset id")), + } } else { Err(mlua::Error::external(format!( "Roblox API returned an error, status {}.", @@ -476,6 +589,26 @@ impl UserData for Remodel { Remodel::read_place_asset(context, asset_id) }); + methods.add_function( + "writeNewModelAsset", + |context, (lua_instance, upload_options): (LuaInstance, UploadOptions)| { + Ok( + Remodel::write_new_model_asset(context, lua_instance, upload_options)? + .to_string(), + ) + }, + ); + + methods.add_function( + "writeNewPlaceAsset", + |context, (lua_instance, upload_options): (LuaInstance, UploadOptions)| { + Ok( + Remodel::write_new_place_asset(context, lua_instance, upload_options)? + .to_string(), + ) + }, + ); + methods.add_function( "writeExistingModelAsset", |context, (instance, asset_id): (LuaInstance, String)| {