From 21b7c2876b468115194e1205adfee7359e633097 Mon Sep 17 00:00:00 2001 From: Carter Penterman Date: Tue, 16 Apr 2024 19:58:24 -0500 Subject: [PATCH] Allow filtering submissions on date range + add extra info to .csv --- commands/submissions.js | 96 +++++++++++++++++++++++++++++++++++++++-- commands/submit.js | 30 ++++++------- common.js | 35 ++++++++++++++- package.json | 3 +- 4 files changed, 144 insertions(+), 20 deletions(-) diff --git a/commands/submissions.js b/commands/submissions.js index ac183f6..0fd996c 100644 --- a/commands/submissions.js +++ b/commands/submissions.js @@ -2,6 +2,9 @@ const { SlashCommandBuilder } = require('@discordjs/builders'); const { MessageAttachment } = 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 Sugar = require("sugar-date"); async function execute(interaction) { const game = interaction.options.getString("game"); @@ -17,10 +20,91 @@ async function execute(interaction) { return; } - const csv = fs.readFileSync(fname); - const file = new MessageAttachment(csv, fname); - await interaction.reply({files: [file]}); + const afterDateStr = interaction.options.getString("after_date"); + const beforeDateStr = interaction.options.getString("before_date"); + let afterTimestamp = undefined; + let beforeTimestamp = undefined; + // Using Sugar for magic date parsing + 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}); + return; + } + afterTimestamp = Math.round(afterDate / 1000); + } + + 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}); + 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}); + return; + } + + const csvFile = fs.readFileSync(fname); + + const lines = parse(csvFile, {delimiter: ',', fromLine: 2}); + + let csvString = "map_id,unix_timestamp,date_string,user_id,username\n"; + let found = false; + for (let lineStr of lines) { + const line = getSubmissionLine(lineStr); + + // Must be between the given date range + if ((!isNaN(afterTimestamp) && line.timestamp < afterTimestamp) || + (!isNaN(beforeTimestamp) && line.timestamp > beforeTimestamp)) { + continue; + } + + 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`; + } + + if (!found) { + let msg; + const dateRangeStr = getDateRangeString(afterTimestamp, beforeTimestamp); + if (dateRangeStr) { + msg = `Could not find any submissions ${dateRangeStr}.`; + } + else { + msg = "Could not find any submissions."; + } + await interaction.reply({content: msg, ephemeral: true}); + return; + } + + const file = new MessageAttachment(Buffer.from(csvString), fname); + const dateRangeStr = getDateRangeString(afterTimestamp, beforeTimestamp); + if (dateRangeStr) { + await interaction.reply({content: `Using date range ${dateRangeStr}:`, files: [file]}); + } + else { + await interaction.reply({files: [file]}); + } +} + +function getDateRangeString(afterTimestamp, beforeTimestamp) { + if (!isNaN(afterTimestamp) && !isNaN(beforeTimestamp)) { + return `between to `; + } + else if (!isNaN(afterTimestamp)) { + return `after `; + } + else if (!isNaN(beforeTimestamp)) { + return `before `; + } + else { + return ""; + } } module.exports = { @@ -32,6 +116,12 @@ module.exports = { .setDescription("Select the maptest game") .setRequired(true) .addChoices(...commands)) + .addStringOption(option => + option.setName("after_date") + .setDescription("Filter out submissions that weren't submitted after the given date")) + .addStringOption(option => + option.setName("before_date") + .setDescription("Filter out submissions that weren't submitted before the given date")) , execute }; diff --git a/commands/submit.js b/commands/submit.js index 07bffe3..a11e86d 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 } = require("../common.js"); +const { AssetType, getAssetInfo, SubmissionColumnsString, createSubmissionLine, getSubmissionLine } = require("../common.js"); async function robloxUserFromDiscord(id) { if (isNaN(id)) return undefined; @@ -42,7 +42,7 @@ async function execute(interaction) { } if (!fs.existsSync(fname)) { - fs.writeFileSync(fname, "id,timestamp\n"); + fs.writeFileSync(fname, SubmissionColumnsString); } const id = interaction.options.getInteger("asset_id"); @@ -71,24 +71,24 @@ async function execute(interaction) { return; } - const csv = fs.readFileSync(fname); - const records = parse(csv, {delimiter: ',', fromLine: 2}); + const csvFile = fs.readFileSync(fname); + const lines = parse(csvFile, {delimiter: ',', fromLine: 2}); - let s = "id,timestamp\n"; - for (let record of records) { - const rid = record[0]; - const rtimestamp = record[1]; - if (id === rid) { - await interaction.reply({content: `This map (id: ${id}) was already submitted on .`, ephemeral: true}); + 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 .`, ephemeral: true}); return; } - s += `${rid},${rtimestamp}\n`; + + csvString += createSubmissionLine(line.modelId, line.timestamp, line.userId, line.username); } - const unix = Math.round(+new Date()/1000); - s += `${id},${unix}\n`; - - fs.writeFileSync(fname, s); + const unixTimestamp = Math.round(+new Date()/1000); + csvString += createSubmissionLine(id, unixTimestamp, userId, await robloxUsernameFromId(userId)); + fs.writeFileSync(fname, csvString); await interaction.reply(`Map (id: ${id}) successfully submitted.`); } diff --git a/common.js b/common.js index 0134e34..f23ccd9 100644 --- a/common.js +++ b/common.js @@ -52,4 +52,37 @@ async function getAssetInfo(assetId) { } } -module.exports = { AssetType, getAssetInfo }; \ No newline at end of file +const SubmissionColumnsString = "map_id,unix_timestamp,user_id,username\n"; + +const SubmissionColumn = { + ModelID: 0, + UnixTimestamp: 1, + UserID: 2, + Username: 3 +}; + +function getSubmissionLine(line) { + const modelId = Number(line[SubmissionColumn.ModelID]); + const timestamp = Number(line[SubmissionColumn.UnixTimestamp]); + let userId = ""; + let username = ""; + + // Backwards compatibility + if (line.length > 2) { + userId = Number(line[SubmissionColumn.UserID]); + username = line[SubmissionColumn.Username]; + } + + return { + modelId: modelId, + timestamp: timestamp, + userId: userId, + username: username + }; +} + +function createSubmissionLine(modelId, timestamp, userId, username) { + return `${modelId},${timestamp},${userId},${username}\n`; +} + +module.exports = { AssetType, getAssetInfo, SubmissionColumn, SubmissionColumnsString, getSubmissionLine, createSubmissionLine }; \ No newline at end of file diff --git a/package.json b/package.json index 9dea21b..578eb5d 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "discord-api-types": "^0.32.1", "discord.js": "^13.6.0", "noblox.js": "^4.15.1", - "node-csv": "^0.1.2" + "node-csv": "^0.1.2", + "sugar-date": "^2.0.6" } }