Skip to content

Commit

Permalink
Automatically retry uploads in tarmac sync if names are moderated
Browse files Browse the repository at this point in the history
  • Loading branch information
LPGhatguy committed Jun 9, 2020
1 parent be5b648 commit 7581f5e
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 8 deletions.
98 changes: 96 additions & 2 deletions src/roblox_web_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,21 @@ pub struct ImageUploadData<'a> {
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct UploadResponse {
pub success: bool,
pub asset_id: u64,
pub backing_asset_id: u64,
}

/// Internal representation of what the asset upload endpoint returns, before
/// we've handled any errors.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct RawUploadResponse {
success: bool,
message: Option<String>,
asset_id: Option<u64>,
backing_asset_id: Option<u64>,
}

pub struct RobloxApiClient {
auth_token: Option<String>,
csrf_token: Option<HeaderValue>,
Expand Down Expand Up @@ -59,10 +69,82 @@ impl RobloxApiClient {
Ok(buffer)
}

/// Upload an image, retrying if the asset endpoint determines that the
/// asset's name is inappropriate. The asset's name will be replaced with a
/// generic known-good string.
pub fn upload_image_with_moderation_retry(
&mut self,
data: ImageUploadData,
) -> Result<UploadResponse, RobloxApiError> {
let response = self.upload_image_raw(&data)?;

// Some other errors will be reported inside the response, even
// though we received a successful HTTP response.
if response.success {
let asset_id = response.asset_id.unwrap();
let backing_asset_id = response.backing_asset_id.unwrap();

Ok(UploadResponse {
asset_id,
backing_asset_id,
})
} else {
let message = response.message.unwrap();

// There are no status codes for this API, so we pattern match
// on the returned error message.
//
// If the error message text mentions something being
// inappropriate, we assume the title was problematic and
// attempt to re-upload.
if message.contains("inappropriate") {
log::warn!(
"Image name '{}' was moderated, retrying with different name...",
data.name
);

let new_data = ImageUploadData {
name: "image",
..data
};

return self.upload_image(new_data);
} else {
Err(RobloxApiError::ApiError { message })
}
}
}

/// Upload an image, returning an error if anything goes wrong.
pub fn upload_image(
&mut self,
data: ImageUploadData,
) -> Result<UploadResponse, RobloxApiError> {
let response = self.upload_image_raw(&data)?;

// Some other errors will be reported inside the response, even
// though we received a successful HTTP response.
if response.success {
let asset_id = response.asset_id.unwrap();
let backing_asset_id = response.backing_asset_id.unwrap();

Ok(UploadResponse {
asset_id,
backing_asset_id,
})
} else {
let message = response.message.unwrap();

Err(RobloxApiError::ApiError { message })
}
}

/// Upload an image, returning the raw response returned by the endpoint,
/// which may have further failures to handle.
fn upload_image_raw(
&mut self,
data: &ImageUploadData,
) -> Result<RawUploadResponse, RobloxApiError> {
let mut url = "https://data.roblox.com/data/upload/json?assetTypeId=13".to_owned();

if let Some(group_id) = data.group_id {
Expand All @@ -77,8 +159,9 @@ impl RobloxApiClient {
.build()?)
})?;

let body = response.text().unwrap();
let body = response.text()?;

// Some errors will be reported through HTTP status codes, handled here.
if response.status().is_success() {
match serde_json::from_str(&body) {
Ok(response) => Ok(response),
Expand All @@ -92,6 +175,8 @@ impl RobloxApiClient {
}
}

/// Execute a request generated by the given function, retrying if the
/// endpoint requests that the user refreshes their CSRF token.
fn execute_with_csrf_retry<F>(&mut self, make_request: F) -> Result<Response, RobloxApiError>
where
F: Fn(&Client) -> Result<Request, RobloxApiError>,
Expand All @@ -113,13 +198,19 @@ impl RobloxApiClient {

Ok(self.client.execute(new_request)?)
} else {
// If the response did not return a CSRF token for us to
// retry with, this request was likely forbidden for other
// reasons.

Ok(response)
}
}
_ => Ok(response),
}
}

/// Attach required headers to a request object before sending it to a
/// Roblox API, like authentication and CSRF protection.
fn attach_headers(&self, request: &mut Request) {
if let Some(auth_token) = &self.auth_token {
let cookie_value = format!(".ROBLOSECURITY={}", auth_token);
Expand All @@ -144,6 +235,9 @@ pub enum RobloxApiError {
source: reqwest::Error,
},

#[error("Roblox API error: {message}")]
ApiError { message: String },

#[error("Roblox API returned success, but had malformed JSON response: {body}")]
BadResponseJson {
body: String,
Expand Down
14 changes: 8 additions & 6 deletions src/sync_backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,14 @@ impl<'a> SyncBackend for RobloxSyncBackend<'a> {
fn upload(&mut self, data: UploadInfo) -> Result<UploadResponse, Error> {
log::info!("Uploading {} to Roblox", &data.name);

let result = self.api_client.upload_image(ImageUploadData {
image_data: Cow::Owned(data.contents),
name: &data.name,
description: "Uploaded by Tarmac.",
group_id: self.upload_to_group_id,
});
let result = self
.api_client
.upload_image_with_moderation_retry(ImageUploadData {
image_data: Cow::Owned(data.contents),
name: &data.name,
description: "Uploaded by Tarmac.",
group_id: self.upload_to_group_id,
});

match result {
Ok(response) => {
Expand Down

0 comments on commit 7581f5e

Please sign in to comment.