Skip to content

Commit

Permalink
handle audio upload quotas correctly (#139)
Browse files Browse the repository at this point in the history
* handle audio upload quotas correctly

* fix linter errors
  • Loading branch information
blake-mealey authored Mar 13, 2022
1 parent c5aee0d commit 1592c83
Show file tree
Hide file tree
Showing 2 changed files with 146 additions and 83 deletions.
79 changes: 44 additions & 35 deletions src/lib/roblox_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,14 +258,6 @@ pub struct CreateImageAssetResponse {
pub backing_asset_id: AssetId,
}

#[derive(Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct GetCreateAudioAssetPriceResponse {
pub price: u32,
pub balance: u32,
pub can_afford: bool,
}

#[derive(Deserialize, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct CreateAudioAssetResponse {
Expand Down Expand Up @@ -330,6 +322,27 @@ pub struct GetSocialLinkResponse {
pub link_type: SocialLinkType,
}

#[derive(Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
struct CreateAssetQuotasResponse {
quotas: Vec<CreateAssetQuota>,
}

#[derive(Deserialize, Clone)]
#[serde(rename_all = "PascalCase")]
pub enum QuotaDuration {
Month,
}

#[derive(Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct CreateAssetQuota {
pub duration: QuotaDuration,
pub usage: u32,
pub capacity: u32,
pub expiration_time: Option<String>,
}

#[derive(Serialize, Deserialize, Clone)]
pub enum ExperienceGenre {
All,
Expand Down Expand Up @@ -449,6 +462,11 @@ pub enum AssetTypeId {
RightShoeAccessory = 71,
DressSkirtAccessory = 72,
}
impl fmt::Display for AssetTypeId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", serde_json::to_string(&self).unwrap(),)
}
}

#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
Expand Down Expand Up @@ -1773,37 +1791,28 @@ impl RobloxApi {
Self::handle_as_json_with_status(req).await
}

