Use API that isn't rate-limited and some extras #7

Merged
Quaternions merged 2 commits from use-different-asset-api into master 2024-04-16 10:50:48 +00:00
3 changed files with 103 additions and 54 deletions

View File

@ -4,6 +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 } = require("../common.js");
async function robloxUserFromDiscord(id) { async function robloxUserFromDiscord(id) {
if (isNaN(id)) return undefined; if (isNaN(id)) return undefined;
@ -28,7 +29,7 @@ async function robloxUsernameFromId(id) {
async function execute(interaction) { async function execute(interaction) {
const userId = await robloxUserFromDiscord(interaction.user.id); const userId = await robloxUserFromDiscord(interaction.user.id);
if (!userId) { if (!userId) {
const msg = "You don't have a Roblox account linked with your Discord account. Use !link with rbhop dog to link your account."; const msg = "You don't have a Roblox account linked with your Discord account. Use !link with the rbhop dog bot to link your account.";
await interaction.reply({content: msg, ephemeral: true}); await interaction.reply({content: msg, ephemeral: true});
return; return;
} }
@ -48,33 +49,25 @@ async function execute(interaction) {
await noblox.setCookie(cookies[game]); await noblox.setCookie(cookies[game]);
// 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: ${id}). You must use the /take command first.`;
await interaction.reply({content: msg, ephemeral: true}); await interaction.reply({content: msg, ephemeral: true});
return; return;
} }
try { const assetInfo = await getAssetInfo(id);
const info = await noblox.getProductInfo(id); if (assetInfo.creatorId !== userId) {
if (info.AssetTypeId != 10) { const assetUsernamePromise = robloxUsernameFromId(assetInfo.creatorId);
await interaction.reply({content: `(id: ${id}) is not a valid model ID.`, ephemeral: true}); const interactionUsernamePromise = robloxUsernameFromId(userId);
return; const assetUsername = await assetUsernamePromise;
} const interactionUsername = await interactionUsernamePromise;
if (info.Creator.Id != userId) {
const assetUsername = await robloxUsernameFromId(info.Creator.Id);
const interactionUsername = await robloxUsernameFromId(userId);
const msg = `The account linked to your Discord (${interactionUsername}) is not the owner of this model (${assetUsername}), so you cannot submit it.`; const msg = `The account linked to your Discord (${interactionUsername}) is not the owner of this model (${assetUsername}), so you cannot submit it.`;
await interaction.reply({content: msg, ephemeral: true}); await interaction.reply({content: msg, ephemeral: true});
return; return;
} }
} catch (error) { // Shouldn't really be possible but who knows...
// Roblox only lets you call the product info API like once per 30 seconds for some reason... if (assetInfo.typeId !== AssetType.Model) {
if (error.message.startsWith("429")) { await interaction.reply({content: `This asset (id: ${id}) is not a model. Your map must be a model.`, ephemeral: true});
await interaction.reply({content: `The maptest bot is being rate-limited by Roblox, please wait a minute before doing this command again.`, ephemeral: true});
return;
}
console.log(error);
await interaction.reply({content: `There is a problem with this asset ID (id: ${id}).`, ephemeral: true});
return; return;
} }
@ -85,8 +78,8 @@ async function execute(interaction) {
for (let record of records) { for (let record of records) {
const rid = record[0]; const rid = record[0];
const rtimestamp = record[1]; const rtimestamp = record[1];
if (id == rid) { if (id === rid) {
await interaction.reply({content: `Tried to submit map (id: ${id}) that already exists!`, ephemeral: true}); await interaction.reply({content: `This map (id: ${id}) was already submitted on <t:${rtimestamp}:d>.`, ephemeral: true});
return; return;
} }
s += `${rid},${rtimestamp}\n`; s += `${rid},${rtimestamp}\n`;

View File

@ -1,6 +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 } = require("../config/config.js"); const { cookies, commands } = require("../config/config.js");
const { AssetType, getAssetInfo } = require("../common.js");
async function execute(interaction) { async function execute(interaction) {
const game = interaction.options.getString("game"); const game = interaction.options.getString("game");
@ -18,27 +19,36 @@ async function execute(interaction) {
await interaction.reply({content: msg, ephemeral: true}); await interaction.reply({content: msg, ephemeral: true});
return; return;
} }
let info;
// Validate that this is a model // Validate that this is a model
try { const assetInfo = await getAssetInfo(id);
info = await noblox.getProductInfo(id); if (assetInfo.status === 404) {
if (info.AssetTypeId != 10) { await interaction.reply({content: `This asset may not exist or is not a model (id: ${id}). Your map must be a model.`, ephemeral: true});
await interaction.reply({content: `(id: ${id}) is not a valid model ID.`, ephemeral: true});
return; return;
} }
} catch (error) { // 403 (Forbidden) means the asset isn't distributed
// Roblox only lets you call the product info API like once per 30 seconds for some reason... if (assetInfo.status === 403 || !assetInfo.forSale) {
if (error.message.startsWith("429")) { await interaction.reply({content: `This model (id: ${id}) is off sale. Please configure it to be on sale (Configure -> Distribute on Creator Store).`, ephemeral: true});
await interaction.reply({content: `The maptest bot is being rate-limited by Roblox, please wait a minute before doing this command again.`, ephemeral: true});
return; return;
} }
console.log(error); if (assetInfo.typeId !== AssetType.Model) {
await interaction.reply({content: `There is a problem with this asset ID (id: ${id}).`, ephemeral: true}); await interaction.reply({content: `This asset (id: ${id}) is not a model. Your map must be a model.`, ephemeral: true});
return;
}
if (assetInfo.price !== 0) {
await interaction.reply({content: `This model (id: ${id}) is not free. Please change the price to be free.`, ephemeral: true});
return; return;
} }
try { // Make a "fake" product info object for the Noblox buy method
await noblox.buy({product: info, price: 0}); const productInfo = {
PriceInRobux: assetInfo.price,
ProductId: assetInfo.productId,
Creator: {
Id: assetInfo.creatorId
}
}
await noblox.buy({product: productInfo, price: 0});
await interaction.reply( await interaction.reply(
` `
Now that your map (id: ${id}) has been taken by the ${game} maptest bot you can load it into the ${game} maptest place. To load your map, join the game and say Now that your map (id: ${id}) has been taken by the ${game} maptest bot you can load it into the ${game} maptest place. To load your map, join the game and say
@ -48,15 +58,6 @@ Now that your map (id: ${id}) has been taken by the ${game} maptest bot you can
If it did not load successfully, you can expand the chat to view the full error message by clicking and dragging on the edge of the chat. If it did not load successfully, you can expand the chat to view the full error message by clicking and dragging on the edge of the chat.
` `
); );
} catch (error) {
if (error.message == "You already own this item.") {
await interaction.reply({content: "The bot has already taken this model!", ephemeral: true});
} else {
await interaction.reply({content: `An error occured trying to take the model (id: ${id}). Make sure it is uncopylocked!`, ephemeral: true});
console.log(`Could not take asset ID ${id}: `);
console.log(error);
}
}
} }
module.exports = { module.exports = {

55
common.js Normal file
View File

@ -0,0 +1,55 @@
const axios = require("axios").default;
// https://create.roblox.com/docs/reference/engine/enums/AssetType
const AssetType = {
Image: 1,
TShirt: 2,
Audio: 3,
Mesh: 4,
Lua: 5,
Hat: 8,
Place: 9,
Model: 10,
Shirt: 11,
Pants: 12,
Decal: 13,
Head: 17,
Face: 18,
Gear: 19,
Badge: 21,
Animation: 24,
GamePass: 34,
Plugin: 38,
MeshPart: 40
};
async function getAssetInfo(assetId) {
const res = await axios.get("https://apis.roblox.com/toolbox-service/v1/items/details", {
params: {
assetIds: assetId
},
validateStatus: (status) => status === 403 || status === 404 || (status >= 200 && status < 300) // Allow 403/404 as a valid status (don't throw an error)
});
if (res.status < 200 || res.status > 300) {
return {
status: res.status
}
}
const data = res.data.data;
const assetInfo = data[0];
return {
status: res.status,
id: assetId,
name: assetInfo.asset.name,
typeId: assetInfo.asset.typeId,
creatorId: assetInfo.creator.id,
price: assetInfo.product.price,
productId: assetInfo.product.productId,
forSale: assetInfo.product.isForSaleOrIsPublicDomain
}
}
module.exports = { AssetType, getAssetInfo };