Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

correctly implement get_create_price for badges #138

Merged
merged 3 commits into from
Mar 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions src/lib/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -486,12 +486,13 @@ pub struct ExperienceTargetConfig {
/// ```
///
/// :::caution
/// By default, Mantle does not have permission to make purchases with Robux. Since creating badges
/// costs Robux, you will need to use the `--allow-purchases` flag when you want to create them.
/// Each user can create up to 5 badges for free every day. After that, badges cost 100 Robux each. By
/// default, Mantle does not have permission to make purchases with Robux, so if you go over your daily
/// quota, you will need to use the `--allow-purchases` flag to create them.
/// :::
///
/// Because Roblox does not offer any way to delete badges, when a badge is "deleted" by
/// Mantle, it is updated in the following ways:
/// Because Roblox does not offer any way to delete badges, when a badge is "deleted" by Mantle, it is
/// updated in the following ways:
///
/// 1. It is disabled
/// 2. Its description is updated to: `Name: <name>\nEnabled: <enabled>\nDescription:\n<description>`
Expand Down
23 changes: 13 additions & 10 deletions src/lib/resource_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ pub trait ResourceManager<TInputs, TOutputs> {
&self,
inputs: TInputs,
dependency_outputs: Vec<TOutputs>,
price: Option<u32>,
) -> Result<TOutputs, String>;

async fn get_update_price(
Expand All @@ -80,6 +81,7 @@ pub trait ResourceManager<TInputs, TOutputs> {
inputs: TInputs,
outputs: TOutputs,
dependency_outputs: Vec<TOutputs>,
price: Option<u32>,
) -> Result<TOutputs, String>;

async fn delete(
Expand Down Expand Up @@ -143,8 +145,7 @@ where
pub fn get_outputs(&self, resource_id: &str) -> Option<TOutputs> {
self.resources
.get(resource_id)
.map(|resource| resource.get_outputs())
.flatten()
.and_then(|resource| resource.get_outputs())
}

fn get_dependency_graph(&self) -> BTreeMap<ResourceId, Vec<ResourceId>> {
Expand Down Expand Up @@ -416,7 +417,7 @@ where
.get_outputs()
.expect("Existing resource should have outputs.");

match manager
let price = match manager
.get_update_price(
resource.get_inputs(),
outputs.clone(),
Expand All @@ -430,7 +431,8 @@ where
logger::log(Paint::yellow(format!(
"{} Robux will be charged from your account.",
price
)))
)));
Some(price)
} else {
return OperationResult::Skipped(format!(
"Resource would cost {} Robux to create. Give Mantle permission to make purchases with --allow-purchases.",
Expand All @@ -439,11 +441,11 @@ where
}
}
Err(error) => return OperationResult::Failed(error),
Ok(_) => {}
Ok(_) => None,
};

match manager
.update(resource.get_inputs(), outputs, dependency_outputs)
.update(resource.get_inputs(), outputs, dependency_outputs, price)
.await
{
Ok(outputs) => OperationResult::SucceededUpdate(outputs),
Expand All @@ -468,7 +470,7 @@ where
logger::log("Inputs:");
logger::log_changeset(get_changeset("", &inputs_hash));

match manager
let price = match manager
.get_create_price(resource.get_inputs(), dependency_outputs.clone())
.await
{
Expand All @@ -478,7 +480,8 @@ where
logger::log(Paint::yellow(format!(
"{} Robux will be charged from your account.",
price
)))
)));
Some(price)
} else {
return OperationResult::Skipped(format!(
"Resource would cost {} Robux to create. Give Mantle permission to make purchases with --allow-purchases.",
Expand All @@ -487,11 +490,11 @@ where
}
}
Err(error) => return OperationResult::Failed(error),
Ok(_) => {}
Ok(_) => None,
};

