Bot checks inventory when taking/submitting

This commit is contained in:
Carter Penterman 2024-04-15 03:13:32 -05:00
parent 10d93041fc
commit e8c41bb821
3 changed files with 90 additions and 151 deletions

View File

@ -3,7 +3,7 @@ const { parse } = require("csv-parse/sync");
const fs = require('node:fs'); 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 } = require("../config/config.js"); const { submissions, commands, cookies } = require("../config/config.js");
async function robloxUserFromDiscord(id) { async function robloxUserFromDiscord(id) {
if (isNaN(id)) return undefined; if (isNaN(id)) return undefined;
@ -45,8 +45,17 @@ async function execute(interaction) {
} }
const id = interaction.options.getInteger("asset_id"); const id = interaction.options.getInteger("asset_id");
await noblox.setCookie(cookies[game]);
// 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.`;
await interaction.reply({content: msg, ephemeral: true});
return;
}
try { try {
const info = await getProductInfo(id); const info = await noblox.getProductInfo(id);
if (info.AssetTypeId != 10) { if (info.AssetTypeId != 10) {
await interaction.reply({content: `(id: ${id}) is not a valid model ID.`, ephemeral: true}); await interaction.reply({content: `(id: ${id}) is not a valid model ID.`, ephemeral: true});
return; return;
@ -54,11 +63,16 @@ async function execute(interaction) {
if (info.Creator.Id != userId) { if (info.Creator.Id != userId) {
const assetUsername = await robloxUsernameFromId(info.Creator.Id); const assetUsername = await robloxUsernameFromId(info.Creator.Id);
const interactionUsername = await robloxUsernameFromId(userId); 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) { } catch (error) {
// Roblox only lets you call the product info API like once per 30 seconds for some reason...
if (error.message.startsWith("429")) {
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); console.log(error);
await interaction.reply({content: `There is a problem with this asset ID (id: ${id}).`, ephemeral: true}); await interaction.reply({content: `There is a problem with this asset ID (id: ${id}).`, ephemeral: true});
return; return;
@ -84,7 +98,6 @@ async function execute(interaction) {
fs.writeFileSync(fname, s); fs.writeFileSync(fname, s);
await interaction.reply(`Map (id: ${id}) successfully submitted.`); await interaction.reply(`Map (id: ${id}) successfully submitted.`);
} }
module.exports = { module.exports = {
@ -102,37 +115,4 @@ module.exports = {
.setRequired(true)) .setRequired(true))
, ,
execute execute
}; };
function getProductInfo (asset) {
return new Promise(async (resolve, reject) => {
const httpOpt = {
url: `//economy.roblox.com/v2/assets/${asset}/details`,
options: {
resolveWithFullResponse: true,
method: 'GET'
}
}
try {
const res = await noblox.http(httpOpt);
if (res.statusCode === 200) {
resolve(JSON.parse(res.body));
} else {
// Sourced from: https://stackoverflow.com/a/32278428
const isAnObject = (val_1) => !!(val_1 instanceof Array || val_1 instanceof Object);
const body = isAnObject(res.body) ? JSON.parse(res.body) : {};
if (body.errors && body.errors.length > 0) {
const errors = body.errors.map((e) => {
return e.message;
});
reject(new Error(`${res.statusCode} ${errors.join(', ')}`));
} else {
reject(new Error(`${res.statusCode} ${res.body}`));
}
}
} catch (error) {
return reject(error);
}
})
}

View File

@ -9,24 +9,37 @@ async function execute(interaction) {
await interaction.reply({content: "Invalid game specified!", ephemeral: true}); await interaction.reply({content: "Invalid game specified!", ephemeral: true});
return; return;
} }
const id = interaction.options.getInteger("asset_id"); const id = interaction.options.getInteger("asset_id");
await noblox.setCookie(cookie).then(async () => { await noblox.setCookie(cookie);
let info; // Check that the bot doesn't already own this asset
// validate that this is a model if (await noblox.getOwnership(await noblox.getCurrentUser("UserID"), id, "Asset")) {
try { const msg = `The ${game} maptest bot already has this model (id: ${id})`;
info = await getProductInfo(id); await interaction.reply({content: msg, ephemeral: true});
if (info.AssetTypeId != 10) { return;
await interaction.reply({content: `(id: ${id}) is not a valid model ID.`, ephemeral: true}); }
return; let info;
} // Validate that this is a model
} catch (error) { try {
console.log(error); info = await noblox.getProductInfo(id);
await interaction.reply({content: `There is a problem with this asset ID (id: ${id}).`, ephemeral: true}); if (info.AssetTypeId != 10) {
await interaction.reply({content: `(id: ${id}) is not a valid model ID.`, ephemeral: true});
return; return;
} }
} catch (error) {
// Roblox only lets you call the product info API like once per 30 seconds for some reason...
if (error.message.startsWith("429")) {
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;
}
buy(undefined, await noblox.getGeneralToken(), info, 0).then(async () => { try {
await interaction.reply( await buy(info);
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
\`\`\` \`\`\`
@ -34,18 +47,49 @@ Now that your map (id: ${id}) has been taken by the ${game} maptest bot you can
\`\`\`Read what it says. If your map successfully loaded type !rtv and then choose your map. \`\`\`Read what it says. If your map successfully loaded type !rtv and then choose your map.
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) {
.catch(async (error) => { if (error.message == "You already own this item.") {
if (error.message == "You already own this item.") { await interaction.reply({content: "The bot has already taken this model!", ephemeral: true});
await interaction.reply({content: "The bot has already taken this model!", ephemeral: true}); } else {
} else { await interaction.reply({content: `An error occured trying to take the model (id: ${id}). Make sure it is uncopylocked!`, ephemeral: true});
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(`Could not take asset ID ${id}: `); console.log(error);
console.log(error); }
}
}
async function buy (product) {
const productId = product.ProductId;
const httpOpt = {
url: '//economy.roblox.com/v1/purchases/products/' + productId,
options: {
method: 'POST',
jar: undefined,
headers: {
'X-CSRF-TOKEN': await noblox.getGeneralToken()
},
json: {
expectedCurrency: 1,
expectedPrice: 0,
expectedSellerId: product.Creator.Id
} }
}); }
}); };
const json = await noblox.http(httpOpt);
console.log(json)
let err = json.errorMsg
if (json.reason === 'InsufficientFunds') {
err = 'You need ' + json.shortfallPrice + ' more robux to purchase this item.';
} else if (json.errorMsg) {
err = json.errorMsg;
}
if (!err) {
return productId;
} else {
throw new Error(err);
}
} }
module.exports = { module.exports = {
@ -63,89 +107,4 @@ module.exports = {
.setRequired(true)) .setRequired(true))
, ,
execute execute
}; };
async function buy (jar, token, product, price) {
const robux = product.PriceInRobux || 0
const productId = product.ProductId
if (price) {
if (typeof price === 'number') {
if (robux !== price) {
throw new Error('Price requirement not met. Requested price: ' + price + ' Actual price: ' + robux)
}
} else if (typeof price === 'object') {
const high = price.high
const low = price.low
if (high) {
if (robux > high) {
throw new Error('Price requirement not met. Requested price: <=' + high + ' Actual price: ' + robux)
}
}
if (low) {
if (robux < low) {
throw new Error('Price requirement not met. Requested price: >=' + low + ' Actual price: ' + robux)
}
}
}
}
const httpOpt = {
url: '//economy.roblox.com/v1/purchases/products/' + productId,
options: {
method: 'POST',
jar: jar,
headers: {
'X-CSRF-TOKEN': token
},
json: {
expectedCurrency: 1,
expectedPrice: robux,
expectedSellerId: product.Creator.Id
}
}
}
const json = await noblox.http(httpOpt);
let err = json.errorMsg;
if (json.reason === 'InsufficientFunds') {
err = 'You need ' + json.shortfallPrice + ' more robux to purchase this item.';
} else if (json.errorMsg) {
err = json.errorMsg;
}
if (!err) {
return { productId, price: robux };
} else {
throw new Error(err);
}
}
function getProductInfo (asset) {
return new Promise(async (resolve, reject) => {
const httpOpt = {
url: `//economy.roblox.com/v2/assets/${asset}/details`,
options: {
resolveWithFullResponse: true,
method: 'GET'
}
}
try {
const res = await noblox.http(httpOpt);
if (res.statusCode === 200) {
resolve(JSON.parse(res.body));
} else {
// Sourced from: https://stackoverflow.com/a/32278428
const isAnObject = (val_1) => !!(val_1 instanceof Array || val_1 instanceof Object);
const body = isAnObject(res.body) ? JSON.parse(res.body) : {};
if (body.errors && body.errors.length > 0) {
const errors = body.errors.map((e) => {
return e.message;
});
reject(new Error(`${res.statusCode} ${errors.join(', ')}`));
} else {
reject(new Error(`${res.statusCode} ${res.body}`));
}
}
} catch (error) {
return reject(error);
}
})
}

View File

@ -6,7 +6,7 @@
"csv-parse": "^5.0.4", "csv-parse": "^5.0.4",
"discord-api-types": "^0.32.1", "discord-api-types": "^0.32.1",
"discord.js": "^13.6.0", "discord.js": "^13.6.0",
"noblox.js": "^4.13.1", "noblox.js": "^4.15.1",
"node-csv": "^0.1.2" "node-csv": "^0.1.2"
} }
} }