diff --git a/bot.js b/bot.js index ecdabf0..6311d3f 100644 --- a/bot.js +++ b/bot.js @@ -1,11 +1,12 @@ const fs = require('node:fs'); -const { Client, Collection } = require("discord.js"); -const {token} = require("./config/config.json"); +const { Client, Collection, GatewayIntentBits, ActivityType } = require("discord.js"); +const { token } = require("./config/config.json"); +const { logChannelId } = require("./config/config.js"); -const client = new Client({intents: ["Guilds"]}); +const client = new Client({intents: [GatewayIntentBits.Guilds]}); client.commands = new Collection(); -const commandFiles = fs.readdirSync('./commands').filter(file => file.endsWith('.js')); +const commandFiles = fs.readdirSync("./commands").filter(file => file.endsWith(".js")); for (const file of commandFiles) { const command = require(`./commands/${file}`); @@ -14,7 +15,7 @@ for (const file of commandFiles) { client.commands.set(command.data.name, command); } -client.on('interactionCreate', async interaction => { +client.on("interactionCreate", async interaction => { if (!interaction.isCommand()) return; const command = client.commands.get(interaction.commandName); @@ -22,22 +23,48 @@ client.on('interactionCreate', async interaction => { if (!command) return; try { + // Show "Bot is thinking..." + await interaction.deferReply(); await command.execute(interaction); } catch (error) { console.error(error); - await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true }); + if (interaction.replied) { + await interaction.followUp("An unknown error occured while performing this command!"); + } + else { + await interaction.editReply("An unknown error occured while performing this command!"); + } + await logError(error, interaction); } }); -client.on("error", async _error => { +/** + * @param {Error} error + * @param {import('discord.js').ChatInputCommandInteraction} interaction + */ +async function logError(error, interaction) { + const logChannel = client.channels.cache.get(logChannelId); + if (!logChannel) { + console.log("Couldn't find log channel with the given id: " + logChannelId); + return; + } + const message = await interaction.fetchReply(); + let errMsg = `An error occured when performing \`/${interaction.commandName}\` (${interaction.options.data.map((option) => `${option.name}: \`${option.value}\``).join(", ")}) at ${message.url}\n`; + errMsg += `\`\`\`\n${error.stack ? error.stack : error}\n\`\`\``; + + await logChannel.send(errMsg); +} + +client.on("error", async error => { + console.error(error); }); client.once("ready", () => { console.log("Ready"); client.user.setActivity({ name: "use /take and /submit", - type: 0 + type: ActivityType.Custom }); }); diff --git a/commands/submissions.js b/commands/submissions.js index 122c504..dbbcb38 100644 --- a/commands/submissions.js +++ b/commands/submissions.js @@ -15,12 +15,12 @@ async function execute(interaction) { const fname = submissions[game]; if (fname === undefined) { - await interaction.reply({content: "Invalid game specified!", ephemeral: true}); + await interaction.editReply("🚫 Invalid game specified!"); return; } if (!fs.existsSync(fname)) { - await interaction.reply(`No submissions exist yet for ${game}.`); + await interaction.editReply(`No submissions exist yet for ${game}.`); return; } @@ -33,7 +33,7 @@ async function execute(interaction) { if (afterDateStr) { const afterDate = Sugar.Date.create(afterDateStr); if (isNaN(afterDate)) { - await interaction.reply({content: `Could not convert '${afterDateStr}' to a valid date.`, ephemeral: true}); + await interaction.editReply(`🚫 Could not convert '${afterDateStr}' to a valid date.`); return; } afterTimestamp = Math.round(afterDate / 1000); @@ -42,14 +42,14 @@ async function execute(interaction) { if (beforeDateStr) { const beforeDate = Sugar.Date.create(beforeDateStr); if (isNaN(beforeDate)) { - await interaction.reply({content: `Could not convert '${beforeDateStr}' to a valid date.`, ephemeral: true}); + await interaction.editReply(`🚫 Could not convert '${beforeDateStr}' to a valid date.`); return; } beforeTimestamp = Math.round(beforeDate / 1000); } if (!isNaN(afterTimestamp) && !isNaN(beforeTimestamp) && beforeTimestamp < afterTimestamp) { - await interaction.reply({content: `Your date range is invalid: ${getDateRangeString(afterTimestamp, beforeTimestamp)}.`, ephemeral: true}); + await interaction.editReply(`🚫 Your date range is invalid: ${getDateRangeString(afterTimestamp, beforeTimestamp)}.`); return; } @@ -82,17 +82,17 @@ async function execute(interaction) { else { msg = "Could not find any submissions."; } - await interaction.reply({content: msg, ephemeral: true}); + await interaction.editReply(msg); return; } const file = new AttachmentBuilder(Buffer.from(csvString), { name: fname }); const dateRangeStr = getDateRangeString(afterTimestamp, beforeTimestamp); if (dateRangeStr) { - await interaction.reply({content: `Using date range ${dateRangeStr}:`, files: [file]}); + await interaction.editReply({content: `Using date range ${dateRangeStr}:`, files: [file]}); } else { - await interaction.reply({files: [file]}); + await interaction.editReply({files: [file]}); } } diff --git a/commands/submit.js b/commands/submit.js index 6c9e988..ffd0080 100644 --- a/commands/submit.js +++ b/commands/submit.js @@ -32,15 +32,14 @@ async function robloxUsernameFromId(id) { async function execute(interaction) { const userId = await robloxUserFromDiscord(interaction.user.id); if (!userId) { - 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.editReply("🚫 You don't have a Roblox account linked with your Discord account. Use !link with the rbhop dog bot to link your account."); return; } - const game = interaction.options.getString("game", true); + const game = interaction.options.getString("game", true); const fname = submissions[game]; if (fname === undefined) { - await interaction.reply({content: "Invalid game specified!", ephemeral: true}); + await interaction.editReply("🚫 Invalid game specified!"); return; } @@ -51,28 +50,39 @@ async function execute(interaction) { const id = interaction.options.getInteger("asset_id", true); 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}); + + + 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.`; + await interaction.editReply(msg); + return; + } + } catch (error) { + if (error.message !== "400 The specified Asset does not exist!") { + throw error; + } + await interaction.editReply(`🚫 This asset does not exist (id: ${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.`); + return; + } + if (assetInfo.creatorId !== userId) { const assetUsernamePromise = robloxUsernameFromId(assetInfo.creatorId); const interactionUsernamePromise = robloxUsernameFromId(userId); const assetUsername = await assetUsernamePromise; const interactionUsername = await interactionUsernamePromise; - 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}); - return; - } - // Shouldn't really be possible but who knows... - if (assetInfo.typeId !== AssetType.Model) { - await interaction.reply({content: `This asset (id: ${id}) is not a model. Your map must be a model.`, ephemeral: true}); + const msg = `🚫 The account linked to your Discord (${interactionUsername}) is not the owner of this model (${assetUsername}), so you cannot submit it.`; + await interaction.editReply(msg); return; } + const csvFile = fs.readFileSync(fname); const lines = parse(csvFile, {delimiter: ',', fromLine: 2}); @@ -80,14 +90,11 @@ async function execute(interaction) { for (let lineStr of lines) { const line = getSubmissionLine(lineStr); if (id === line.modelId) { - await interaction.reply({content: `This map (id: ${id}) was already submitted on .`, ephemeral: true}); + await interaction.editReply(`🚫 This map (id: ${id}) was already submitted on .`); return; } } - // Show "Bot is thinking..." - await interaction.deferReply(); - // Validate and send the validation result const validation = await validateMapAsset(id, game); const msg = getValidationMessage(validation, game, true); diff --git a/commands/take.js b/commands/take.js index edaf778..2800c82 100644 --- a/commands/take.js +++ b/commands/take.js @@ -10,35 +10,45 @@ async function execute(interaction) { const game = interaction.options.getString("game", true); const cookie = cookies[game]; if (cookie === undefined) { - await interaction.reply({content: "Invalid game specified!", ephemeral: true}); + await interaction.editReply("🚫 Invalid game specified!"); return; } const id = interaction.options.getInteger("asset_id", true); await noblox.setCookie(cookie); - // Check that the bot doesn't already own this asset - if (await noblox.getOwnership(await noblox.getCurrentUser("UserID"), id, "Asset")) { - const msg = `The ${game} maptest bot already has this model (id: ${id})`; - await interaction.reply({content: msg, ephemeral: true}); + + try { + // Check that the bot doesn't already own this asset + if (await noblox.getOwnership(await noblox.getCurrentUser("UserID"), id, "Asset")) { + const msg = `🚫 The ${game} maptest bot already has this model (id: ${id})`; + await interaction.editReply(msg); + return; + } + } catch (error) { + if (error.message !== "400 The specified Asset does not exist!") { + throw error; + } + 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 === 404) { - await interaction.reply({content: `This asset may not exist or is not a model (id: ${id}). Your map must be a model.`, ephemeral: true}); + 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.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.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.`); return; } if (assetInfo.typeId !== AssetType.Model) { - await interaction.reply({content: `This asset (id: ${id}) is not a model. Your map must be a model.`, ephemeral: true}); + await interaction.editReply(`🚫 This asset (id: ${id}) is not a model. Your map must be a model.`); 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}); + await interaction.editReply(`🚫 This model (id: ${id}) is not free. Please change the price to be free.`); return; } @@ -50,9 +60,6 @@ async function execute(interaction) { Id: assetInfo.creatorId } }; - - // Show "Bot is thinking..." - await interaction.deferReply(); // Kick off the buy request const buyPromise = noblox.buy({product: productInfo, price: 0}); diff --git a/common.js b/common.js index f71847a..054b98a 100644 --- a/common.js +++ b/common.js @@ -29,7 +29,7 @@ async function getAssetInfo(assetId) { 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) + validateStatus: (_status) => true }); if (res.status < 200 || res.status > 300) { diff --git a/config/config.js b/config/config.js index 84634ab..60f6e63 100644 --- a/config/config.js +++ b/config/config.js @@ -1,4 +1,4 @@ -const { bhopCookie, surfCookie, deathrunCookie, flytrialsCookie } = require("./config.json"); +const { bhopCookie, surfCookie, deathrunCookie, flytrialsCookie, devLogChannelId } = require("./config.json"); const cookies = { bhop: bhopCookie, @@ -21,4 +21,6 @@ const gamePlaces = { flytrials: "https://www.roblox.com/games/12724901535/" }; -module.exports = { cookies, submissions, commands, gamePlaces }; +const logChannelId = devLogChannelId ?? "1233066743023538257"; // Default is the error-logs channel in rbhop + +module.exports = { cookies, submissions, commands, gamePlaces, logChannelId }; diff --git a/package.json b/package.json index 161c789..6e942c0 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "discord.js": "^14.14.1", "noblox.js": "^4.15.1", "node-csv": "^0.1.2", - "rbxm-parser": "^1.0.4", + "rbxm-parser": "^1.0.6", "sugar-date": "^2.0.6" }, "devDependencies": {