Skip to content
This repository has been archived by the owner on Jul 22, 2023. It is now read-only.

Add writeNewModelAsset and writeNewPlaceAsset #40

Closed
wants to merge 12 commits into from
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Remodel Changelog

## Unreleased Changes
* Added APIs for uploading new models and places on Roblox.com:
* `remodel.writeNewModelAsset`
* `remodel.writeNewPlaceAsset`

## 0.8.1 (2021-04-09)
* Updated to latest rbx_xml, which should fix `OptionalCoordinateFrame`-related issues.
Expand Down
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,54 @@ If the instance is a `DataModel`, this method will throw. Places should be saved

Throws on error.

### `remodel.writeNewPlaceAsset`
```
type Options {
name: string,
description: string?,
isPublic: boolean?,
allowComments: boolean?,
}

remodel.writeNewPlaceAsset(instance: DataModel, options: Options)
nezuo marked this conversation as resolved.
Show resolved Hide resolved
```

Uploads the given `DataModel` instance to Roblox.com as a new place with the corresponding options.

The options: `description`, `isPublic`, and `allowComments` are all optional.
``description`` default to an empty string. ``isPublic`` and ``allowComments`` default to ``false``.
nezuo marked this conversation as resolved.
Show resolved Hide resolved

``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`
```
type Options {
name: string,
description: string?,
isPublic: boolean?,
allowComments: boolean?,
}

