Parse map models and validate them ()

The enhancements:
- Added map validation that checks for some common errors that maps have
- /take: Does map validation but will still take the map even if there are problems
- /take: Improved the help text and added links to the maptest places
- /submit: Does map validation and will not let you submit if there are problems
- /submissions: Now shows the map's display name and creator
- Added an ESLint config and tidied some things up
- Updated some of the packages

Reviewed-on: 
Co-authored-by: Carter Penterman <carterpenterman@gmail.com>
Co-committed-by: Carter Penterman <carterpenterman@gmail.com>
This commit is contained in:
2024-04-24 06:28:07 +00:00
committed by Quaternions
parent f8476577d6
commit 3a167cf686
8 changed files with 267 additions and 45 deletions

@ -1,13 +1,17 @@
const { SlashCommandBuilder } = require('@discordjs/builders');
const { MessageAttachment } = require("discord.js");
const { AttachmentBuilder } = require("discord.js");
const fs = require('node:fs');
const { submissions, commands } = require("../config/config.js");
const { parse } = require("csv-parse/sync");
const { getSubmissionLine } = require("../common.js");
const { getSubmissionLine, safeCsvFormat } = require("../common.js");
const Sugar = require("sugar-date");
const { Buffer } = require("buffer");
/**
* @param {import('discord.js').ChatInputCommandInteraction} interaction
*/
async function execute(interaction) {
const game = interaction.options.getString("game");
const game = interaction.options.getString("game", true);
const fname = submissions[game];
if (fname === undefined) {
@ -53,7 +57,7 @@ async function execute(interaction) {
const lines = parse(csvFile, {delimiter: ',', fromLine: 2});
let csvString = "map_id,unix_timestamp,date_string,user_id,username\n";
let csvString = "map_id,unix_timestamp,date_string,user_id,username,display_name,creator\n";
let found = false;
for (let lineStr of lines) {
const line = getSubmissionLine(lineStr);
@ -66,7 +70,7 @@ async function execute(interaction) {
found = true;
const dateStr = new Date(line.timestamp * 1000).toLocaleString("en-US", {dateStyle: "short"});
csvString += `${line.modelId},${line.timestamp},${dateStr},${line.userId},${line.username}\n`;
csvString += `${line.modelId},${line.timestamp},${dateStr},${line.userId},${safeCsvFormat(line.username)},${safeCsvFormat(line.displayName)},${safeCsvFormat(line.creator)}\n`;
}
if (!found) {
@ -82,7 +86,7 @@ async function execute(interaction) {
return;
}
const file = new MessageAttachment(Buffer.from(csvString), fname);
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]});

@ -4,14 +4,14 @@ 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 } = require("../common.js");
const { AssetType, getAssetInfo, SubmissionColumnsString, createSubmissionLine, getSubmissionLine, validateMapAsset, getValidationMessage } = require("../common.js");
async function robloxUserFromDiscord(id) {
if (isNaN(id)) return undefined;
try {
const res = await axios.get(`https://api.fiveman1.net/v1/users/${id}`);
return res.data.result.robloxId;
} catch (error) {
} catch {
return undefined;
}
}
@ -21,11 +21,14 @@ async function robloxUsernameFromId(id) {
try {
const res = await axios.get(`https://users.roblox.com/v1/users/${id}`);
return res.data.name;
} catch (error) {
} catch {
return undefined;
}
}
/**
* @param {import('discord.js').ChatInputCommandInteraction} interaction
*/
async function execute(interaction) {
const userId = await robloxUserFromDiscord(interaction.user.id);
if (!userId) {
@ -33,7 +36,7 @@ async function execute(interaction) {
await interaction.reply({content: msg, ephemeral: true});
return;
}
const game = interaction.options.getString("game");
const game = interaction.options.getString("game", true);
const fname = submissions[game];
if (fname === undefined) {
@ -45,7 +48,7 @@ async function execute(interaction) {
fs.writeFileSync(fname, SubmissionColumnsString);
}
const id = interaction.options.getInteger("asset_id");
const id = interaction.options.getInteger("asset_id", true);
await noblox.setCookie(cookies[game]);
// Check that the bot owns this model
@ -74,23 +77,38 @@ async function execute(interaction) {
const csvFile = fs.readFileSync(fname);
const lines = parse(csvFile, {delimiter: ',', fromLine: 2});
let csvString = SubmissionColumnsString;
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 <t:${line.timestamp}:d>.`, ephemeral: true});
return;
}
}
csvString += createSubmissionLine(line.modelId, line.timestamp, line.userId, line.username);
const mapValidatePromise = validateMapAsset(id, game);
const message = await interaction.reply("⌛ Validating map...");
const validation = await mapValidatePromise;
const msg = getValidationMessage(validation, game, true);
await message.edit(msg);
if (!validation.valid) {
await interaction.followUp("Due to having problems, your map was **NOT submitted**.");
return;
}
let csvString = SubmissionColumnsString;
for (let lineStr of lines) {
const line = getSubmissionLine(lineStr);
csvString += createSubmissionLine(line.modelId, line.timestamp, line.userId, line.username, line.displayName, line.creator);
}
const unixTimestamp = Math.round(+new Date()/1000);
csvString += createSubmissionLine(id, unixTimestamp, userId, await robloxUsernameFromId(userId));
csvString += createSubmissionLine(id, unixTimestamp, userId, await robloxUsernameFromId(userId), validation.displayName, validation.creator);
fs.writeFileSync(fname, csvString);
await interaction.reply(`Map (id: ${id}) successfully submitted.`);
await interaction.followUp(`Map (id: ${id}) successfully submitted.`);
}
module.exports = {

@ -1,17 +1,20 @@
const { SlashCommandBuilder } = require('@discordjs/builders');
const noblox = require("noblox.js");
const { cookies, commands } = require("../config/config.js");
const { AssetType, getAssetInfo } = require("../common.js");
const { cookies, commands, gamePlaces } = require("../config/config.js");
const { AssetType, getAssetInfo, validateMapAsset, getValidationMessage } = require("../common.js");
/**
* @param {import('discord.js').ChatInputCommandInteraction} interaction
*/
async function execute(interaction) {
const game = interaction.options.getString("game");
const game = interaction.options.getString("game", true);
const cookie = cookies[game];
if (cookie === undefined) {
await interaction.reply({content: "Invalid game specified!", ephemeral: true});
return;
}
const id = interaction.options.getInteger("asset_id");
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")) {
@ -46,16 +49,23 @@ async function execute(interaction) {
Creator: {
Id: assetInfo.creatorId
}
}
};
await noblox.buy({product: productInfo, price: 0});
await interaction.reply(
const buyPromise = noblox.buy({product: productInfo, price: 0});
const mapValidatePromise = validateMapAsset(id, game);
const message = await interaction.reply("⌛ Validating map...");
const validation = await mapValidatePromise;
const msg = getValidationMessage(validation, game, false);
await message.edit(msg);
await buyPromise;
await interaction.followUp(
`
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
\`\`\`
!map ${id}
\`\`\`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.
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]}>).
To load your map, join the game and do \`!map ${id}\`. If your map successfully loaded, do \`!rtv\` and then choose your map.
Otherwise, you can expand the chat to view the full error message by clicking and dragging on the edge of the chat.
`
);
}