Use new product/asset API #22

Merged
itzaname merged 2 commits from update-product-api into master 2024-08-22 04:00:15 +00:00
3 changed files with 74 additions and 64 deletions
Showing only changes of commit 4f77012a9a - Show all commits

View File

@ -4,7 +4,7 @@ const fs = require('node:fs');
const noblox = require("noblox.js"); const noblox = require("noblox.js");
const axios = require("axios").default; const axios = require("axios").default;
const { submissions, commands, cookies } = require("../config/config.js"); const { submissions, commands, cookies } = require("../config/config.js");
const { AssetType, getAssetInfo, SubmissionColumnsString, createSubmissionLine, getSubmissionLine, validateMapAsset, getValidationMessage } = require("../common.js"); const { getAssetInfo, SubmissionColumnsString, createSubmissionLine, getSubmissionLine, validateMapAsset, getValidationMessage } = require("../common.js");
async function robloxUserFromDiscord(id) { async function robloxUserFromDiscord(id) {
if (isNaN(id)) return undefined; if (isNaN(id)) return undefined;
@ -50,12 +50,10 @@ async function execute(interaction) {
const id = interaction.options.getInteger("asset_id", true); const id = interaction.options.getInteger("asset_id", true);
await noblox.setCookie(cookies[game]); await noblox.setCookie(cookies[game]);
try { try {
// Check that the bot owns this model // Check that the bot owns this model
if (!(await noblox.getOwnership(await noblox.getCurrentUser("UserID"), id, "Asset"))) { if (!(await noblox.getOwnership(await noblox.getCurrentUser("UserID"), id, "Asset"))) {
const msg = `🚫 The ${game} maptest bot's inventory does not contain this asset (id: ${id}). You must use the /take command first.`; const msg = `🚫 The ${game} maptest bot's inventory does not contain this asset (\`${id}\`). You must use the /take command first.`;
await interaction.editReply(msg); await interaction.editReply(msg);
return; return;
} }
@ -63,13 +61,13 @@ async function execute(interaction) {
if (error.message !== "400 The specified Asset does not exist!") { if (error.message !== "400 The specified Asset does not exist!") {
throw error; throw error;
} }
await interaction.editReply(`🚫 This asset does not exist (id: ${id}).`); await interaction.editReply(`🚫 This asset does not exist (\`${id}\`).`);
return; return;
} }
const assetInfo = await getAssetInfo(id); const assetInfo = await getAssetInfo(id);
if (assetInfo.typeId !== AssetType.Model) { if (!assetInfo.isModel) {
await interaction.editReply(`🚫 This asset (id: ${id}) is not a model. Your map must be a model.`); await interaction.editReply(`🚫 This asset (\`${id}\`) is not a model. Your map must be a model.`);
return; return;
} }
@ -83,7 +81,6 @@ async function execute(interaction) {
return; return;
} }
const csvFile = fs.readFileSync(fname); const csvFile = fs.readFileSync(fname);
const lines = parse(csvFile, {delimiter: ',', fromLine: 2}); const lines = parse(csvFile, {delimiter: ',', fromLine: 2});

View File

@ -1,7 +1,7 @@
const { SlashCommandBuilder } = require('@discordjs/builders'); const { SlashCommandBuilder } = require('@discordjs/builders');
const noblox = require("noblox.js"); const noblox = require("noblox.js");
const { cookies, commands, gamePlaces } = require("../config/config.js"); const { cookies, commands, gamePlaces } = require("../config/config.js");
const { AssetType, getAssetInfo, validateMapAsset, getValidationMessage } = require("../common.js"); const { getAssetInfo, buyModel, validateMapAsset, getValidationMessage } = require("../common.js");
/** /**
* @param {import('discord.js').ChatInputCommandInteraction} interaction * @param {import('discord.js').ChatInputCommandInteraction} interaction
@ -25,52 +25,34 @@ async function execute(interaction) {
if (error.message !== "400 The specified Asset does not exist!") { if (error.message !== "400 The specified Asset does not exist!") {
throw error; throw error;
} }
await interaction.editReply(`🚫 This asset does not exist (id: ${id}).`); await interaction.editReply(`🚫 This asset does not exist (id: \`${id}\`).`);
return; return;
} }
// Validate that this is a model // Validate that this is a model
const assetInfo = await getAssetInfo(id); const assetInfo = await getAssetInfo(id);
if (assetInfo.status !== 403 && (assetInfo.status < 200 || assetInfo.status > 300)) { if (assetInfo.status !== 403 && (assetInfo.status < 200 || assetInfo.status >= 300)) {
await interaction.editReply(`🚫 This asset may not exist or is not a model (id: ${id}). Your map must be a model.`); await interaction.editReply(`🚫 This asset may not exist or is not a model (id: \`${id}\`). Your map must be a model.`);
return; return;
} }
// 403 (Forbidden) means the asset isn't distributed // 403 (Forbidden) means the asset isn't distributed
if (assetInfo.status === 403 || !assetInfo.forSale) { if (assetInfo.status === 403 || (assetInfo.forSale === false)) {
await interaction.editReply(`🚫 This model (id: ${id}) is off sale. Please configure it to be on sale (Configure -> Distribute on Creator Store). It is also possible that this is not a valid model.`); await interaction.editReply(`🚫 This [model](<https://create.roblox.com/store/asset/${id}/>) (id: \`${id}\`) is off sale. Please configure it to be on sale (Configure -> Distribute on Creator Store).`);
return; return;
} }
if (assetInfo.typeId !== AssetType.Model) { if (!assetInfo.isModel) {
await interaction.editReply(`🚫 This asset (id: ${id}) is not a model. Your map must be a model.`); await interaction.editReply(`🚫 This asset (id: \`${id}\` is not a model. Your map must be a model.`);
return; return;
} }
if (!assetInfo.isFree) { if (!assetInfo.isFree) {
await interaction.editReply(`🚫 This model (id: ${id}) is not free. Please change the price to be free.`); await interaction.editReply(`🚫 This [model](<https://create.roblox.com/store/asset/${id}/>) (id: \`${id}\`) is not free. Please change the price to be free.`);
return; return;
} }
// Kick off the buy request // Kick off the buy request
let buyPromise; let buyPromise;
if (!alreadyOwned) { if (!alreadyOwned) {
const jar = noblox.options.jar; buyPromise = buyModel(id);
const xcsrf = await noblox.getGeneralToken(jar);
buyPromise = noblox.http("https://apis.roblox.com/marketplace-fiat-service/v1/product/purchase", {
method: "POST",
resolveWithFullResponse: true,
jar,
headers: {
"X-CSRF-TOKEN": xcsrf,
"Content-Type": "application/json"
},
body: JSON.stringify({
expectedPrice: {currencyCode: "USD", quantity: {significand: 0, exponent: 0}},
productKey: {
productNamespace: "PRODUCT_NAMESPACE_CREATOR_MARKETPLACE_ASSET",
productTargetId: `${id}`,
productType: "PRODUCT_TYPE_MODEL"
}
})
});
} }
// Validate and send the validation result // Validate and send the validation result
@ -85,8 +67,12 @@ async function execute(interaction) {
} }
// Make sure the buy request is done // Make sure the buy request is done
const res = await buyPromise; const success = await buyPromise;
console.log(JSON.parse(res.body)); if (!success) {
await interaction.followUp(`🚫 Something went wrong when trying to buy the [model](<https://create.roblox.com/store/asset/${id}/>) (id: \`${id}\`).`);
return;
}
await interaction.followUp( await interaction.followUp(
` `
Now that your [map (id: ${id})](<https://create.roblox.com/store/asset/${id}/>) has been taken by the bot you can load it into the [${game} maptest place](<${gamePlaces[game]}>). Now that your [map (id: ${id})](<https://create.roblox.com/store/asset/${id}/>) has been taken by the bot you can load it into the [${game} maptest place](<${gamePlaces[game]}>).

View File

@ -1,5 +1,6 @@
const axios = require("axios").default; const axios = require("axios").default;
const { RobloxFile } = require("rbxm-parser"); const { RobloxFile } = require("rbxm-parser");
const noblox = require("noblox.js");
// https://create.roblox.com/docs/reference/engine/enums/AssetType // https://create.roblox.com/docs/reference/engine/enums/AssetType
const AssetType = { const AssetType = {
@ -25,44 +26,70 @@ const AssetType = {
}; };
async function getAssetInfo(assetId) { async function getAssetInfo(assetId) {
const res = await axios.get("https://apis.roblox.com/toolbox-service/v1/items/details", { const jar = noblox.options.jar;
params: { const xcsrf = await noblox.getGeneralToken(jar);
assetIds: assetId
}, const res = await noblox.http(`https://apis.roblox.com/user/cloud/v2/creator-store-products/PRODUCT_NAMESPACE_CREATOR_MARKETPLACE_ASSET-PRODUCT_TYPE_MODEL-${assetId}`, {
validateStatus: (_status) => true method: "GET",
resolveWithFullResponse: true,
jar: jar,
headers: {
"X-CSRF-TOKEN": xcsrf,
"Content-Type": "application/json"
}
}); });
if (res.status < 200 || res.status > 300) { if (res.statusCode < 200 || res.statusCode >= 300) {
return { return {
status: res.status status: res.status,
isModel: false
}; };
} }
const data = res.data.data; const assetInfo = JSON.parse(res.body);
const assetInfo = data[0]; const quantity = assetInfo.purchasePrice.quantity;
let isFree, forSale; const isFree = quantity.significand === 0 && quantity.exponent === 0;
if (assetInfo.fiatProduct) { const forSale = assetInfo.published && assetInfo.purchasable;
const quantity = assetInfo.fiatProduct.purchasePrice.quantity;
isFree = quantity.significand === 0 && quantity.exponent === 0;
forSale = assetInfo.fiatProduct.published && assetInfo.fiatProduct.purchasable;
}
else {
isFree = assetInfo.product.price === 0;
forSale = assetInfo.product.isForSaleOrIsPublicDomain;
}
console.log(assetInfo);
return { return {
status: res.status, status: res.statusCode,
id: assetId, isModel: true,
name: assetInfo.asset.name, id: assetInfo.modelAssetId,
typeId: assetInfo.asset.typeId, creatorId: +assetInfo.userSeller,
creatorId: assetInfo.creator.id,
isFree: isFree, isFree: isFree,
forSale: forSale forSale: forSale
}; };
} }
async function buyModel(modelId) {
const reqJson = {
expectedPrice: {currencyCode: "USD", quantity: {significand: 0, exponent: 0}},
productKey: {
productNamespace: "PRODUCT_NAMESPACE_CREATOR_MARKETPLACE_ASSET",
productTargetId: `${modelId}`,
productType: "PRODUCT_TYPE_MODEL"
}
};
const jar = noblox.options.jar;
const xcsrf = await noblox.getGeneralToken(jar);
const res = await noblox.http("https://apis.roblox.com/marketplace-fiat-service/v1/product/purchase", {
method: "POST",
resolveWithFullResponse: true,
jar: jar,
headers: {
"X-CSRF-TOKEN": xcsrf,
"Content-Type": "application/json"
},
body: JSON.stringify(reqJson)
});
const resJson = JSON.parse(res.body);
// Return true if purchased, false otherwise
return res.statusCode >= 200 && res.statusCode < 300 && resJson.purchaseTransactionStatus === "PURCHASE_TRANSACTION_STATUS_SUCCESS";
}
const SubmissionColumnsString = "map_id,unix_timestamp,user_id,username,display_name,creator\n"; const SubmissionColumnsString = "map_id,unix_timestamp,user_id,username,display_name,creator\n";
const SubmissionColumn = { const SubmissionColumn = {
@ -321,4 +348,4 @@ function getValidationMessage(validation, game, errorOnFail) {
return msg; return msg;
} }
module.exports = { AssetType, getAssetInfo, SubmissionColumn, SubmissionColumnsString, getSubmissionLine, createSubmissionLine, validateMapAsset, getValidationMessage, safeCsvFormat }; module.exports = { AssetType, getAssetInfo, buyModel, SubmissionColumn, SubmissionColumnsString, getSubmissionLine, createSubmissionLine, validateMapAsset, getValidationMessage, safeCsvFormat };