remodel.writeNewModelAsset(instance: Instance, options: Options)
nezuo marked this conversation as resolved.
Show resolved Hide resolved
```

Uploads the given instance to Roblox.com as a new model with the corresponding options.

The options: `description`, `isPublic`, and `allowComments` are all optional.
``description`` default to an empty string. ``isPublic`` 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)
Expand Down
174 changes: 168 additions & 6 deletions src/remodel_api/remodel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use reqwest::{
header::{ACCEPT, CONTENT_TYPE, COOKIE, USER_AGENT},
StatusCode,
};
use rlua::{Context, UserData, UserDataMethods};
use rlua::{Context, Table, UserData, UserDataMethods, Value};

use crate::{
remodel_context::RemodelContext,
Expand All @@ -28,6 +28,56 @@ fn xml_decode_options() -> rbx_xml::DecodeOptions {
rbx_xml::DecodeOptions::new().property_behavior(rbx_xml::DecodePropertyBehavior::ReadUnknown)
}

fn get_required_string_option(options: &Table, option: &str) -> rlua::Result<String> {
let value = options.get(option).map_err(rlua::Error::external)?;

match value {
Value::String(value) => Ok(value.to_str().map_err(rlua::Error::external)?.to_string()),
Value::Nil => Err(rlua::Error::external(format!(
"The option {} must be specified",
option
))),
_ => Err(rlua::Error::external(format!(
"The option {} must be a string",
option
))),
}
}

fn get_string_option(options: &Table, option: &str, default: &str) -> rlua::Result<String> {
let value = options.get(option).map_err(rlua::Error::external)?;

match value {
Value::String(value) => Ok(value.to_str().map_err(rlua::Error::external)?.to_string()),
Value::Nil => Ok(default.to_string()),
_ => Err(rlua::Error::external(format!(
"The option {} must be a string",
option
))),
}
}

fn get_bool_option(options: &Table, option: &str, default: bool) -> rlua::Result<bool> {
let value = options.get(option).map_err(rlua::Error::external)?;

match value {
Value::Boolean(value) => Ok(value),
Value::Nil => Ok(default),
_ => Err(rlua::Error::external(format!(
"The option {} must be a bool",
option
))),
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These functions feel pretty heavy! We should be able to write one generic function for required parameters and one for optional ones. We can leverage the FromLua trait to make this work for any type and avoid a lot of boilerplate.


fn bool_into_query(boolean: bool) -> String {
if boolean {
String::from("True")
} else {
String::from("False")
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usage of this should not be pervasive enough to warrant a function; we should be able to keep the bool-to-query stuff entirely inside the code that actually does HTTP requests.


pub struct Remodel;

impl Remodel {
Expand Down Expand Up @@ -282,10 +332,11 @@ impl Remodel {
Remodel::import_tree_root(context, source_tree)
}

fn write_existing_model_asset(
fn write_model_asset(
context: Context<'_>,
lua_instance: LuaInstance,
asset_id: u64,
queries: &[(&str, &str)],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is the right interface here. We should instead accept a struct of known fields that are turned into the correct querystring parameters by this function.

) -> rlua::Result<()> {
let tree = lua_instance.tree.lock().unwrap();
let instance = tree
Expand All @@ -302,13 +353,14 @@ impl Remodel {
rbx_binary::to_writer_default(&mut buffer, &tree, &[lua_instance.id])
.map_err(rlua::Error::external)?;

Remodel::upload_asset(context, buffer, asset_id)
Remodel::upload_asset(context, buffer, asset_id, queries)
}

fn write_existing_place_asset(
fn write_place_asset(
context: Context<'_>,
lua_instance: LuaInstance,
asset_id: u64,
queries: &[(&str, &str)],
) -> rlua::Result<()> {
let tree = lua_instance.tree.lock().unwrap();
let instance = tree
Expand All @@ -325,10 +377,81 @@ impl Remodel {
rbx_binary::to_writer_default(&mut buffer, &tree, instance.children())
.map_err(rlua::Error::external)?;

Remodel::upload_asset(context, buffer, asset_id)
Remodel::upload_asset(context, buffer, asset_id, queries)
}

fn write_new_model_asset(
context: Context<'_>,
lua_instance: LuaInstance,
name: String,
description: String,
is_public: bool,
allow_comments: bool,
) -> rlua::Result<()> {
let is_public = bool_into_query(is_public);
let allow_comments = bool_into_query(allow_comments);

Remodel::write_model_asset(
context,
lua_instance,
0,
&[
("type", "Model"),
("name", &name),
("description", &description),
("isPublic", &is_public),
("allowComments", &allow_comments),
],
)
}

fn write_new_place_asset(
context: Context<'_>,
lua_instance: LuaInstance,
name: String,
description: String,
is_public: bool,
allow_comments: bool,
) -> rlua::Result<()> {
let is_public = bool_into_query(is_public);
let allow_comments = bool_into_query(allow_comments);

Remodel::write_place_asset(
context,
lua_instance,
0,
&[
("type", "Model"),
("name", &name),
("description", &description),
("isPublic", &is_public),
("allowComments", &allow_comments),
],
)
}

fn upload_asset(context: Context<'_>, buffer: Vec<u8>, asset_id: u64) -> rlua::Result<()> {
fn write_existing_model_asset(
context: Context<'_>,
lua_instance: LuaInstance,
asset_id: u64,
) -> rlua::Result<()> {
Remodel::write_model_asset(context, lua_instance, asset_id, &[])
}

fn write_existing_place_asset(
context: Context<'_>,
lua_instance: LuaInstance,
asset_id: u64,
) -> rlua::Result<()> {
Remodel::write_model_asset(context, lua_instance, asset_id, &[])
}

fn upload_asset(
context: Context<'_>,
buffer: Vec<u8>,
asset_id: u64,
queries: &[(&str, &str)],
) -> rlua::Result<()> {
let re_context = RemodelContext::get(context)?;
let auth_cookie = re_context.auth_cookie().ok_or_else(|| {
rlua::Error::external(
Expand All @@ -349,6 +472,7 @@ impl Remodel {
.header(USER_AGENT, "Roblox/WinInet")
.header(CONTENT_TYPE, "application/xml")
.header(ACCEPT, "application/json")
.query(queries)
.body(buffer.clone())
};

Expand Down Expand Up @@ -471,6 +595,44 @@ impl UserData for Remodel {
Remodel::read_place_asset(context, asset_id)
});

methods.add_function(
"writeNewModelAsset",
|context, (instance, options): (LuaInstance, Table)| {
let name = get_required_string_option(&options, "name")?;
let description = get_string_option(&options, "description", "")?;
let is_public = get_bool_option(&options, "isPublic", false)?;
let allow_comments = get_bool_option(&options, "allowComments", false)?;

Remodel::write_new_model_asset(
context,
instance,
name,
description,
is_public,
allow_comments,
)
},
);

methods.add_function(
"writeNewPlaceAsset",
|context, (instance, options): (LuaInstance, Table)| {
let name = get_required_string_option(&options, "name")?;
let description = get_string_option(&options, "description", "")?;
let is_public = get_bool_option(&options, "isPublic", false)?;
let allow_comments = get_bool_option(&options, "allowComments", false)?;

Remodel::write_new_place_asset(
context,
instance,
name,
description,
is_public,
allow_comments,
)
},
);

methods.add_function(
"writeExistingModelAsset",
|context, (instance, asset_id): (LuaInstance, String)| {
Expand Down