From 7e7d3c15b6de3919320623f177da8844b21ac040 Mon Sep 17 00:00:00 2001 From: Jessica Xu Date: Wed, 29 Mar 2023 11:34:30 +1100 Subject: [PATCH 01/12] initial travelguide add subcommand --- commands/travelguide.js | 66 +++++++++++++++++++++++++++++++++++++++++ config/travelguide.json | 18 +++++++++++ 2 files changed, 84 insertions(+) create mode 100644 commands/travelguide.js create mode 100644 config/travelguide.json diff --git a/commands/travelguide.js b/commands/travelguide.js new file mode 100644 index 00000000..90d7970b --- /dev/null +++ b/commands/travelguide.js @@ -0,0 +1,66 @@ +const travelguide = require("../config/travelguide.json"); +const { SlashCommandBuilder } = require("@discordjs/builders"); +const { MessageEmbed, MessageActionRow, MessageButton } = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("travelguide") + .setDescription( + "Add to and display a travel guide for the recommended restuarants, scenic views and more!", + ) + .addSubcommand((subcommand) => + subcommand + .setName("add") + .setDescription( + "Add a recommendation to the travel guide. Then the recommendation will be considered for approval.", + ) + // [recommendation name] [category] [description] [season optional] [recommender?] + .addStringOption((option) => + option + .setName("recommendation location") + .setDescription("Name of the recommended place.") + .setRequired(true), + ) + .addStringOption((option) => + option + .setName("category") + .setDescription( + "The recommended category out of: entertainment, scenic views and restuarants.", + ) + .setRequired(true) + .addChoices( + { name: 'entertainment', value: 'entertainment' }, + { name: 'scenic views', value: 'scenic views' }, + { name: 'restuarants', value: 'restuarants' }, + )); + ) + .addStringOption((option) => + option + .setName("description") + .setDescription( + "Brief description of the recommended place in 1-2 sentences.", + ) + .setRequired(true), + ) + .addStringOption((option) => + option + .setName("season") + .setDescription("The recommended season for the location.") + .setRequired(false), + ), + + ), + async execute(interaction) { + if (interaction.options.getSubcommand() === "add") { + let jsonObj = JSON.parse(travelguide); + let recommendation = { + "name": interaction.options.getString("recommendation location"), + "description": interaction.options.getString("description"), + "season": "", + } + let category = interaction.options.getString("category"); + jsonObj.category.push(recommendation); + jsonObj = JSON.stringify(jsonObj); + } + } +}; diff --git a/config/travelguide.json b/config/travelguide.json new file mode 100644 index 00000000..21f33c33 --- /dev/null +++ b/config/travelguide.json @@ -0,0 +1,18 @@ +{ + "guide": { + "category1": [ + { + "name": "store1", + "description": "nice place", + "season": "" + } + ], + "category2": [ + { + "name": "store2", + "description": "nice place", + "season": "" + } + ] + } +} From 526b62f44798acf9d25709bdbb35b1e9f0789c47 Mon Sep 17 00:00:00 2001 From: Chloe Date: Sun, 21 Apr 2024 20:44:34 +1000 Subject: [PATCH 02/12] fixing formatting --- commands/travelguide.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/commands/travelguide.js b/commands/travelguide.js index 90d7970b..ad690ac9 100644 --- a/commands/travelguide.js +++ b/commands/travelguide.js @@ -17,23 +17,24 @@ module.exports = { // [recommendation name] [category] [description] [season optional] [recommender?] .addStringOption((option) => option - .setName("recommendation location") + .setName("recommendation_location") .setDescription("Name of the recommended place.") .setRequired(true), ) - .addStringOption((option) => + .addStringOption(option => option .setName("category") .setDescription( - "The recommended category out of: entertainment, scenic views and restuarants.", + "The recommended category out of: entertainment, scenic views and restaurants.", ) .setRequired(true) .addChoices( - { name: 'entertainment', value: 'entertainment' }, - { name: 'scenic views', value: 'scenic views' }, - { name: 'restuarants', value: 'restuarants' }, - )); + { name: "entertainment", value: "entertainment" }, + { name: "scenic views", value: "scenic views" }, + { name: "restaurants", value: "restaurants" }, + )), ) + .addStringOption((option) => option .setName("description") @@ -48,19 +49,18 @@ module.exports = { .setDescription("The recommended season for the location.") .setRequired(false), ), - ), async execute(interaction) { if (interaction.options.getSubcommand() === "add") { let jsonObj = JSON.parse(travelguide); let recommendation = { - "name": interaction.options.getString("recommendation location"), - "description": interaction.options.getString("description"), - "season": "", - } + name: interaction.options.getString("recommendation location"), + description: interaction.options.getString("description"), + season: "", + }; let category = interaction.options.getString("category"); jsonObj.category.push(recommendation); jsonObj = JSON.stringify(jsonObj); } - } + }, }; From 614481ff50a8bf6cc3cb121460c6fc0d8e9ce14f Mon Sep 17 00:00:00 2001 From: Chloe Date: Mon, 20 May 2024 22:28:22 +1000 Subject: [PATCH 03/12] fixing syntax error --- commands/travelguide.js | 6 +++--- package-lock.json | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/commands/travelguide.js b/commands/travelguide.js index ad690ac9..5c2ce2af 100644 --- a/commands/travelguide.js +++ b/commands/travelguide.js @@ -21,7 +21,7 @@ module.exports = { .setDescription("Name of the recommended place.") .setRequired(true), ) - .addStringOption(option => + .addStringOption((option) => option .setName("category") .setDescription( @@ -32,9 +32,8 @@ module.exports = { { name: "entertainment", value: "entertainment" }, { name: "scenic views", value: "scenic views" }, { name: "restaurants", value: "restaurants" }, - )), + ), ) - .addStringOption((option) => option .setName("description") @@ -50,6 +49,7 @@ module.exports = { .setRequired(false), ), ), + async execute(interaction) { if (interaction.options.getSubcommand() === "add") { let jsonObj = JSON.parse(travelguide); diff --git a/package-lock.json b/package-lock.json index 25b90fa7..5a3c25b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1215,6 +1215,7 @@ "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", "hasInstallScript": true, + "license": "MIT", "dependencies": { "@mapbox/node-pre-gyp": "^1.0.0", "nan": "^2.17.0", From 01452bffb11335131cf65d2a60939735374ac9e2 Mon Sep 17 00:00:00 2001 From: Chloe Date: Thu, 30 May 2024 15:26:08 +1000 Subject: [PATCH 04/12] added get function --- commands/travelguide.js | 83 +++++++++++++++++++++++++++++++++++++---- config/travelguide.json | 24 +++++++----- 2 files changed, 91 insertions(+), 16 deletions(-) diff --git a/commands/travelguide.js b/commands/travelguide.js index 5c2ce2af..689e8786 100644 --- a/commands/travelguide.js +++ b/commands/travelguide.js @@ -1,6 +1,8 @@ const travelguide = require("../config/travelguide.json"); const { SlashCommandBuilder } = require("@discordjs/builders"); const { MessageEmbed, MessageActionRow, MessageButton } = require("discord.js"); +const fs = require("fs"); +const { guide } = require("../config/travelguide.json"); module.exports = { data: new SlashCommandBuilder() @@ -46,21 +48,88 @@ module.exports = { option .setName("season") .setDescription("The recommended season for the location.") - .setRequired(false), + .setRequired(false) + .addChoices( + { name: "summer", value: "summer" }, + { name: "autumn", value: "autumn" }, + { name: "winter", value: "winter" }, + { name: "spring", value: "spring" }, + { name: "all year round", value: "all year round" }, + ), + ), + ) + .addSubcommand((subcommand) => + subcommand + .setName("get") + .setDescription("Get a recommendation from the travel guide") + .addStringOption((option) => + option + .setName("category") + .setDescription("Sort by the following category") + .setRequired(true) + .addChoices( + { name: "entertainment", value: "entertainment" }, + { name: "scenic views", value: "scenic views" }, + { name: "restaurants", value: "restaurants" }, + ), ), ), async execute(interaction) { if (interaction.options.getSubcommand() === "add") { - let jsonObj = JSON.parse(travelguide); + let category = interaction.options.getString("category"); + let season = interaction.options.getString("season"); + let location = interaction.options.getString("recommendation_location"); + let description = interaction.options.getString("description"); + let recommendation = { - name: interaction.options.getString("recommendation location"), - description: interaction.options.getString("description"), - season: "", + location: location, + description: description, + season: season ? season : null, }; + + if (guide.hasOwnProperty(category)) { + let exists = guide[category].some( + (item) => + item.location === recommendation.location && + item.description === recommendation.description && + item.season === recommendation.season, + ); + + if (!exists) { + guide[category].push(recommendation); + console.log(`Added recommendation to ${category}`); + } else { + return await interaction.reply({ + content: "This entry has already been recommended before.", + ephemeral: true, + }); + } + } + fs.writeFileSync("./config/travelguide.json", JSON.stringify({ guide }, null, 4)); + + let returnString = `The recommendation at location: ${location}, with description: ${description}, `; + returnString = season + ? returnString + + `during season: ${season}, has been added to the ${category} database.` + : returnString + `has been added to the ${category} database.`; + return await interaction.reply({ + content: returnString, + ephemeral: true, + }); + } else if (interaction.options.getSubcommand() === "get") { let category = interaction.options.getString("category"); - jsonObj.category.push(recommendation); - jsonObj = JSON.stringify(jsonObj); + console.log(guide[category]); + // if (guide[category]) + return await interaction.reply({ + content: guide[category], + ephemeral: true, + }); + } else { + return await interaction.reply({ + content: "You do not have permission to execute this command.", + ephemeral: true, + }); } }, }; diff --git a/config/travelguide.json b/config/travelguide.json index 21f33c33..140d5603 100644 --- a/config/travelguide.json +++ b/config/travelguide.json @@ -1,18 +1,24 @@ { "guide": { - "category1": [ + "entertainment": [], + "scenic views": [ { - "name": "store1", - "description": "nice place", - "season": "" + "location": "your moms house", + "description": "good fun", + "season": "summer" + }, + { + "location": "your dadshouse", + "description": "good fun", + "season": "summer" } ], - "category2": [ + "restaurants": [ { - "name": "store2", - "description": "nice place", - "season": "" + "location": "asasd", + "description": "asdasd", + "season": "sss" } ] } -} +} \ No newline at end of file From e87e8e6ee8562930fbeacb94ae03ef51ec5d2477 Mon Sep 17 00:00:00 2001 From: Chloe Date: Thu, 30 May 2024 16:43:45 +1000 Subject: [PATCH 05/12] linting --- commands/travelguide.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/commands/travelguide.js b/commands/travelguide.js index 689e8786..e860dbe6 100644 --- a/commands/travelguide.js +++ b/commands/travelguide.js @@ -19,7 +19,7 @@ module.exports = { // [recommendation name] [category] [description] [season optional] [recommender?] .addStringOption((option) => option - .setName("recommendation_location") + .setName("recommendation location") .setDescription("Name of the recommended place.") .setRequired(true), ) @@ -120,6 +120,7 @@ module.exports = { } else if (interaction.options.getSubcommand() === "get") { let category = interaction.options.getString("category"); console.log(guide[category]); + // if (guide[category]) return await interaction.reply({ content: guide[category], From 440a17c2a501a4b3e4877fbc94d3048394ea8517 Mon Sep 17 00:00:00 2001 From: Chloe Date: Mon, 10 Jun 2024 19:18:54 +1000 Subject: [PATCH 06/12] completed travelguide command --- commands/travelguide.js | 332 +++++++++++++++++++++++++++++++++++----- config/travelguide.json | 23 +-- index.js | 1 + 3 files changed, 292 insertions(+), 64 deletions(-) diff --git a/commands/travelguide.js b/commands/travelguide.js index e860dbe6..32999b9c 100644 --- a/commands/travelguide.js +++ b/commands/travelguide.js @@ -1,9 +1,124 @@ -const travelguide = require("../config/travelguide.json"); const { SlashCommandBuilder } = require("@discordjs/builders"); -const { MessageEmbed, MessageActionRow, MessageButton } = require("discord.js"); +const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require("discord.js"); const fs = require("fs"); const { guide } = require("../config/travelguide.json"); +// Creates general object and id constants for function use +const prevId = "travelguidePrevButtonId"; +const nextId = "travelguideNextButtonId"; + +const prevButton = new ButtonBuilder() + .setCustomId(prevId) + .setLabel("Previous") + .setStyle(ButtonStyle.Secondary) + .setEmoji("⬅️"); + +const nextButton = new ButtonBuilder() + .setCustomId(nextId) + .setLabel("Next") + .setStyle(ButtonStyle.Secondary) + .setEmoji("➡️"); + +const generateLikeButtons = (currentIndex, numEntries) => { + const buttons = []; + const endIndex = Math.min(currentIndex + 3, numEntries); + + // Generate a button for each entry on the current page + for (let i = currentIndex; i < endIndex; i++) { + const button = new ButtonBuilder() + .setCustomId(`like_${i}`) + .setLabel(`Like ${i + 1}`) + .setStyle(ButtonStyle.Secondary); + + buttons.push(button); + } + + return buttons; +}; + +/** + * Creates an actionRowBuilder with next and previous buttons + * @param {number} currentIndex + * @param {number} numEntries + * @returns + */ +const getComponents = (currentIndex, numEntries) => { + const buttons = [ + ...(currentIndex > 0 ? [prevButton] : []), + ...(numEntries - (currentIndex + 3) > 0 ? [nextButton] : []), + ...generateLikeButtons(currentIndex, numEntries), + ]; + + if (buttons.length === 0) { + return []; + } + + return [new ActionRowBuilder().addComponents(buttons)]; +}; + +/** + * Creates an embed with recommendations starting from an index. + * @param {number} start The index to start from. + * @param {number} pages How many pages the embed has. + * @param {Array} recommendations An array of recommendations. + * @returns {EmbedBuilder} + */ + +const generateGetEmbed = (start, pages, recommendations) => { + const current = recommendations.slice(start, start + 3); + const pageNum = Math.floor(start / pages) + 1; + + return new EmbedBuilder({ + title: `Travelguide - Page ${pageNum}`, + color: 0x3a76f8, + author: { + name: "CSESoc Bot", + icon_url: "https://i.imgur.com/EE3Q40V.png", + }, + fields: current.map((recommendation, index) => ({ + name: `${start + index + 1}. ${recommendation.location}`, + value: `**Description**: ${recommendation.description} + **Season**: ${recommendation.season} + **Likes**: ${recommendation.likes.length}`, + })), + footer: { + text: "CSESoc Bot", + }, + timestamp: new Date(), + }); +}; + +/** + * Creates an embed of the added recommendation + * @param {object} recommendation The recommendation + */ + +const generateAddEmbed = (recommendation, category) => { + return new EmbedBuilder() + .setAuthor({ + name: "CSESoc Bot", + iconURL: "https://i.imgur.com/EE3Q40V.png", + }) + .setTitle(`${recommendation.location} has been added!`) + .setDescription( + `**Description**: ${recommendation.description} + **Season**: ${recommendation.season} + **Category**: ${category}`, + ) + .setColor(0x3a76f8) + .setFooter({ + text: "CSESoc Bot", + }) + .setTimestamp(); +}; + +/** + * Updates the travelguide.json database + */ +const updateFile = () => { + fs.writeFileSync("./config/travelguide.json", JSON.stringify({ guide }, null, 4)); +}; + module.exports = { data: new SlashCommandBuilder() .setName("travelguide") @@ -19,7 +134,7 @@ module.exports = { // [recommendation name] [category] [description] [season optional] [recommender?] .addStringOption((option) => option - .setName("recommendation location") + .setName("recommendation-location") .setDescription("Name of the recommended place.") .setRequired(true), ) @@ -48,7 +163,7 @@ module.exports = { option .setName("season") .setDescription("The recommended season for the location.") - .setRequired(false) + .setRequired(true) .addChoices( { name: "summer", value: "summer" }, { name: "autumn", value: "autumn" }, @@ -66,70 +181,203 @@ module.exports = { option .setName("category") .setDescription("Sort by the following category") - .setRequired(true) + .setRequired(false) .addChoices( { name: "entertainment", value: "entertainment" }, { name: "scenic views", value: "scenic views" }, { name: "restaurants", value: "restaurants" }, ), + ) + .addStringOption((option) => + option + .setName("season") + .setDescription("Sort by the following season") + .setRequired(false) + .addChoices( + { name: "summer", value: "summer" }, + { name: "autumn", value: "autumn" }, + { name: "winter", value: "winter" }, + { name: "spring", value: "spring" }, + { name: "all year round", value: "all year round" }, + ), ), + ) + .addSubcommand((subcommand) => + subcommand + .setName("delete") + .setDescription("Delete your own recommendation from the travel guide."), ), async execute(interaction) { + const authorId = interaction.user.id; + if (interaction.options.getSubcommand() === "add") { - let category = interaction.options.getString("category"); - let season = interaction.options.getString("season"); - let location = interaction.options.getString("recommendation_location"); - let description = interaction.options.getString("description"); + const category = interaction.options.getString("category"); + const season = interaction.options.getString("season"); + const location = interaction.options.getString("recommendation-location"); + const description = interaction.options.getString("description"); - let recommendation = { + const recommendation = { location: location, description: description, season: season ? season : null, + category: category, + likes: [], + authorId: authorId, + dateAdded: Date.now(), }; + const exists = guide.some( + (item) => + item.location === recommendation.location && + item.description === recommendation.description && + item.season === recommendation.season && + item.category === recommendation.category, + ); + + if (!exists) { + guide.push(recommendation); + } else { + return await interaction.reply({ + content: "This entry has already been recommended before.", + }); + } - if (guide.hasOwnProperty(category)) { - let exists = guide[category].some( - (item) => - item.location === recommendation.location && - item.description === recommendation.description && - item.season === recommendation.season, + updateFile(); + + return await interaction.reply({ + embeds: [generateAddEmbed(recommendation, category)], + }); + } else if (interaction.options.getSubcommand() === "get") { + const category = interaction.options.getString("category"); + const season = interaction.options.getString("season"); + let recommendations = guide; + if (category) { + recommendations = recommendations.filter((entry) => entry.category === category); + } + if (season) { + recommendations = recommendations.filter((entry) => entry.season === season); + } + if (recommendations.length === 0) { + return await interaction.reply({ + content: `There are currently no recommendations for your selection, add your own recommendation using the **/travelguide add command**`, + }); + } + let currentIndex = 0; + const pages = Math.ceil(recommendations.length / 3); + + await interaction.reply({ + embeds: [generateGetEmbed(currentIndex, pages, recommendations)], + components: getComponents(currentIndex, recommendations.length), + }); + + // Creates a collector for button interaction events, setting a 120s maximum + // timeout and a 30s inactivity timeout + const filter = (resInteraction) => { + return ( + (resInteraction.customId === prevId || + resInteraction.customId === nextId || + resInteraction.customId.startsWith("like_")) && + resInteraction.user.id === authorId && + resInteraction.message.interaction.id === interaction.id ); + }; + const collector = interaction.channel.createMessageComponentCollector({ + filter, + time: 120000, + idle: 30000, + }); - if (!exists) { - guide[category].push(recommendation); - console.log(`Added recommendation to ${category}`); - } else { - return await interaction.reply({ - content: "This entry has already been recommended before.", - ephemeral: true, - }); + collector.on("collect", async (i) => { + if (i.customId === prevId) { + currentIndex -= 3; + } else if (i.customId === nextId) { + currentIndex += 3; + } else if (i.customId.startsWith("like_")) { + const index = parseInt(i.customId.split("_")[1], 10); + const userIndex = recommendations[index].likes.indexOf(authorId); + if (userIndex === -1) { + // If the user hasn't liked it, increment the likes and add their ID to the likes array + recommendations[index].likes.push(authorId); + } else { + // If the user has already liked it, remove their ID from the likes array + recommendations[index].likes.splice(userIndex, 1); + } + updateFile(); } + await i.update({ + embeds: [generateGetEmbed(currentIndex, pages, recommendations)], + components: getComponents(currentIndex, recommendations.length), + }); + }); + + // Clears buttons from embed page after timeout on collector + /*eslint-disable */ + collector.on("end", (collection) => { + interaction.editReply({ components: [] }); + }); + } else if (interaction.options.getSubcommand() === "delete") { + const userEntries = []; + userEntries.push(...guide.filter((entry) => entry.authorId === authorId)); + if (userEntries.length === 0) { + return await interaction.reply({ + content: `There are currently no recommendations for your deletion, add recommendations using the **/travelguide add command**`, + }); } - fs.writeFileSync("./config/travelguide.json", JSON.stringify({ guide }, null, 4)); + // Generate an embed listing the user's entries + const userEntriesEmbed = new EmbedBuilder({ + title: `Your recommendations`, + description: "Below are your recommendations.", + color: 0x3a76f8, + author: { + name: "CSESoc Bot", + icon_url: "https://i.imgur.com/EE3Q40V.png", + }, + fields: userEntries.map((recommendation, index) => ({ + name: `${0 + index + 1}. ${recommendation.location}`, + value: `**Description**: ${recommendation.description} + **Season**: ${recommendation.season} + **Likes**: ${recommendation.likes.length}`, + })), + footer: { + text: "CSESoc Bot", + }, + timestamp: new Date(), + }); - let returnString = `The recommendation at location: ${location}, with description: ${description}, `; - returnString = season - ? returnString + - `during season: ${season}, has been added to the ${category} database.` - : returnString + `has been added to the ${category} database.`; - return await interaction.reply({ - content: returnString, - ephemeral: true, + // Send the embed + await interaction.reply({ embeds: [userEntriesEmbed]}); + + // Prompt for entry index + await interaction.channel.send("Please provide the entry number to delete."); + + const collector = interaction.channel.createMessageCollector({ + filter: (message) => message.author.id === authorId, + max: 1, + time: 10_000, }); - } else if (interaction.options.getSubcommand() === "get") { - let category = interaction.options.getString("category"); - console.log(guide[category]); - - // if (guide[category]) - return await interaction.reply({ - content: guide[category], - ephemeral: true, + + collector.on("collect", async (message) => { + const entryIndex = parseInt(message.content.trim()); + if (isNaN(entryIndex) || entryIndex < 1 || entryIndex > userEntries.length) { + await interaction.followUp( + "Invalid entry number. Please provide a valid number.", + ); + return; + } + + // Delete the entry + const deletedEntry = userEntries[entryIndex - 1]; + guide.splice(guide.indexOf(deletedEntry), 1); + updateFile(); + + // Notify the user about the deletion + await interaction.followUp(`Entry "${deletedEntry.location}" has been deleted.`); }); + + collector.on("end", (collected) => {}); } else { return await interaction.reply({ content: "You do not have permission to execute this command.", - ephemeral: true, }); } }, diff --git a/config/travelguide.json b/config/travelguide.json index 140d5603..10d71e20 100644 --- a/config/travelguide.json +++ b/config/travelguide.json @@ -1,24 +1,3 @@ { - "guide": { - "entertainment": [], - "scenic views": [ - { - "location": "your moms house", - "description": "good fun", - "season": "summer" - }, - { - "location": "your dadshouse", - "description": "good fun", - "season": "summer" - } - ], - "restaurants": [ - { - "location": "asasd", - "description": "asdasd", - "season": "sss" - } - ] - } + "guide": [] } \ No newline at end of file diff --git a/index.js b/index.js index 3f0dbeca..b2138b35 100644 --- a/index.js +++ b/index.js @@ -11,6 +11,7 @@ const client = new Client({ GatewayIntentBits.GuildMessageReactions, GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.GuildPresences, + GatewayIntentBits.MessageContent, ], partials: ["MESSAGE", "CHANNEL", "REACTION", "GUILD_MEMBER", "USER"], }); From 53d0c7a50437c3011398e7fbc2084e0895076336 Mon Sep 17 00:00:00 2001 From: Chloe Date: Mon, 10 Jun 2024 19:25:59 +1000 Subject: [PATCH 07/12] linting --- commands/travelguide.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/travelguide.js b/commands/travelguide.js index 32999b9c..8a538ccb 100644 --- a/commands/travelguide.js +++ b/commands/travelguide.js @@ -345,7 +345,7 @@ module.exports = { }); // Send the embed - await interaction.reply({ embeds: [userEntriesEmbed]}); + await interaction.reply({ embeds: [userEntriesEmbed] }); // Prompt for entry index await interaction.channel.send("Please provide the entry number to delete."); From d055204bbaf32a0093cd19e87c85569b283b6c47 Mon Sep 17 00:00:00 2001 From: Chloe Date: Thu, 13 Jun 2024 12:07:47 +1000 Subject: [PATCH 08/12] adding deletion confirmation --- commands/travelguide.js | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/commands/travelguide.js b/commands/travelguide.js index 8a538ccb..4748ad04 100644 --- a/commands/travelguide.js +++ b/commands/travelguide.js @@ -360,18 +360,39 @@ module.exports = { const entryIndex = parseInt(message.content.trim()); if (isNaN(entryIndex) || entryIndex < 1 || entryIndex > userEntries.length) { await interaction.followUp( - "Invalid entry number. Please provide a valid number.", + "Invalid entry number. No entry was deleted.", ); return; } + // Confirm entry + await interaction.channel.send( + `Type 'Y' to confirm the deletion of index **${message.content}**`, + ); + + const confirmCollector = interaction.channel.createMessageCollector({ + filter: (message) => message.author.id === authorId, + max: 1, + time: 10_000, + }); - // Delete the entry - const deletedEntry = userEntries[entryIndex - 1]; - guide.splice(guide.indexOf(deletedEntry), 1); - updateFile(); + confirmCollector.on("collect", async (message) => { + const confirmMessage = message.content.trim(); + if (confirmMessage === "Y") { + // Delete the entry + const deletedEntry = userEntries[entryIndex - 1]; + guide.splice(guide.indexOf(deletedEntry), 1); + updateFile(); + + // Notify the user about the deletion + await interaction.followUp( + `Entry "${deletedEntry.location}" has been deleted.`, + ); + } else { + await interaction.followUp(`No entry has been deleted.`); + } + return; + }); - // Notify the user about the deletion - await interaction.followUp(`Entry "${deletedEntry.location}" has been deleted.`); }); collector.on("end", (collected) => {}); From d62643371db723fdb53197ac4ee36049a65f6318 Mon Sep 17 00:00:00 2001 From: Chloe Date: Thu, 13 Jun 2024 12:35:58 +1000 Subject: [PATCH 09/12] linting --- commands/travelguide.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/commands/travelguide.js b/commands/travelguide.js index 4748ad04..7cb29d3a 100644 --- a/commands/travelguide.js +++ b/commands/travelguide.js @@ -359,9 +359,7 @@ module.exports = { collector.on("collect", async (message) => { const entryIndex = parseInt(message.content.trim()); if (isNaN(entryIndex) || entryIndex < 1 || entryIndex > userEntries.length) { - await interaction.followUp( - "Invalid entry number. No entry was deleted.", - ); + await interaction.followUp("Invalid entry number. No entry was deleted."); return; } // Confirm entry @@ -392,7 +390,6 @@ module.exports = { } return; }); - }); collector.on("end", (collected) => {}); From 17e93d001582a92ea2bdafc255c22d8b9851a5da Mon Sep 17 00:00:00 2001 From: Chloe Date: Thu, 27 Jun 2024 16:56:51 +1000 Subject: [PATCH 10/12] migrating travelguide.json to postgres --- commands/travelguide.js | 627 ++++++++++++++++++---------------- config/travelguide.json | 3 - events/travelguide_ready.js | 12 + lib/database/dbtravelguide.js | 280 +++++++++++++++ 4 files changed, 622 insertions(+), 300 deletions(-) delete mode 100644 config/travelguide.json create mode 100644 events/travelguide_ready.js create mode 100644 lib/database/dbtravelguide.js diff --git a/commands/travelguide.js b/commands/travelguide.js index 7cb29d3a..1cd00d02 100644 --- a/commands/travelguide.js +++ b/commands/travelguide.js @@ -1,9 +1,12 @@ -const { SlashCommandBuilder } = require("@discordjs/builders"); +const { SlashCommandBuilder, SlashCommandSubcommandBuilder } = require("@discordjs/builders"); const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require("discord.js"); -const fs = require("fs"); -const { guide } = require("../config/travelguide.json"); +const { DBTravelguide } = require("../lib/database/dbtravelguide"); +const { v4: uuidv4 } = require("uuid"); + +// //////////////////////////////////////////// +// //// GENERATE EMBEDS AND ACTION ROW //////// +// //////////////////////////////////////////// -// Creates general object and id constants for function use const prevId = "travelguidePrevButtonId"; const nextId = "travelguideNextButtonId"; @@ -19,6 +22,12 @@ const nextButton = new ButtonBuilder() .setStyle(ButtonStyle.Secondary) .setEmoji("➡️"); +/** + * + * @param {Number} currentIndex + * @param {Number} numEntries + * @returns like buttons for an embed + */ const generateLikeButtons = (currentIndex, numEntries) => { const buttons = []; const endIndex = Math.min(currentIndex + 3, numEntries); @@ -37,12 +46,12 @@ const generateLikeButtons = (currentIndex, numEntries) => { }; /** - * Creates an actionRowBuilder with next and previous buttons + * Creates an ActionRow with all buttons * @param {number} currentIndex * @param {number} numEntries - * @returns + * @returns {ActionRowBuilder} ActionRow containing all buttons for an embed */ -const getComponents = (currentIndex, numEntries) => { +const generateActionRow = (currentIndex, numEntries) => { const buttons = [ ...(currentIndex > 0 ? [prevButton] : []), ...(numEntries - (currentIndex + 3) > 0 ? [nextButton] : []), @@ -61,9 +70,8 @@ const getComponents = (currentIndex, numEntries) => { * @param {number} start The index to start from. * @param {number} pages How many pages the embed has. * @param {Array} recommendations An array of recommendations. - * @returns {EmbedBuilder} + * @returns {EmbedBuilder} Embed containing 3 recommendations for the travelguide GET command */ - const generateGetEmbed = (start, pages, recommendations) => { const current = recommendations.slice(start, start + 3); const pageNum = Math.floor(start / pages) + 1; @@ -89,20 +97,23 @@ const generateGetEmbed = (start, pages, recommendations) => { }; /** - * Creates an embed of the added recommendation - * @param {object} recommendation The recommendation + * + * @param {String} location + * @param {String} description + * @param {String} season + * @param {String} category + * @returns an embed containing a summary of the newly added recommendation */ - -const generateAddEmbed = (recommendation, category) => { +const generateAddEmbed = (location, description, season, category) => { return new EmbedBuilder() .setAuthor({ name: "CSESoc Bot", iconURL: "https://i.imgur.com/EE3Q40V.png", }) - .setTitle(`${recommendation.location} has been added!`) + .setTitle(`${location} has been added!`) .setDescription( - `**Description**: ${recommendation.description} - **Season**: ${recommendation.season} + `**Description**: ${description} + **Season**: ${season} **Category**: ${category}`, ) .setColor(0x3a76f8) @@ -112,291 +123,313 @@ const generateAddEmbed = (recommendation, category) => { .setTimestamp(); }; +// //////////////////////////////////////////// +// //////// SETTING UP THE COMMANDS /////////// +// //////////////////////////////////////////// + +const commandTravelguideAdd = new SlashCommandSubcommandBuilder() + .setName("add") + .setDescription( + "Add a recommendation to the travel guide. Then the recommendation will be considered for approval.", + ) + // [recommendation name] [category] [description] [season optional] [recommender?] + .addStringOption((option) => + option + .setName("recommendation-location") + .setDescription("Name of the recommended place.") + .setRequired(true), + ) + .addStringOption((option) => + option + .setName("category") + .setDescription( + "The recommended category out of: entertainment, scenic views and restaurants.", + ) + .setRequired(true) + .addChoices( + { name: "entertainment", value: "entertainment" }, + { name: "scenic views", value: "scenic views" }, + { name: "restaurants", value: "restaurants" }, + ), + ) + .addStringOption((option) => + option + .setName("description") + .setDescription("Brief description of the recommended place in 1-2 sentences.") + .setRequired(true), + ) + .addStringOption((option) => + option + .setName("season") + .setDescription("The recommended season for the location.") + .setRequired(true) + .addChoices( + { name: "summer", value: "summer" }, + { name: "autumn", value: "autumn" }, + { name: "winter", value: "winter" }, + { name: "spring", value: "spring" }, + { name: "all year round", value: "all year round" }, + ), + ); + +const commandTravelguideGet = new SlashCommandSubcommandBuilder() + .setName("get") + .setDescription("Get a recommendation from the travel guide") + .addStringOption((option) => + option + .setName("category") + .setDescription("Sort by the following category") + .setRequired(false) + .addChoices( + { name: "entertainment", value: "entertainment" }, + { name: "scenic views", value: "scenic views" }, + { name: "restaurants", value: "restaurants" }, + ), + ) + .addStringOption((option) => + option + .setName("season") + .setDescription("Sort by the following season") + .setRequired(false) + .addChoices( + { name: "summer", value: "summer" }, + { name: "autumn", value: "autumn" }, + { name: "winter", value: "winter" }, + { name: "spring", value: "spring" }, + { name: "all year round", value: "all year round" }, + ), + ); + +const commandTravelguideDelete = new SlashCommandSubcommandBuilder() + .setName("delete") + .setDescription("Delete your own recommendation from the travel guide."); + +const baseCommand = new SlashCommandBuilder() + .setName("travelguide") + .setDescription( + "Add to and display a travel guide for the recommended restuarants, scenic views and more!", + ) + .addSubcommand(commandTravelguideAdd) + .addSubcommand(commandTravelguideGet) + .addSubcommand(commandTravelguideDelete); + +// //////////////////////////////////////////// +// ///////// HANDLING THE COMMAND ///////////// +// //////////////////////////////////////////// + /** - * Updates the travelguide.json database + * + * @param {CommandInteraction} interaction */ -const updateFile = () => { - fs.writeFileSync("./config/travelguide.json", JSON.stringify({ guide }, null, 4)); -}; +async function handleInteraction(interaction) { + /** @type {DBTravelguide} */ + const travelguideStorage = global.travelguideStorage; + const authorId = interaction.user.id; + + // figure out which command was called + const subcommand = interaction.options.getSubcommand(false); + switch (subcommand) { + case "add": + await handleTravelguideAdd(interaction, travelguideStorage, authorId); + break; + case "get": + await handleTravelguideGet(interaction, travelguideStorage, authorId); + break; + case "delete": + await handleTravelguideDelete(interaction, travelguideStorage, authorId); + break; + default: + await interaction.reply("Internal Error AHHHHHHH! CONTACT ME PLEASE!"); + } +} -module.exports = { - data: new SlashCommandBuilder() - .setName("travelguide") - .setDescription( - "Add to and display a travel guide for the recommended restuarants, scenic views and more!", - ) - .addSubcommand((subcommand) => - subcommand - .setName("add") - .setDescription( - "Add a recommendation to the travel guide. Then the recommendation will be considered for approval.", - ) - // [recommendation name] [category] [description] [season optional] [recommender?] - .addStringOption((option) => - option - .setName("recommendation-location") - .setDescription("Name of the recommended place.") - .setRequired(true), - ) - .addStringOption((option) => - option - .setName("category") - .setDescription( - "The recommended category out of: entertainment, scenic views and restaurants.", - ) - .setRequired(true) - .addChoices( - { name: "entertainment", value: "entertainment" }, - { name: "scenic views", value: "scenic views" }, - { name: "restaurants", value: "restaurants" }, - ), - ) - .addStringOption((option) => - option - .setName("description") - .setDescription( - "Brief description of the recommended place in 1-2 sentences.", - ) - .setRequired(true), - ) - .addStringOption((option) => - option - .setName("season") - .setDescription("The recommended season for the location.") - .setRequired(true) - .addChoices( - { name: "summer", value: "summer" }, - { name: "autumn", value: "autumn" }, - { name: "winter", value: "winter" }, - { name: "spring", value: "spring" }, - { name: "all year round", value: "all year round" }, - ), - ), - ) - .addSubcommand((subcommand) => - subcommand - .setName("get") - .setDescription("Get a recommendation from the travel guide") - .addStringOption((option) => - option - .setName("category") - .setDescription("Sort by the following category") - .setRequired(false) - .addChoices( - { name: "entertainment", value: "entertainment" }, - { name: "scenic views", value: "scenic views" }, - { name: "restaurants", value: "restaurants" }, - ), - ) - .addStringOption((option) => - option - .setName("season") - .setDescription("Sort by the following season") - .setRequired(false) - .addChoices( - { name: "summer", value: "summer" }, - { name: "autumn", value: "autumn" }, - { name: "winter", value: "winter" }, - { name: "spring", value: "spring" }, - { name: "all year round", value: "all year round" }, - ), - ), - ) - .addSubcommand((subcommand) => - subcommand - .setName("delete") - .setDescription("Delete your own recommendation from the travel guide."), - ), - - async execute(interaction) { - const authorId = interaction.user.id; - - if (interaction.options.getSubcommand() === "add") { - const category = interaction.options.getString("category"); - const season = interaction.options.getString("season"); - const location = interaction.options.getString("recommendation-location"); - const description = interaction.options.getString("description"); - - const recommendation = { - location: location, - description: description, - season: season ? season : null, - category: category, - likes: [], - authorId: authorId, - dateAdded: Date.now(), - }; - const exists = guide.some( - (item) => - item.location === recommendation.location && - item.description === recommendation.description && - item.season === recommendation.season && - item.category === recommendation.category, - ); - - if (!exists) { - guide.push(recommendation); +// //////////////////////////////////////////// +// //////// HANDLING THE SUBCOMMANDS ////////// +// //////////////////////////////////////////// + +/** + * Adds a new recommendation to the database and displays a summary of the recommendation + * @param {CommandInteraction} interaction + * @param {DBTravelguide} travelguideStorage + * @param {Number} authorId + */ +async function handleTravelguideAdd(interaction, travelguideStorage, authorId) { + const location = interaction.options.getString("recommendation-location"); + const description = interaction.options.getString("description"); + const season = interaction.options.getString("season"); + const category = interaction.options.getString("category"); + + // check if entry exists in db + const exists = await travelguideStorage.getRecommendation(location, description, category); + if (exists.length === 0) { + travelguideStorage.addRecommendation( + uuidv4(), + location, + description, + season, + category, + authorId, + ); + } else { + return await interaction.reply({ + content: "This entry has already been recommended before.", + }); + } + + return await interaction.reply({ + embeds: [generateAddEmbed(location, description, season, category)], + }); +} + +/** + * Gets 3 recommendations sorted by category/season/neither + * @param {CommandInteraction} interaction + * @param {DBTravelguide} travelguideStorage + * @param {Number} authorId + */ +async function handleTravelguideGet(interaction, travelguideStorage, authorId) { + const category = interaction.options.getString("category"); + const season = interaction.options.getString("season"); + let recommendations = await travelguideStorage.getRecommendations(category, season); + if (recommendations.length === 0) { + return await interaction.reply({ + content: `There are currently no recommendations for your selection, add your own recommendation using the **/travelguide add command**`, + }); + } + let currentIndex = 0; + const pages = Math.ceil(recommendations.length / 3); + + await interaction.reply({ + embeds: [generateGetEmbed(currentIndex, pages, recommendations)], + components: generateActionRow(currentIndex, recommendations.length), + }); + + // Creates a collector for button interaction events, setting a 120s maximum + // timeout and a 30s inactivity timeout + const filter = (resInteraction) => { + return ( + (resInteraction.customId === prevId || + resInteraction.customId === nextId || + resInteraction.customId.startsWith("like_")) && + resInteraction.user.id === authorId && + resInteraction.message.interaction.id === interaction.id + ); + }; + const collector = interaction.channel.createMessageComponentCollector({ + filter, + time: 120000, + idle: 30000, + }); + + collector.on("collect", async (i) => { + if (i.customId === prevId) { + currentIndex -= 3; + } else if (i.customId === nextId) { + currentIndex += 3; + } else if (i.customId.startsWith("like_")) { + const index = parseInt(i.customId.split("_")[1], 10); + const recId = recommendations[index].rec_id; + + await travelguideStorage.likeRecommendation(authorId, recId); + recommendations = await travelguideStorage.getRecommendations(category, season); + } + + await i.update({ + embeds: [generateGetEmbed(currentIndex, pages, recommendations)], + components: generateActionRow(currentIndex, recommendations.length), + }); + }); + + // Clears buttons from embed page after timeout on collector + /*eslint-disable */ + collector.on("end", (collection) => { + interaction.editReply({ components: [] }); + }); +} + +/** + * Deletes a recommendation that the user owns + * @param {CommandInteraction} interaction + * @param {DBTravelguide} travelguideStorage + * @param {Number} authorId + */ +async function handleTravelguideDelete(interaction, travelguideStorage, authorId) { + const authorEntries = await travelguideStorage.getAuthorRecommendations(authorId); + if (authorEntries.length === 0) { + return await interaction.reply({ + content: `There are currently no recommendations for your deletion, add recommendations using the **/travelguide add command**`, + }); + } + // Generate an embed listing the user's entries + const authorEntriesEmbed = new EmbedBuilder({ + title: `Your recommendations`, + color: 0x3a76f8, + author: { + name: "CSESoc Bot", + icon_url: "https://i.imgur.com/EE3Q40V.png", + }, + fields: authorEntries.map((recommendation, index) => ({ + name: `${0 + index + 1}. ${recommendation.location}`, + value: `**Description**: ${recommendation.description} + **Season**: ${recommendation.season} + **Likes**: ${recommendation.likes.length}`, + })), + footer: { + text: "CSESoc Bot", + }, + timestamp: new Date(), + }); + + // Send the embed + await interaction.reply({ embeds: [authorEntriesEmbed] }); + + // Prompt for entry index + await interaction.channel.send("Please provide the entry number to delete."); + + const collector = interaction.channel.createMessageCollector({ + filter: (message) => message.author.id === authorId, + max: 1, + time: 10_000, + }); + + collector.on("collect", async (message) => { + const entryIndex = parseInt(message.content.trim()); + if (isNaN(entryIndex) || entryIndex < 1 || entryIndex > authorEntries.length) { + await interaction.followUp("Invalid entry number. No entry was deleted."); + return; + } + // Confirm entry + await interaction.channel.send( + `Type 'Y' to confirm the deletion of index **${message.content}**`, + ); + + const confirmCollector = interaction.channel.createMessageCollector({ + filter: (message) => message.author.id === authorId, + max: 1, + time: 10_000, + }); + + confirmCollector.on("collect", async (message) => { + const confirmMessage = message.content.trim(); + if (confirmMessage === "Y") { + // get the recommendationId + const recId = authorEntries[entryIndex - 1].rec_id; + const deletedLocation = authorEntries[entryIndex - 1].location; + // Delete the entry + await travelguideStorage.deleteRecommendation(authorId, recId); + // Notify the user about the deletion + await interaction.followUp(`Entry "${deletedLocation}" has been deleted.`); } else { - return await interaction.reply({ - content: "This entry has already been recommended before.", - }); + await interaction.followUp(`No entry has been deleted.`); } + return; + }); + }); - updateFile(); - - return await interaction.reply({ - embeds: [generateAddEmbed(recommendation, category)], - }); - } else if (interaction.options.getSubcommand() === "get") { - const category = interaction.options.getString("category"); - const season = interaction.options.getString("season"); - let recommendations = guide; - if (category) { - recommendations = recommendations.filter((entry) => entry.category === category); - } - if (season) { - recommendations = recommendations.filter((entry) => entry.season === season); - } - if (recommendations.length === 0) { - return await interaction.reply({ - content: `There are currently no recommendations for your selection, add your own recommendation using the **/travelguide add command**`, - }); - } - let currentIndex = 0; - const pages = Math.ceil(recommendations.length / 3); - - await interaction.reply({ - embeds: [generateGetEmbed(currentIndex, pages, recommendations)], - components: getComponents(currentIndex, recommendations.length), - }); - - // Creates a collector for button interaction events, setting a 120s maximum - // timeout and a 30s inactivity timeout - const filter = (resInteraction) => { - return ( - (resInteraction.customId === prevId || - resInteraction.customId === nextId || - resInteraction.customId.startsWith("like_")) && - resInteraction.user.id === authorId && - resInteraction.message.interaction.id === interaction.id - ); - }; - const collector = interaction.channel.createMessageComponentCollector({ - filter, - time: 120000, - idle: 30000, - }); - - collector.on("collect", async (i) => { - if (i.customId === prevId) { - currentIndex -= 3; - } else if (i.customId === nextId) { - currentIndex += 3; - } else if (i.customId.startsWith("like_")) { - const index = parseInt(i.customId.split("_")[1], 10); - const userIndex = recommendations[index].likes.indexOf(authorId); - if (userIndex === -1) { - // If the user hasn't liked it, increment the likes and add their ID to the likes array - recommendations[index].likes.push(authorId); - } else { - // If the user has already liked it, remove their ID from the likes array - recommendations[index].likes.splice(userIndex, 1); - } - updateFile(); - } - await i.update({ - embeds: [generateGetEmbed(currentIndex, pages, recommendations)], - components: getComponents(currentIndex, recommendations.length), - }); - }); - - // Clears buttons from embed page after timeout on collector - /*eslint-disable */ - collector.on("end", (collection) => { - interaction.editReply({ components: [] }); - }); - } else if (interaction.options.getSubcommand() === "delete") { - const userEntries = []; - userEntries.push(...guide.filter((entry) => entry.authorId === authorId)); - if (userEntries.length === 0) { - return await interaction.reply({ - content: `There are currently no recommendations for your deletion, add recommendations using the **/travelguide add command**`, - }); - } - // Generate an embed listing the user's entries - const userEntriesEmbed = new EmbedBuilder({ - title: `Your recommendations`, - description: "Below are your recommendations.", - color: 0x3a76f8, - author: { - name: "CSESoc Bot", - icon_url: "https://i.imgur.com/EE3Q40V.png", - }, - fields: userEntries.map((recommendation, index) => ({ - name: `${0 + index + 1}. ${recommendation.location}`, - value: `**Description**: ${recommendation.description} - **Season**: ${recommendation.season} - **Likes**: ${recommendation.likes.length}`, - })), - footer: { - text: "CSESoc Bot", - }, - timestamp: new Date(), - }); - - // Send the embed - await interaction.reply({ embeds: [userEntriesEmbed] }); - - // Prompt for entry index - await interaction.channel.send("Please provide the entry number to delete."); - - const collector = interaction.channel.createMessageCollector({ - filter: (message) => message.author.id === authorId, - max: 1, - time: 10_000, - }); - - collector.on("collect", async (message) => { - const entryIndex = parseInt(message.content.trim()); - if (isNaN(entryIndex) || entryIndex < 1 || entryIndex > userEntries.length) { - await interaction.followUp("Invalid entry number. No entry was deleted."); - return; - } - // Confirm entry - await interaction.channel.send( - `Type 'Y' to confirm the deletion of index **${message.content}**`, - ); - - const confirmCollector = interaction.channel.createMessageCollector({ - filter: (message) => message.author.id === authorId, - max: 1, - time: 10_000, - }); - - confirmCollector.on("collect", async (message) => { - const confirmMessage = message.content.trim(); - if (confirmMessage === "Y") { - // Delete the entry - const deletedEntry = userEntries[entryIndex - 1]; - guide.splice(guide.indexOf(deletedEntry), 1); - updateFile(); - - // Notify the user about the deletion - await interaction.followUp( - `Entry "${deletedEntry.location}" has been deleted.`, - ); - } else { - await interaction.followUp(`No entry has been deleted.`); - } - return; - }); - }); - - collector.on("end", (collected) => {}); - } else { - return await interaction.reply({ - content: "You do not have permission to execute this command.", - }); - } - }, + collector.on("end", (collected) => {}); +} + +module.exports = { + data: baseCommand, + execute: handleInteraction, }; diff --git a/config/travelguide.json b/config/travelguide.json deleted file mode 100644 index 10d71e20..00000000 --- a/config/travelguide.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "guide": [] -} \ No newline at end of file diff --git a/events/travelguide_ready.js b/events/travelguide_ready.js new file mode 100644 index 00000000..ec28b5ee --- /dev/null +++ b/events/travelguide_ready.js @@ -0,0 +1,12 @@ +// @ts-check +const { DBTravelguide } = require("../lib/database/dbtravelguide"); +/* eslint-disable */ + +module.exports = { + name: "ready", + once: true, + async execute() { + const travelguideStorage = new DBTravelguide(); + global.travelguideStorage = travelguideStorage; + }, +}; diff --git a/lib/database/dbtravelguide.js b/lib/database/dbtravelguide.js new file mode 100644 index 00000000..0807727d --- /dev/null +++ b/lib/database/dbtravelguide.js @@ -0,0 +1,280 @@ +const { Pool } = require("pg"); +const yaml = require("js-yaml"); +const fs = require("fs"); +const { type } = require("os"); + +class DBTravelguide { + constructor() { + // Loads the db configuration + const details = this.load_db_login(); + + this.pool = new Pool({ + user: details["user"], + password: details["password"], + host: details["host"], + port: details["port"], + database: details["dbname"], + }); + + const table_name = "travelguide"; + + // Creates the table if it doesn't exists + (async () => { + const is_check = await this.check_table(table_name); + if (is_check == false) { + await this.create_travelguide_table(); + } + })(); + } + + // Get document, or throw exception on error + load_db_login() { + try { + const doc = yaml.load(fs.readFileSync("./config/database.yml")); + return doc; + } catch (ex) { + console.log(`Something wrong happened in travelguide load_db_login ${ex}`); + } + } + + // Checks if the table exists in the db + async check_table(table_name) { + const client = await this.pool.connect(); + try { + await client.query("BEGIN"); + const values = [table_name]; + const result = await client.query( + "SELECT * FROM information_schema.tables WHERE table_name=$1", + values, + ); + await client.query("COMMIT"); + + if (result.rowCount == 0) { + return false; + } else { + return true; + } + } catch (ex) { + console.log(`Something wrong happened in travelguide check_table ${ex}`); + } finally { + await client.query("ROLLBACK"); + client.release(); + } + } + + // Creates a new table for travelguide messages + async create_travelguide_table() { + const client = await this.pool.connect(); + try { + await client.query("BEGIN"); + const query = `CREATE TABLE TRAVELGUIDE ( + REC_ID TEXT PRIMARY KEY, + LOCATION TEXT NOT NULL, + DESCRIPTION TEXT NOT NULL, + SEASON TEXT, + CATEGORY TEXT NOT NULL, + LIKES NUMERIC[], + AUTHOR_ID NUMERIC NOT NULL, + DATE_ADDED TIMESTAMP DEFAULT CURRENT_TIMESTAMP + )`; + await client.query(query); + await client.query("COMMIT"); + } catch (ex) { + console.log(`Something wrong happened in travelguide create_travelguide_table${ex}`); + } finally { + await client.query("ROLLBACK"); + client.release(); + } + } + + /** + * Adds new recommendation to the db + * @param {String} recommendationId + * @param {String} location + * @param {String} description + * @param {String} season + * @param {String} category + * @param {Number} authorId + */ + async addRecommendation(recommendationId, location, description, season, category, authorId) { + const client = await this.pool.connect(); + try { + await client.query("BEGIN"); + + const query = `INSERT INTO TRAVELGUIDE (REC_ID, LOCATION, DESCRIPTION, SEASON, + CATEGORY, LIKES, AUTHOR_ID) VALUES ($1,$2,$3,$4,$5,$6,$7)`; + const likes = []; + const values = [ + recommendationId, + location, + description, + season, + category, + likes, + authorId, + ]; + await client.query(query, values); + await client.query("COMMIT"); + } catch (ex) { + console.log(`Something wrong happend in travelguide addRecommendation ${ex}`); + } finally { + await client.query("ROLLBACK"); + client.release(); + } + } + + /** + * Adds/removes a userid to/from a recommendation's 'likes' + * @param {Number} userId + * @param {String} recommendationId + */ + async likeRecommendation(userId, recommendationId) { + const client = await this.pool.connect(); + try { + await client.query("BEGIN"); + + const checkQuery = `SELECT LIKES FROM TRAVELGUIDE WHERE REC_ID = $1`; + const checkValues = [recommendationId]; + const result = await client.query(checkQuery, checkValues); + + if (result.rows.length > 0) { + const likesArray = result.rows[0].likes; + let updateQuery; + const updateValues = [userId, recommendationId]; + + if (likesArray.includes(parseInt(userId))) { + // Remove the userId from the likes array + updateQuery = `UPDATE TRAVELGUIDE SET LIKES = array_remove(LIKES, $1) WHERE REC_ID = $2`; + } else { + // Add the userId to the likes array + updateQuery = `UPDATE TRAVELGUIDE SET LIKES = array_append(LIKES, $1) WHERE REC_ID = $2`; + } + await client.query(updateQuery, updateValues); + await client.query("COMMIT"); + } + } catch (ex) { + console.log(`Something wrong happend in travelguide likeRecommendation${ex}`); + } finally { + await client.query("ROLLBACK"); + client.release(); + } + } + + /** + * Deletes a recommendation from the db + * @param {Number} authorId + * @param {String} recommendationId + */ + async deleteRecommendation(authorId, recommendationId) { + const client = await this.pool.connect(); + try { + await client.query("BEGIN"); + const values = [authorId, recommendationId]; + const query = `DELETE FROM TRAVELGUIDE WHERE AUTHOR_ID=$1 AND REC_ID=$2`; + + await client.query(query, values); + await client.query("COMMIT"); + console.log("in delete"); + } catch (ex) { + console.log(`Something wrong happened in travelguide deleteRecommendation ${ex}`); + } finally { + await client.query("ROLLBACK"); + client.release(); + } + } + + /** + * Gets all the recommendations from an author + * @param {Number} authorid + * @returns Recommendations from given author + */ + async getAuthorRecommendations(authorId) { + const client = await this.pool.connect(); + try { + await client.query("BEGIN"); + const values = [authorId]; + let result = await client.query("SELECT * FROM TRAVELGUIDE WHERE AUTHOR_ID=$1", values); + await client.query("COMMIT"); + return result.rows; + } catch (ex) { + console.log(`Something wrong happend in travelguide getAuthorRecommendations${ex}`); + } finally { + await client.query("ROLLBACK"); + client.release(); + } + } + + /** + * Gets recommendations from category/season + * @param {String} category + * @param {String} season + * @returns + */ + async getRecommendations(category, season) { + const client = await this.pool.connect(); + try { + await client.query("BEGIN"); + let query = "SELECT * FROM TRAVELGUIDE"; + + const values = []; + let valueIndex = 1; + + if (category || season) { + query += " WHERE"; + if (category) { + query += ` CATEGORY=$${valueIndex}`; + values.push(category); + valueIndex++; + } + if (season) { + if (category) { + query += " AND"; + } + query += ` SEASON=$${valueIndex}`; + values.push(season); + } + } + query += " ORDER BY DATE_ADDED"; + + const result = await client.query(query, values); + await client.query("COMMIT"); + return result.rows; + } catch (ex) { + console.log(`Something wrong happened in travelguide - getRecommendations ${ex}`); + } finally { + await client.query("ROLLBACK"); + client.release(); + } + } + + /** + * Gets a recommendation from location, description and category + * @param {String} location + * @param {String} description + * @param {String} category + * @returns row containing the recommendation + */ + async getRecommendation(location, description, category) { + const client = await this.pool.connect(); + try { + await client.query("BEGIN"); + const values = [location, description, category]; + const result = await client.query( + "SELECT * FROM TRAVELGUIDE WHERE LOCATION=$1 AND DESCRIPTION=$2 AND CATEGORY=$3", + values, + ); + await client.query("COMMIT"); + + return result.rows; + } catch (ex) { + console.log(`Something wrong happend in travelguide getRecommendation ${ex}`); + } finally { + await client.query("ROLLBACK"); + client.release(); + } + } +} + +module.exports = { + DBTravelguide, +}; From aedf5957814c0c327ea4c4b5df520484ecb2b747 Mon Sep 17 00:00:00 2001 From: Chloe Date: Thu, 27 Jun 2024 18:38:02 +1000 Subject: [PATCH 11/12] linting --- commands/travelguide.js | 1 - lib/database/dbtravelguide.js | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/commands/travelguide.js b/commands/travelguide.js index 1cd00d02..9afbc4a4 100644 --- a/commands/travelguide.js +++ b/commands/travelguide.js @@ -1,6 +1,5 @@ const { SlashCommandBuilder, SlashCommandSubcommandBuilder } = require("@discordjs/builders"); const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require("discord.js"); -const { DBTravelguide } = require("../lib/database/dbtravelguide"); const { v4: uuidv4 } = require("uuid"); // //////////////////////////////////////////// diff --git a/lib/database/dbtravelguide.js b/lib/database/dbtravelguide.js index 0807727d..00d07985 100644 --- a/lib/database/dbtravelguide.js +++ b/lib/database/dbtravelguide.js @@ -1,7 +1,6 @@ const { Pool } = require("pg"); const yaml = require("js-yaml"); const fs = require("fs"); -const { type } = require("os"); class DBTravelguide { constructor() { @@ -193,7 +192,10 @@ class DBTravelguide { try { await client.query("BEGIN"); const values = [authorId]; - let result = await client.query("SELECT * FROM TRAVELGUIDE WHERE AUTHOR_ID=$1", values); + let result = await client.query( + "SELECT * FROM TRAVELGUIDE WHERE AUTHOR_ID=$1 ORDER BY DATE_ADDED", + values, + ); await client.query("COMMIT"); return result.rows; } catch (ex) { From 46b156d06259edbecd4b93e05ee0411218c427b0 Mon Sep 17 00:00:00 2001 From: Chloe Date: Thu, 27 Jun 2024 18:41:04 +1000 Subject: [PATCH 12/12] linting --- lib/database/dbtravelguide.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/database/dbtravelguide.js b/lib/database/dbtravelguide.js index 00d07985..15e87e69 100644 --- a/lib/database/dbtravelguide.js +++ b/lib/database/dbtravelguide.js @@ -192,7 +192,7 @@ class DBTravelguide { try { await client.query("BEGIN"); const values = [authorId]; - let result = await client.query( + const result = await client.query( "SELECT * FROM TRAVELGUIDE WHERE AUTHOR_ID=$1 ORDER BY DATE_ADDED", values, );