From 4f77012a9a6c0b29597430b6c98827cc3504a16c Mon Sep 17 00:00:00 2001 From: Carter Penterman <carterpenterman@gmail.com> Date: Wed, 21 Aug 2024 22:48:30 -0500 Subject: [PATCH 1/4] Use new asset API --- commands/submit.js | 13 +++----- commands/take.js | 46 ++++++++++----------------- common.js | 79 +++++++++++++++++++++++++++++++--------------- 3 files changed, 74 insertions(+), 64 deletions(-) diff --git a/commands/submit.js b/commands/submit.js index ffd0080..a2d6468 100644 --- a/commands/submit.js +++ b/commands/submit.js @@ -4,7 +4,7 @@ const fs = require('node:fs'); const noblox = require("noblox.js"); const axios = require("axios").default; 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) { if (isNaN(id)) return undefined; @@ -50,12 +50,10 @@ async function execute(interaction) { const id = interaction.options.getInteger("asset_id", true); await noblox.setCookie(cookies[game]); - - try { // Check that the bot owns this model 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); return; } @@ -63,13 +61,13 @@ async function execute(interaction) { if (error.message !== "400 The specified Asset does not exist!") { throw error; } - await interaction.editReply(`🚫 This asset does not exist (id: ${id}).`); + await interaction.editReply(`🚫 This asset does not exist (\`${id}\`).`); return; } const assetInfo = await getAssetInfo(id); - if (assetInfo.typeId !== AssetType.Model) { - await interaction.editReply(`🚫 This asset (id: ${id}) is not a model. Your map must be a model.`); + if (!assetInfo.isModel) { + await interaction.editReply(`🚫 This asset (\`${id}\`) is not a model. Your map must be a model.`); return; } @@ -82,7 +80,6 @@ async function execute(interaction) { await interaction.editReply(msg); return; } - const csvFile = fs.readFileSync(fname); const lines = parse(csvFile, {delimiter: ',', fromLine: 2}); diff --git a/commands/take.js b/commands/take.js index 69ca127..2270eb9 100644 --- a/commands/take.js +++ b/commands/take.js @@ -1,7 +1,7 @@ const { SlashCommandBuilder } = require('@discordjs/builders'); const noblox = require("noblox.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 @@ -25,52 +25,34 @@ async function execute(interaction) { if (error.message !== "400 The specified Asset does not exist!") { throw error; } - await interaction.editReply(`🚫 This asset does not exist (id: ${id}).`); + await interaction.editReply(`🚫 This asset does not exist (id: \`${id}\`).`); return; } // Validate that this is a model const assetInfo = await getAssetInfo(id); - 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.`); + 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.`); return; } // 403 (Forbidden) means the asset isn't distributed - if (assetInfo.status === 403 || !assetInfo.forSale) { - 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.`); + if (assetInfo.status === 403 || (assetInfo.forSale === false)) { + 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; } - if (assetInfo.typeId !== AssetType.Model) { - await interaction.editReply(`🚫 This asset (id: ${id}) is not a model. Your map must be a model.`); + if (!assetInfo.isModel) { + await interaction.editReply(`🚫 This asset (id: \`${id}\` is not a model. Your map must be a model.`); return; } 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; } // Kick off the buy request let buyPromise; if (!alreadyOwned) { - const jar = noblox.options.jar; - 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" - } - }) - }); + buyPromise = buyModel(id); } // Validate and send the validation result @@ -85,8 +67,12 @@ async function execute(interaction) { } // Make sure the buy request is done - const res = await buyPromise; - console.log(JSON.parse(res.body)); + const success = await buyPromise; + 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( ` 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]}>). diff --git a/common.js b/common.js index ae240d0..3aba732 100644 --- a/common.js +++ b/common.js @@ -1,5 +1,6 @@ const axios = require("axios").default; const { RobloxFile } = require("rbxm-parser"); +const noblox = require("noblox.js"); // https://create.roblox.com/docs/reference/engine/enums/AssetType const AssetType = { @@ -25,44 +26,70 @@ const AssetType = { }; async function getAssetInfo(assetId) { - const res = await axios.get("https://apis.roblox.com/toolbox-service/v1/items/details", { - params: { - assetIds: assetId - }, - validateStatus: (_status) => true + const jar = noblox.options.jar; + const xcsrf = await noblox.getGeneralToken(jar); + + const res = await noblox.http(`https://apis.roblox.com/user/cloud/v2/creator-store-products/PRODUCT_NAMESPACE_CREATOR_MARKETPLACE_ASSET-PRODUCT_TYPE_MODEL-${assetId}`, { + 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 { - status: res.status + status: res.status, + isModel: false }; } - const data = res.data.data; - const assetInfo = data[0]; - let isFree, forSale; - if (assetInfo.fiatProduct) { - 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); + const assetInfo = JSON.parse(res.body); + const quantity = assetInfo.purchasePrice.quantity; + const isFree = quantity.significand === 0 && quantity.exponent === 0; + const forSale = assetInfo.published && assetInfo.purchasable; return { - status: res.status, - id: assetId, - name: assetInfo.asset.name, - typeId: assetInfo.asset.typeId, - creatorId: assetInfo.creator.id, + status: res.statusCode, + isModel: true, + id: assetInfo.modelAssetId, + creatorId: +assetInfo.userSeller, isFree: isFree, 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 SubmissionColumn = { @@ -321,4 +348,4 @@ function getValidationMessage(validation, game, errorOnFail) { return msg; } -module.exports = { AssetType, getAssetInfo, SubmissionColumn, SubmissionColumnsString, getSubmissionLine, createSubmissionLine, validateMapAsset, getValidationMessage, safeCsvFormat }; \ No newline at end of file +module.exports = { AssetType, getAssetInfo, buyModel, SubmissionColumn, SubmissionColumnsString, getSubmissionLine, createSubmissionLine, validateMapAsset, getValidationMessage, safeCsvFormat }; \ No newline at end of file From b3fdd8e2f1fbfbb63b32f9a5e9f3e542c185695f Mon Sep 17 00:00:00 2001 From: Carter Penterman <carterpenterman@gmail.com> Date: Mon, 26 Aug 2024 21:38:17 -0500 Subject: [PATCH 2/4] Improve error messages a bit for new API --- commands/submit.js | 10 +++++----- commands/take.js | 13 ++++++++----- common.js | 2 +- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/commands/submit.js b/commands/submit.js index a2d6468..f8a6351 100644 --- a/commands/submit.js +++ b/commands/submit.js @@ -53,7 +53,7 @@ async function execute(interaction) { try { // Check that the bot owns this model if (!(await noblox.getOwnership(await noblox.getCurrentUser("UserID"), id, "Asset"))) { - const msg = `🚫 The ${game} maptest bot's inventory does not contain this asset (\`${id}\`). You must use the /take command first.`; + const msg = `🚫 The ${game} maptest bot's inventory does not contain this asset (id: \`${id}\`). You must use the /take command first.`; await interaction.editReply(msg); return; } @@ -61,13 +61,13 @@ async function execute(interaction) { if (error.message !== "400 The specified Asset does not exist!") { throw error; } - await interaction.editReply(`🚫 This asset does not exist (\`${id}\`).`); + await interaction.editReply(`🚫 This asset does not exist (id: \`${id}\`).`); return; } const assetInfo = await getAssetInfo(id); if (!assetInfo.isModel) { - await interaction.editReply(`🚫 This asset (\`${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; } @@ -87,7 +87,7 @@ async function execute(interaction) { for (let lineStr of lines) { const line = getSubmissionLine(lineStr); if (id === line.modelId) { - await interaction.editReply(`🚫 This map (id: ${id}) was already submitted on <t:${line.timestamp}:d>.`); + await interaction.editReply(`🚫 This map (id: \`${id}\`) was already submitted on <t:${line.timestamp}:d>.`); return; } } @@ -112,7 +112,7 @@ async function execute(interaction) { csvString += createSubmissionLine(id, unixTimestamp, userId, await robloxUsernameFromId(userId), validation.displayName, validation.creator); fs.writeFileSync(fname, csvString); - await interaction.followUp(`Map (id: ${id}) successfully submitted.`); + await interaction.followUp(`Map (id: \`${id}\`) successfully submitted.`); } module.exports = { diff --git a/commands/take.js b/commands/take.js index 2270eb9..389bd4e 100644 --- a/commands/take.js +++ b/commands/take.js @@ -31,17 +31,20 @@ async function execute(interaction) { // Validate that this is a model const assetInfo = await getAssetInfo(id); - 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.`); + if (assetInfo.status === 404) { + 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). Alternatively, the provided ID may not actually be a model.`); return; } - // 403 (Forbidden) means the asset isn't distributed - if (assetInfo.status === 403 || (assetInfo.forSale === false)) { + if (assetInfo.status < 200 || assetInfo.status >= 300) { + await interaction.editReply(`🚫 Something unexpected went wrong trying to retrive info about the model (id: \`${id}\`).`); + return; + } + if (assetInfo.forSale === false) { 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; } 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; } if (!assetInfo.isFree) { diff --git a/common.js b/common.js index 3aba732..856e0c4 100644 --- a/common.js +++ b/common.js @@ -41,7 +41,7 @@ async function getAssetInfo(assetId) { if (res.statusCode < 200 || res.statusCode >= 300) { return { - status: res.status, + status: res.statusCode, isModel: false }; } From d1066e27372b183661211498de65254afd9f9e4e Mon Sep 17 00:00:00 2001 From: Carter Penterman <carterpenterman@gmail.com> Date: Tue, 27 Aug 2024 21:28:33 -0500 Subject: [PATCH 3/4] Use alternate API for current user and skip cookie validation --- commands/submit.js | 6 +++--- commands/take.js | 6 +++--- common.js | 20 +++++++++++++++++++- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/commands/submit.js b/commands/submit.js index f8a6351..0319970 100644 --- a/commands/submit.js +++ b/commands/submit.js @@ -4,7 +4,7 @@ const fs = require('node:fs'); const noblox = require("noblox.js"); const axios = require("axios").default; const { submissions, commands, cookies } = require("../config/config.js"); -const { getAssetInfo, SubmissionColumnsString, createSubmissionLine, getSubmissionLine, validateMapAsset, getValidationMessage } = require("../common.js"); +const { getAssetInfo, getCurrentUser, SubmissionColumnsString, createSubmissionLine, getSubmissionLine, validateMapAsset, getValidationMessage } = require("../common.js"); async function robloxUserFromDiscord(id) { if (isNaN(id)) return undefined; @@ -48,11 +48,11 @@ async function execute(interaction) { } const id = interaction.options.getInteger("asset_id", true); - await noblox.setCookie(cookies[game]); + noblox.setCookie(cookies[game], false); try { // Check that the bot owns this model - if (!(await noblox.getOwnership(await noblox.getCurrentUser("UserID"), id, "Asset"))) { + if (!(await noblox.getOwnership(await getCurrentUser(), id, "Asset"))) { const msg = `🚫 The ${game} maptest bot's inventory does not contain this asset (id: \`${id}\`). You must use the /take command first.`; await interaction.editReply(msg); return; diff --git a/commands/take.js b/commands/take.js index 389bd4e..db322d7 100644 --- a/commands/take.js +++ b/commands/take.js @@ -1,7 +1,7 @@ const { SlashCommandBuilder } = require('@discordjs/builders'); const noblox = require("noblox.js"); const { cookies, commands, gamePlaces } = require("../config/config.js"); -const { getAssetInfo, buyModel, validateMapAsset, getValidationMessage } = require("../common.js"); +const { getAssetInfo, buyModel, getCurrentUser, validateMapAsset, getValidationMessage } = require("../common.js"); /** * @param {import('discord.js').ChatInputCommandInteraction} interaction @@ -15,12 +15,12 @@ async function execute(interaction) { } const id = interaction.options.getInteger("asset_id", true); - await noblox.setCookie(cookie); + noblox.setCookie(cookie, false); let alreadyOwned; try { // Check if the bot already owns this asset - alreadyOwned = await noblox.getOwnership(await noblox.getCurrentUser("UserID"), id, "Asset"); + alreadyOwned = await noblox.getOwnership(await getCurrentUser(), id, "Asset"); } catch (error) { if (error.message !== "400 The specified Asset does not exist!") { throw error; diff --git a/common.js b/common.js index 856e0c4..48389e1 100644 --- a/common.js +++ b/common.js @@ -90,6 +90,24 @@ async function buyModel(modelId) { return res.statusCode >= 200 && res.statusCode < 300 && resJson.purchaseTransactionStatus === "PURCHASE_TRANSACTION_STATUS_SUCCESS"; } +async function getCurrentUser() { + const jar = noblox.options.jar; + const xcsrf = await noblox.getGeneralToken(jar); + + const res = await noblox.http("https://users.roblox.com/v1/users/authenticated", { + method: "GET", + resolveWithFullResponse: true, + jar: jar, + headers: { + "X-CSRF-TOKEN": xcsrf, + "Content-Type": "application/json" + } + }); + + const resJson = JSON.parse(res.body); + return resJson.id; +} + const SubmissionColumnsString = "map_id,unix_timestamp,user_id,username,display_name,creator\n"; const SubmissionColumn = { @@ -348,4 +366,4 @@ function getValidationMessage(validation, game, errorOnFail) { return msg; } -module.exports = { AssetType, getAssetInfo, buyModel, SubmissionColumn, SubmissionColumnsString, getSubmissionLine, createSubmissionLine, validateMapAsset, getValidationMessage, safeCsvFormat }; \ No newline at end of file +module.exports = { AssetType, getAssetInfo, buyModel, getCurrentUser, SubmissionColumn, SubmissionColumnsString, getSubmissionLine, createSubmissionLine, validateMapAsset, getValidationMessage, safeCsvFormat }; \ No newline at end of file From 37e691eebf355eaeccc8292d06ba223fefda8566 Mon Sep 17 00:00:00 2001 From: Carter Penterman <carterpenterman@gmail.com> Date: Tue, 27 Aug 2024 23:11:28 -0500 Subject: [PATCH 4/4] Update Noblox to 6.0.2 --- commands/submit.js | 5 +++-- commands/take.js | 5 +++-- common.js | 20 +------------------- package.json | 2 +- 4 files changed, 8 insertions(+), 24 deletions(-) diff --git a/commands/submit.js b/commands/submit.js index 0319970..9b31046 100644 --- a/commands/submit.js +++ b/commands/submit.js @@ -4,7 +4,7 @@ const fs = require('node:fs'); const noblox = require("noblox.js"); const axios = require("axios").default; const { submissions, commands, cookies } = require("../config/config.js"); -const { getAssetInfo, getCurrentUser, SubmissionColumnsString, createSubmissionLine, getSubmissionLine, validateMapAsset, getValidationMessage } = require("../common.js"); +const { getAssetInfo, SubmissionColumnsString, createSubmissionLine, getSubmissionLine, validateMapAsset, getValidationMessage } = require("../common.js"); async function robloxUserFromDiscord(id) { if (isNaN(id)) return undefined; @@ -52,7 +52,8 @@ async function execute(interaction) { try { // Check that the bot owns this model - if (!(await noblox.getOwnership(await getCurrentUser(), id, "Asset"))) { + const robloxUser = await noblox.getAuthenticatedUser(); + if (!(await noblox.getOwnership(robloxUser.id, id, "Asset"))) { const msg = `🚫 The ${game} maptest bot's inventory does not contain this asset (id: \`${id}\`). You must use the /take command first.`; await interaction.editReply(msg); return; diff --git a/commands/take.js b/commands/take.js index db322d7..5826d4a 100644 --- a/commands/take.js +++ b/commands/take.js @@ -1,7 +1,7 @@ const { SlashCommandBuilder } = require('@discordjs/builders'); const noblox = require("noblox.js"); const { cookies, commands, gamePlaces } = require("../config/config.js"); -const { getAssetInfo, buyModel, getCurrentUser, validateMapAsset, getValidationMessage } = require("../common.js"); +const { getAssetInfo, buyModel, validateMapAsset, getValidationMessage } = require("../common.js"); /** * @param {import('discord.js').ChatInputCommandInteraction} interaction @@ -20,7 +20,8 @@ async function execute(interaction) { let alreadyOwned; try { // Check if the bot already owns this asset - alreadyOwned = await noblox.getOwnership(await getCurrentUser(), id, "Asset"); + const robloxUser = await noblox.getAuthenticatedUser(); + alreadyOwned = await noblox.getOwnership(robloxUser.id, id, "Asset"); } catch (error) { if (error.message !== "400 The specified Asset does not exist!") { throw error; diff --git a/common.js b/common.js index 48389e1..856e0c4 100644 --- a/common.js +++ b/common.js @@ -90,24 +90,6 @@ async function buyModel(modelId) { return res.statusCode >= 200 && res.statusCode < 300 && resJson.purchaseTransactionStatus === "PURCHASE_TRANSACTION_STATUS_SUCCESS"; } -async function getCurrentUser() { - const jar = noblox.options.jar; - const xcsrf = await noblox.getGeneralToken(jar); - - const res = await noblox.http("https://users.roblox.com/v1/users/authenticated", { - method: "GET", - resolveWithFullResponse: true, - jar: jar, - headers: { - "X-CSRF-TOKEN": xcsrf, - "Content-Type": "application/json" - } - }); - - const resJson = JSON.parse(res.body); - return resJson.id; -} - const SubmissionColumnsString = "map_id,unix_timestamp,user_id,username,display_name,creator\n"; const SubmissionColumn = { @@ -366,4 +348,4 @@ function getValidationMessage(validation, game, errorOnFail) { return msg; } -module.exports = { AssetType, getAssetInfo, buyModel, getCurrentUser, SubmissionColumn, SubmissionColumnsString, getSubmissionLine, createSubmissionLine, validateMapAsset, getValidationMessage, safeCsvFormat }; \ No newline at end of file +module.exports = { AssetType, getAssetInfo, buyModel, SubmissionColumn, SubmissionColumnsString, getSubmissionLine, createSubmissionLine, validateMapAsset, getValidationMessage, safeCsvFormat }; \ No newline at end of file diff --git a/package.json b/package.json index 1f9e676..d6e01ca 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "csv-parse": "^5.0.4", "discord-api-types": "^0.37.81", "discord.js": "^14.14.1", - "noblox.js": "^4.15.1", + "noblox.js": "^6.0.2", "node-csv": "^0.1.2", "rbxm-parser": "^1.1.0", "sugar-date": "^2.0.6"