match manager
.create(resource.get_inputs(), dependency_outputs)
.create(resource.get_inputs(), dependency_outputs, price)
.await
{
Ok(outputs) => OperationResult::SucceededCreate(outputs),
Expand Down
19 changes: 15 additions & 4 deletions src/lib/roblox_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1532,6 +1532,7 @@ impl RobloxApi {
description: String,
icon_file_path: PathBuf,
payment_source: CreatorType,
expected_cost: u32,
) -> Result<CreateBadgeResponse, String> {
let req = self
.client
Expand All @@ -1544,7 +1545,8 @@ impl RobloxApi {
.part("request.files", Self::get_file_part(icon_file_path).await?)
.text("request.name", name)
.text("request.description", description)
.text("request.paymentSourceType", payment_source.to_string()),
.text("request.paymentSourceType", payment_source.to_string())
.text("request.expectedCost", expected_cost.to_string()),
);

Self::handle_as_json(req).await
Expand All @@ -1571,6 +1573,15 @@ impl RobloxApi {
Ok(())
}

pub async fn get_create_badge_free_quota(&self, experience_id: AssetId) -> Result<i32, String> {
let req = self.client.get(&format!(
"https://badges.roblox.com/v1/universes/{}/free-badges-quota",
experience_id
));

Self::handle_as_json(req).await
}

pub async fn list_badges(
&self,
experience_id: AssetId,
Expand Down Expand Up @@ -1742,7 +1753,7 @@ impl RobloxApi {

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

let mut req = self
Expand Down Expand Up @@ -1777,7 +1788,7 @@ impl RobloxApi {

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

let req = self
Expand Down Expand Up @@ -1811,7 +1822,7 @@ impl RobloxApi {

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

let req = self
Expand Down
60 changes: 46 additions & 14 deletions src/lib/roblox_resource_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ use std::path::{Path, PathBuf};
use async_trait::async_trait;
use chrono::Utc;
use serde::{Deserialize, Serialize};
use yansi::Paint;

use crate::logger;

use super::{
resource_graph::{
Expand Down Expand Up @@ -302,10 +305,36 @@ impl ResourceManager<RobloxInputs, RobloxOutputs> for RobloxResourceManager {
async fn get_create_price(
&self,
inputs: RobloxInputs,
_dependency_outputs: Vec<RobloxOutputs>,
dependency_outputs: Vec<RobloxOutputs>,
) -> Result<Option<u32>, String> {
match inputs {
RobloxInputs::Badge(_) => Ok(Some(100)),
RobloxInputs::Badge(_) => {
let experience = single_output!(dependency_outputs, RobloxOutputs::Experience);
let free_quota = self
.roblox_api
.get_create_badge_free_quota(experience.asset_id)
.await?;
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),
));
Ok(None)
} else {
Ok(Some(100))
}
}
RobloxInputs::AudioAsset(inputs) => {
let GetCreateAudioAssetPriceResponse {
price, can_afford, ..
Expand All @@ -329,6 +358,7 @@ impl ResourceManager<RobloxInputs, RobloxOutputs> for RobloxResourceManager {
&self,
inputs: RobloxInputs,
dependency_outputs: Vec<RobloxOutputs>,
price: Option<u32>,
) -> Result<RobloxOutputs, String> {
match inputs {
RobloxInputs::Experience(inputs) => {
Expand Down Expand Up @@ -531,6 +561,7 @@ impl ResourceManager<RobloxInputs, RobloxOutputs> for RobloxResourceManager {
inputs.description,
self.get_path(inputs.icon_file_path),
self.payment_source.clone(),
price.unwrap_or(0),
)
.await?;

Expand Down Expand Up @@ -615,37 +646,38 @@ impl ResourceManager<RobloxInputs, RobloxOutputs> for RobloxResourceManager {
inputs: RobloxInputs,
outputs: RobloxOutputs,
dependency_outputs: Vec<RobloxOutputs>,
price: Option<u32>,
) -> Result<RobloxOutputs, String> {
match (inputs.clone(), outputs.clone()) {
(RobloxInputs::Experience(_), RobloxOutputs::Experience(_)) => {
self.delete(outputs, dependency_outputs.clone()).await?;
self.create(inputs, dependency_outputs).await
self.create(inputs, dependency_outputs, price).await
}
(RobloxInputs::ExperienceConfiguration(_), RobloxOutputs::ExperienceConfiguration) => {
self.create(inputs, dependency_outputs).await
self.create(inputs, dependency_outputs, price).await
}
(RobloxInputs::ExperienceActivation(_), RobloxOutputs::ExperienceActivation) => {
self.create(inputs, dependency_outputs).await
self.create(inputs, dependency_outputs, price).await
}
(RobloxInputs::ExperienceIcon(_), RobloxOutputs::ExperienceIcon(_)) => {
self.create(inputs, dependency_outputs).await
self.create(inputs, dependency_outputs, price).await
}
(RobloxInputs::ExperienceThumbnail(_), RobloxOutputs::ExperienceThumbnail(_)) => {
self.delete(outputs, dependency_outputs.clone()).await?;
self.create(inputs, dependency_outputs).await
self.create(inputs, dependency_outputs, price).await
}
(RobloxInputs::ExperienceThumbnailOrder, RobloxOutputs::ExperienceThumbnailOrder) => {
self.create(inputs, dependency_outputs).await
self.create(inputs, dependency_outputs, price).await
}
// TODO: is this correct?
(RobloxInputs::Place(_), RobloxOutputs::Place(_)) => {
self.create(inputs, dependency_outputs).await
self.create(inputs, dependency_outputs, price).await
}
(RobloxInputs::PlaceFile(_), RobloxOutputs::PlaceFile(_)) => {
self.create(inputs, dependency_outputs).await
self.create(inputs, dependency_outputs, price).await
}
(RobloxInputs::PlaceConfiguration(_), RobloxOutputs::PlaceConfiguration) => {
self.create(inputs, dependency_outputs).await
self.create(inputs, dependency_outputs, price).await
}
(RobloxInputs::SocialLink(inputs), RobloxOutputs::SocialLink(outputs)) => {
let experience = single_output!(dependency_outputs, RobloxOutputs::Experience);
Expand All @@ -663,7 +695,7 @@ impl ResourceManager<RobloxInputs, RobloxOutputs> for RobloxResourceManager {
Ok(RobloxOutputs::SocialLink(outputs))
}
(RobloxInputs::ProductIcon(_), RobloxOutputs::ProductIcon(_)) => {
self.create(inputs, dependency_outputs).await
self.create(inputs, dependency_outputs, price).await
}
(RobloxInputs::Product(inputs), RobloxOutputs::Product(outputs)) => {
let experience = single_output!(dependency_outputs, RobloxOutputs::Experience);
Expand Down Expand Up @@ -731,10 +763,10 @@ impl ResourceManager<RobloxInputs, RobloxOutputs> for RobloxResourceManager {
}))
}
(RobloxInputs::ImageAsset(_), RobloxOutputs::ImageAsset(_)) => {
self.create(inputs, dependency_outputs).await
self.create(inputs, dependency_outputs, price).await
}
(RobloxInputs::AudioAsset(_), RobloxOutputs::AudioAsset(_)) => {
self.create(inputs, dependency_outputs).await
self.create(inputs, dependency_outputs, price).await
}
(RobloxInputs::AssetAlias(inputs), RobloxOutputs::AssetAlias(outputs)) => {
let experience = single_output!(dependency_outputs, RobloxOutputs::Experience);
Expand Down
3 changes: 1 addition & 2 deletions src/lib/state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -435,8 +435,7 @@ fn get_desired_experience_graph(
let path = path.map_err(|e| format!("Glob pattern invalid: {}", e))?;
let name = path
.file_stem()
.map(OsStr::to_str)
.flatten()
.and_then(OsStr::to_str)
.ok_or(format!("Asset path is not a file: {}", path.display()))?
.to_owned();

Expand Down