pub async fn get_create_audio_asset_price(
pub async fn get_create_asset_quota(
&self,
file_path: PathBuf,
group_id: Option<AssetId>,
) -> Result<GetCreateAudioAssetPriceResponse, String> {
let data = fs::read(&file_path).map_err(|e| {
format!(
"Unable to read audio asset file: {}\n\t{}",
file_path.display(),
e
)
})?;

let file_name = format!(
"Audio/{}",
file_path.file_stem().and_then(OsStr::to_str).unwrap()
);

asset_type: AssetTypeId,
) -> Result<CreateAssetQuota, String> {
let req = self
.client
.post("https://publish.roblox.com/v1/audio/verify")
.query(&[("name", &file_name)])
.header(reqwest::header::CONTENT_TYPE, "*/*")
.json(&json!({
"name": file_name,
"fileSize": data.len(),
"file": base64::encode(data),
"groupId": group_id,
}));
.get("https://publish.roblox.com/v1/asset-quotas")
.query(&[
// TODO: Understand what this parameter does
("resourceType", "1"),
("assetType", &asset_type.to_string()),
]);

Self::handle_as_json(req).await
// TODO: Understand how to interpret multiple quota objects (rather than just using the first one)
(Self::handle_as_json::<CreateAssetQuotasResponse>(req).await?)
.quotas
.first()
.cloned()
.ok_or(format!(
"No create quotas found for asset type {}",
asset_type
))
}

pub async fn create_audio_asset(
Expand Down
150 changes: 102 additions & 48 deletions src/lib/roblox_resource_manager.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
use std::path::{Path, PathBuf};

use async_trait::async_trait;
use chrono::Utc;
use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
use yansi::Paint;

use crate::logger;
use crate::{
logger,
roblox_api::{AssetTypeId, CreateAssetQuota, QuotaDuration},
};

use super::{
resource_graph::{
Expand All @@ -15,8 +18,8 @@ use super::{
CreateAudioAssetResponse, CreateBadgeResponse, CreateDeveloperProductResponse,
CreateExperienceResponse, CreateGamePassResponse, CreateImageAssetResponse,
CreateSocialLinkResponse, CreatorType, ExperienceConfigurationModel,
GetCreateAudioAssetPriceResponse, GetDeveloperProductResponse, GetPlaceResponse,
PlaceConfigurationModel, RobloxApi, SocialLinkType, UploadImageResponse,
GetDeveloperProductResponse, GetPlaceResponse, PlaceConfigurationModel, RobloxApi,
SocialLinkType, UploadImageResponse,
},
roblox_auth::RobloxAuth,
};
Expand Down Expand Up @@ -314,41 +317,24 @@ impl ResourceManager<RobloxInputs, RobloxOutputs> for RobloxResourceManager {
.roblox_api
.get_create_badge_free_quota(experience.asset_id)
.await?;

let quota_reset =
format_quota_reset((Utc::today() + Duration::days(1)).and_hms(0, 0, 0));

if free_quota > 0 {
let utc_now = Utc::now();
let utc_reset = (utc_now + chrono::Duration::days(1))
.date()
.and_hms(0, 0, 0);
let duration = utc_reset.signed_duration_since(utc_now);
let duration_str = format!(
"{:02}:{:02}:{:02}",
duration.num_hours(),
duration.num_minutes() - duration.num_hours() * 60,
duration.num_seconds() - duration.num_minutes() * 60
);
logger::log("");
logger::log(Paint::yellow(
format!("You will have {} free badge(s) remaining in the current period after creation. Your quota will reset in {}.", free_quota - 1, duration_str),
format!("You will have {} free badge(s) remaining in the current period after creation. Your quota will reset in {}.", free_quota - 1, quota_reset),
));
Ok(None)
} else {
Ok(Some(100))
}
}
RobloxInputs::AudioAsset(inputs) => {
let GetCreateAudioAssetPriceResponse {
price, can_afford, ..
} = self
.roblox_api
.get_create_audio_asset_price(self.get_path(inputs.file_path), inputs.group_id)
.await?;
logger::log("");
logger::log(Paint::yellow(
format!("You have no free badges remaining in the current period. Your quota will reset in {}.", quota_reset),
));

// TODO: Add support for failing early like this for all other resource types (e.g. return the price and current balance from this function)
if !can_afford {
return Err(format!("You do not have enough Robux to create an audio asset with the price of {}", price));
Ok(Some(100))
}

Ok(Some(price))
}
_ => Ok(None),
}
Expand Down Expand Up @@ -593,16 +579,54 @@ impl ResourceManager<RobloxInputs, RobloxOutputs> for RobloxResourceManager {
}))
}
RobloxInputs::AudioAsset(inputs) => {
let CreateAudioAssetResponse { id } = self
let CreateAssetQuota {
usage,
capacity,
expiration_time,
duration,
} = self
.roblox_api
.create_audio_asset(
self.get_path(inputs.file_path),
inputs.group_id,
self.payment_source.clone(),
)
.await?;
.get_create_asset_quota(AssetTypeId::Audio)
.await?;

let quota_reset = format_quota_reset(match expiration_time {
Some(ref x) => DateTime::parse_from_rfc3339(x)
.map_err(|e| format!("Unable to parse expiration_time: {}", e))?
.with_timezone(&Utc),
None => {
Utc::now()
+ match duration {
// TODO: Learn how Roblox computes a "Month" to ensure this is an accurate estimate
QuotaDuration::Month => Duration::days(30),
}
}
});

if usage < capacity {
logger::log("");
logger::log(Paint::yellow(
format!(
"You will have {} audio upload(s) remaining in the current period after creation. Your quota will reset in {}.",
capacity - usage - 1,
quota_reset
)));

let CreateAudioAssetResponse { id } = self
.roblox_api
.create_audio_asset(
self.get_path(inputs.file_path),
inputs.group_id,
self.payment_source.clone(),
)
.await?;

Ok(RobloxOutputs::AudioAsset(AssetOutputs { asset_id: id }))
Ok(RobloxOutputs::AudioAsset(AssetOutputs { asset_id: id }))
} else {
Err(format!(
"You have reached your audio upload quota. Your quota will reset in {}.",
quota_reset
))
}
}
RobloxInputs::AssetAlias(inputs) => {
let experience = single_output!(dependency_outputs, RobloxOutputs::Experience);
Expand All @@ -628,16 +652,11 @@ impl ResourceManager<RobloxInputs, RobloxOutputs> for RobloxResourceManager {

async fn get_update_price(
&self,
inputs: RobloxInputs,
outputs: RobloxOutputs,
dependency_outputs: Vec<RobloxOutputs>,
_inputs: RobloxInputs,
_outputs: RobloxOutputs,
_dependency_outputs: Vec<RobloxOutputs>,
) -> Result<Option<u32>, String> {
match (inputs.clone(), outputs) {
(RobloxInputs::AudioAsset(_), RobloxOutputs::AudioAsset(_)) => {
self.get_create_price(inputs, dependency_outputs).await
}
_ => Ok(None),
}
Ok(None)
}

// TODO: Consider moving `outputs` into `dependency_outputs`.
Expand Down Expand Up @@ -927,3 +946,38 @@ impl ResourceManager<RobloxInputs, RobloxOutputs> for RobloxResourceManager {
Ok(())
}
}

fn format_quota_reset(reset: DateTime<Utc>) -> String {
let now = Utc::now();
let duration = reset.signed_duration_since(now);

let mut parts = Vec::<String>::new();
if duration.num_days() > 0 {
parts.push(format!("{}d", duration.num_days()));
}
if duration.num_hours() > 0 {
parts.push(format!(
"{}h",
duration.num_hours() - duration.num_days() * 24
));
}
if duration.num_minutes() > 0 {
parts.push(format!(
"{}m",
duration.num_minutes() - duration.num_hours() * 60
));
}
if duration.num_seconds() > 0 {
parts.push(format!(
"{}s",
duration.num_seconds() - duration.num_minutes() * 60
));
} else {
parts.push(format!(
"{}ms",
duration.num_milliseconds() - duration.num_seconds() * 1000
));
}

parts.join(" ")
}

0 comments on commit 1592c83

Please sign in to comment.