Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 17e93d0

Browse files
committedJun 27, 2024·
migrating travelguide.json to postgres
1 parent d626433 commit 17e93d0

File tree

4 files changed

+622
-300
lines changed

4 files changed

+622
-300
lines changed
 

‎commands/travelguide.js

Lines changed: 330 additions & 297 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
const { SlashCommandBuilder } = require("@discordjs/builders");
1+
const { SlashCommandBuilder, SlashCommandSubcommandBuilder } = require("@discordjs/builders");
22
const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require("discord.js");
3-
const fs = require("fs");
4-
const { guide } = require("../config/travelguide.json");
3+
const { DBTravelguide } = require("../lib/database/dbtravelguide");
4+
const { v4: uuidv4 } = require("uuid");
5+
6+
// ////////////////////////////////////////////
7+
// //// GENERATE EMBEDS AND ACTION ROW ////////
8+
// ////////////////////////////////////////////
59

6-
// Creates general object and id constants for function use
710
const prevId = "travelguidePrevButtonId";
811
const nextId = "travelguideNextButtonId";
912

@@ -19,6 +22,12 @@
1922
.setStyle(ButtonStyle.Secondary)
2023
.setEmoji("➡️");
2124

25+
/**
26+
*
27+
* @param {Number} currentIndex
28+
* @param {Number} numEntries
29+
* @returns like buttons for an embed
30+
*/
2231
const generateLikeButtons = (currentIndex, numEntries) => {
2332
const buttons = [];
2433
const endIndex = Math.min(currentIndex + 3, numEntries);
@@ -37,12 +46,12 @@
3746
};
3847

3948
/**
40-
* Creates an actionRowBuilder with next and previous buttons
49+
* Creates an ActionRow with all buttons
4150
* @param {number} currentIndex
4251
* @param {number} numEntries
43-
* @returns
52+
* @returns {ActionRowBuilder} ActionRow containing all buttons for an embed
4453
*/
45-
const getComponents = (currentIndex, numEntries) => {
54+
const generateActionRow = (currentIndex, numEntries) => {
4655
const buttons = [
4756
...(currentIndex > 0 ? [prevButton] : []),
4857
...(numEntries - (currentIndex + 3) > 0 ? [nextButton] : []),
@@ -61,9 +70,8 @@
6170
* @param {number} start The index to start from.
6271
* @param {number} pages How many pages the embed has.
6372
* @param {Array<String>} recommendations An array of recommendations.
64-
* @returns {EmbedBuilder}
73+
* @returns {EmbedBuilder} Embed containing 3 recommendations for the travelguide GET command
6574
*/
66-
6775
const generateGetEmbed = (start, pages, recommendations) => {
6876
const current = recommendations.slice(start, start + 3);
6977
const pageNum = Math.floor(start / pages) + 1;
@@ -89,20 +97,23 @@
8997
};
9098

9199
/**
92-
* Creates an embed of the added recommendation
93-
* @param {object} recommendation The recommendation
100+
*
101+
* @param {String} location
102+
* @param {String} description
103+
* @param {String} season
104+
* @param {String} category
105+
* @returns an embed containing a summary of the newly added recommendation
94106
*/
95-
96-
const generateAddEmbed = (recommendation, category) => {
107+
const generateAddEmbed = (location, description, season, category) => {
97108
return new EmbedBuilder()
98109
.setAuthor({
99110
name: "CSESoc Bot",
100111
iconURL: "https://i.imgur.com/EE3Q40V.png",
101112
})
102-
.setTitle(`${recommendation.location} has been added!`)
113+
.setTitle(`${location} has been added!`)
103114
.setDescription(
104-
`**Description**: ${recommendation.description}
105-
**Season**: ${recommendation.season}
115+
`**Description**: ${description}
116+
**Season**: ${season}
106117
**Category**: ${category}`,
107118
)
108119
.setColor(0x3a76f8)
@@ -112,291 +123,313 @@
112123
.setTimestamp();
113124
};
114125

126+
// ////////////////////////////////////////////
127+
// //////// SETTING UP THE COMMANDS ///////////
128+
// ////////////////////////////////////////////
129+
130+
const commandTravelguideAdd = new SlashCommandSubcommandBuilder()
131+
.setName("add")
132+
.setDescription(
133+
"Add a recommendation to the travel guide. Then the recommendation will be considered for approval.",
134+
)
135+
// [recommendation name] [category] [description] [season optional] [recommender?]
136+
.addStringOption((option) =>
137+
option
138+
.setName("recommendation-location")
139+
.setDescription("Name of the recommended place.")
140+
.setRequired(true),
141+
)
142+
.addStringOption((option) =>
143+
option
144+
.setName("category")
145+
.setDescription(
146+
"The recommended category out of: entertainment, scenic views and restaurants.",
147+
)
148+
.setRequired(true)
149+
.addChoices(
150+
{ name: "entertainment", value: "entertainment" },
151+
{ name: "scenic views", value: "scenic views" },
152+
{ name: "restaurants", value: "restaurants" },
153+
),
154+
)
155+
.addStringOption((option) =>
156+
option
157+
.setName("description")
158+
.setDescription("Brief description of the recommended place in 1-2 sentences.")
159+
.setRequired(true),
160+
)
161+
.addStringOption((option) =>
162+
option
163+
.setName("season")
164+
.setDescription("The recommended season for the location.")
165+
.setRequired(true)
166+
.addChoices(
167+
{ name: "summer", value: "summer" },
168+
{ name: "autumn", value: "autumn" },
169+
{ name: "winter", value: "winter" },
170+
{ name: "spring", value: "spring" },
171+
{ name: "all year round", value: "all year round" },
172+
),
173+
);
174+
175+
const commandTravelguideGet = new SlashCommandSubcommandBuilder()
176+
.setName("get")
177+
.setDescription("Get a recommendation from the travel guide")
178+
.addStringOption((option) =>
179+
option
180+
.setName("category")
181+
.setDescription("Sort by the following category")
182+
.setRequired(false)
183+
.addChoices(
184+
{ name: "entertainment", value: "entertainment" },
185+
{ name: "scenic views", value: "scenic views" },
186+
{ name: "restaurants", value: "restaurants" },
187+
),
188+
)
189+
.addStringOption((option) =>
190+
option
191+
.setName("season")
192+
.setDescription("Sort by the following season")
193+
.setRequired(false)
194+
.addChoices(
195+
{ name: "summer", value: "summer" },
196+
{ name: "autumn", value: "autumn" },
197+
{ name: "winter", value: "winter" },
198+
{ name: "spring", value: "spring" },
199+
{ name: "all year round", value: "all year round" },
200+
),
201+
);
202+
203+
const commandTravelguideDelete = new SlashCommandSubcommandBuilder()
204+
.setName("delete")
205+
.setDescription("Delete your own recommendation from the travel guide.");
206+
207+
const baseCommand = new SlashCommandBuilder()
208+
.setName("travelguide")
209+
.setDescription(
210+
"Add to and display a travel guide for the recommended restuarants, scenic views and more!",
211+
)
212+
.addSubcommand(commandTravelguideAdd)
213+
.addSubcommand(commandTravelguideGet)
214+
.addSubcommand(commandTravelguideDelete);
215+
216+
// ////////////////////////////////////////////
217+
// ///////// HANDLING THE COMMAND /////////////
218+
// ////////////////////////////////////////////
219+
115220
/**
116-
* Updates the travelguide.json database
221+
*
222+
* @param {CommandInteraction} interaction
117223
*/
118-
const updateFile = () => {
119-
fs.writeFileSync("./config/travelguide.json", JSON.stringify({ guide }, null, 4));
120-
};
224+
async function handleInteraction(interaction) {
225+
/** @type {DBTravelguide} */
226+
const travelguideStorage = global.travelguideStorage;
227+
const authorId = interaction.user.id;
228+
229+
// figure out which command was called
230+
const subcommand = interaction.options.getSubcommand(false);
231+
switch (subcommand) {
232+
case "add":
233+
await handleTravelguideAdd(interaction, travelguideStorage, authorId);
234+
break;
235+
case "get":
236+
await handleTravelguideGet(interaction, travelguideStorage, authorId);
237+
break;
238+
case "delete":
239+
await handleTravelguideDelete(interaction, travelguideStorage, authorId);
240+
break;
241+
default:
242+
await interaction.reply("Internal Error AHHHHHHH! CONTACT ME PLEASE!");
243+
}
244+
}
121245

122-
module.exports = {
123-
data: new SlashCommandBuilder()
124-
.setName("travelguide")
125-
.setDescription(
126-
"Add to and display a travel guide for the recommended restuarants, scenic views and more!",
127-
)
128-
.addSubcommand((subcommand) =>
129-
subcommand
130-
.setName("add")
131-
.setDescription(
132-
"Add a recommendation to the travel guide. Then the recommendation will be considered for approval.",
133-
)
134-
// [recommendation name] [category] [description] [season optional] [recommender?]
135-
.addStringOption((option) =>
136-
option
137-
.setName("recommendation-location")
138-
.setDescription("Name of the recommended place.")
139-
.setRequired(true),
140-
)
141-
.addStringOption((option) =>
142-
option
143-
.setName("category")
144-
.setDescription(
145-
"The recommended category out of: entertainment, scenic views and restaurants.",
146-
)
147-
.setRequired(true)
148-
.addChoices(
149-
{ name: "entertainment", value: "entertainment" },
150-
{ name: "scenic views", value: "scenic views" },
151-
{ name: "restaurants", value: "restaurants" },
152-
),
153-
)
154-
.addStringOption((option) =>
155-
option
156-
.setName("description")
157-
.setDescription(
158-
"Brief description of the recommended place in 1-2 sentences.",
159-
)
160-
.setRequired(true),
161-
)
162-
.addStringOption((option) =>
163-
option
164-
.setName("season")
165-
.setDescription("The recommended season for the location.")
166-
.setRequired(true)
167-
.addChoices(
168-
{ name: "summer", value: "summer" },
169-
{ name: "autumn", value: "autumn" },
170-
{ name: "winter", value: "winter" },
171-
{ name: "spring", value: "spring" },
172-
{ name: "all year round", value: "all year round" },
173-
),
174-
),
175-
)
176-
.addSubcommand((subcommand) =>
177-
subcommand
178-
.setName("get")
179-
.setDescription("Get a recommendation from the travel guide")
180-
.addStringOption((option) =>
181-
option
182-
.setName("category")
183-
.setDescription("Sort by the following category")
184-
.setRequired(false)
185-
.addChoices(
186-
{ name: "entertainment", value: "entertainment" },
187-
{ name: "scenic views", value: "scenic views" },
188-
{ name: "restaurants", value: "restaurants" },
189-
),
190-
)
191-
.addStringOption((option) =>
192-
option
193-
.setName("season")
194-
.setDescription("Sort by the following season")
195-
.setRequired(false)
196-
.addChoices(
197-
{ name: "summer", value: "summer" },
198-
{ name: "autumn", value: "autumn" },
199-
{ name: "winter", value: "winter" },
200-
{ name: "spring", value: "spring" },
201-
{ name: "all year round", value: "all year round" },
202-
),
203-
),
204-
)
205-
.addSubcommand((subcommand) =>
206-
subcommand
207-
.setName("delete")
208-
.setDescription("Delete your own recommendation from the travel guide."),
209-
),
210-
211-
async execute(interaction) {
212-
const authorId = interaction.user.id;
213-
214-
if (interaction.options.getSubcommand() === "add") {
215-
const category = interaction.options.getString("category");
216-
const season = interaction.options.getString("season");
217-
const location = interaction.options.getString("recommendation-location");
218-
const description = interaction.options.getString("description");
219-
220-
const recommendation = {
221-
location: location,
222-
description: description,
223-
season: season ? season : null,
224-
category: category,
225-
likes: [],
226-
authorId: authorId,
227-
dateAdded: Date.now(),
228-
};
229-
const exists = guide.some(
230-
(item) =>
231-
item.location === recommendation.location &&
232-
item.description === recommendation.description &&
233-
item.season === recommendation.season &&
234-
item.category === recommendation.category,
235-
);
236-
237-
if (!exists) {
238-
guide.push(recommendation);
246+
// ////////////////////////////////////////////
247+
// //////// HANDLING THE SUBCOMMANDS //////////
248+
// ////////////////////////////////////////////
249+
250+
/**
251+
* Adds a new recommendation to the database and displays a summary of the recommendation
252+
* @param {CommandInteraction} interaction
253+
* @param {DBTravelguide} travelguideStorage
254+
* @param {Number} authorId
255+
*/
256+
async function handleTravelguideAdd(interaction, travelguideStorage, authorId) {
257+
const location = interaction.options.getString("recommendation-location");
258+
const description = interaction.options.getString("description");
259+
const season = interaction.options.getString("season");
260+
const category = interaction.options.getString("category");
261+
262+
// check if entry exists in db
263+
const exists = await travelguideStorage.getRecommendation(location, description, category);
264+
if (exists.length === 0) {
265+
travelguideStorage.addRecommendation(
266+
uuidv4(),
267+
location,
268+
description,
269+
season,
270+
category,
271+
authorId,
272+
);
273+
} else {
274+
return await interaction.reply({
275+
content: "This entry has already been recommended before.",
276+
});
277+
}
278+
279+
return await interaction.reply({
280+
embeds: [generateAddEmbed(location, description, season, category)],
281+
});
282+
}
283+
284+
/**
285+
* Gets 3 recommendations sorted by category/season/neither
286+
* @param {CommandInteraction} interaction
287+
* @param {DBTravelguide} travelguideStorage
288+
* @param {Number} authorId
289+
*/
290+
async function handleTravelguideGet(interaction, travelguideStorage, authorId) {
291+
const category = interaction.options.getString("category");
292+
const season = interaction.options.getString("season");
293+
let recommendations = await travelguideStorage.getRecommendations(category, season);
294+
if (recommendations.length === 0) {
295+
return await interaction.reply({
296+
content: `There are currently no recommendations for your selection, add your own recommendation using the **/travelguide add command**`,
297+
});
298+
}
299+
let currentIndex = 0;
300+
const pages = Math.ceil(recommendations.length / 3);
301+
302+
await interaction.reply({
303+
embeds: [generateGetEmbed(currentIndex, pages, recommendations)],
304+
components: generateActionRow(currentIndex, recommendations.length),
305+
});
306+
307+
// Creates a collector for button interaction events, setting a 120s maximum
308+
// timeout and a 30s inactivity timeout
309+
const filter = (resInteraction) => {
310+
return (
311+
(resInteraction.customId === prevId ||
312+
resInteraction.customId === nextId ||
313+
resInteraction.customId.startsWith("like_")) &&
314+
resInteraction.user.id === authorId &&
315+
resInteraction.message.interaction.id === interaction.id
316+
);
317+
};
318+
const collector = interaction.channel.createMessageComponentCollector({
319+
filter,
320+
time: 120000,
321+
idle: 30000,
322+
});
323+
324+
collector.on("collect", async (i) => {
325+
if (i.customId === prevId) {
326+
currentIndex -= 3;
327+
} else if (i.customId === nextId) {
328+
currentIndex += 3;
329+
} else if (i.customId.startsWith("like_")) {
330+
const index = parseInt(i.customId.split("_")[1], 10);
331+
const recId = recommendations[index].rec_id;
332+
333+
await travelguideStorage.likeRecommendation(authorId, recId);
334+
recommendations = await travelguideStorage.getRecommendations(category, season);
335+
}
336+
337+
await i.update({
338+
embeds: [generateGetEmbed(currentIndex, pages, recommendations)],
339+
components: generateActionRow(currentIndex, recommendations.length),
340+
});
341+
});
342+
343+
// Clears buttons from embed page after timeout on collector
344+
/*eslint-disable */
345+
collector.on("end", (collection) => {
346+
interaction.editReply({ components: [] });
347+
});
348+
}
349+
350+
/**
351+
* Deletes a recommendation that the user owns
352+
* @param {CommandInteraction} interaction
353+
* @param {DBTravelguide} travelguideStorage
354+
* @param {Number} authorId
355+
*/
356+
async function handleTravelguideDelete(interaction, travelguideStorage, authorId) {
357+
const authorEntries = await travelguideStorage.getAuthorRecommendations(authorId);
358+
if (authorEntries.length === 0) {
359+
return await interaction.reply({
360+
content: `There are currently no recommendations for your deletion, add recommendations using the **/travelguide add command**`,
361+
});
362+
}
363+
// Generate an embed listing the user's entries
364+
const authorEntriesEmbed = new EmbedBuilder({
365+
title: `Your recommendations`,
366+
color: 0x3a76f8,
367+
author: {
368+
name: "CSESoc Bot",
369+
icon_url: "https://i.imgur.com/EE3Q40V.png",
370+
},
371+
fields: authorEntries.map((recommendation, index) => ({
372+
name: `${0 + index + 1}. ${recommendation.location}`,
373+
value: `**Description**: ${recommendation.description}
374+
**Season**: ${recommendation.season}
375+
**Likes**: ${recommendation.likes.length}`,
376+
})),
377+
footer: {
378+
text: "CSESoc Bot",
379+
},
380+
timestamp: new Date(),
381+
});
382+
383+
// Send the embed
384+
await interaction.reply({ embeds: [authorEntriesEmbed] });
385+
386+
// Prompt for entry index
387+
await interaction.channel.send("Please provide the entry number to delete.");
388+
389+
const collector = interaction.channel.createMessageCollector({
390+
filter: (message) => message.author.id === authorId,
391+
max: 1,
392+
time: 10_000,
393+
});
394+
395+
collector.on("collect", async (message) => {
396+
const entryIndex = parseInt(message.content.trim());
397+
if (isNaN(entryIndex) || entryIndex < 1 || entryIndex > authorEntries.length) {
398+
await interaction.followUp("Invalid entry number. No entry was deleted.");
399+
return;
400+
}
401+
// Confirm entry
402+
await interaction.channel.send(
403+
`Type 'Y' to confirm the deletion of index **${message.content}**`,
404+
);
405+
406+
const confirmCollector = interaction.channel.createMessageCollector({
407+
filter: (message) => message.author.id === authorId,
408+
max: 1,
409+
time: 10_000,
410+
});
411+
412+
confirmCollector.on("collect", async (message) => {
413+
const confirmMessage = message.content.trim();
414+
if (confirmMessage === "Y") {
415+
// get the recommendationId
416+
const recId = authorEntries[entryIndex - 1].rec_id;
417+
const deletedLocation = authorEntries[entryIndex - 1].location;
418+
// Delete the entry
419+
await travelguideStorage.deleteRecommendation(authorId, recId);
420+
// Notify the user about the deletion
421+
await interaction.followUp(`Entry "${deletedLocation}" has been deleted.`);
239422
} else {
240-
return await interaction.reply({
241-
content: "This entry has already been recommended before.",
242-
});
423+
await interaction.followUp(`No entry has been deleted.`);
243424
}
425+
return;
426+
});
427+
});
244428

245-
updateFile();
246-
247-
return await interaction.reply({
248-
embeds: [generateAddEmbed(recommendation, category)],
249-
});
250-
} else if (interaction.options.getSubcommand() === "get") {
251-
const category = interaction.options.getString("category");
252-
const season = interaction.options.getString("season");
253-
let recommendations = guide;
254-
if (category) {
255-
recommendations = recommendations.filter((entry) => entry.category === category);
256-
}
257-
if (season) {
258-
recommendations = recommendations.filter((entry) => entry.season === season);
259-
}
260-
if (recommendations.length === 0) {
261-
return await interaction.reply({
262-
content: `There are currently no recommendations for your selection, add your own recommendation using the **/travelguide add command**`,
263-
});
264-
}
265-
let currentIndex = 0;
266-
const pages = Math.ceil(recommendations.length / 3);
267-
268-
await interaction.reply({
269-
embeds: [generateGetEmbed(currentIndex, pages, recommendations)],
270-
components: getComponents(currentIndex, recommendations.length),
271-
});
272-
273-
// Creates a collector for button interaction events, setting a 120s maximum
274-
// timeout and a 30s inactivity timeout
275-
const filter = (resInteraction) => {
276-
return (
277-
(resInteraction.customId === prevId ||
278-
resInteraction.customId === nextId ||
279-
resInteraction.customId.startsWith("like_")) &&
280-
resInteraction.user.id === authorId &&
281-
resInteraction.message.interaction.id === interaction.id
282-
);
283-
};
284-
const collector = interaction.channel.createMessageComponentCollector({
285-
filter,
286-
time: 120000,
287-
idle: 30000,
288-
});
289-
290-
collector.on("collect", async (i) => {
291-
if (i.customId === prevId) {
292-
currentIndex -= 3;
293-
} else if (i.customId === nextId) {
294-
currentIndex += 3;
295-
} else if (i.customId.startsWith("like_")) {
296-
const index = parseInt(i.customId.split("_")[1], 10);
297-
const userIndex = recommendations[index].likes.indexOf(authorId);
298-
if (userIndex === -1) {
299-
// If the user hasn't liked it, increment the likes and add their ID to the likes array
300-
recommendations[index].likes.push(authorId);
301-
} else {
302-
// If the user has already liked it, remove their ID from the likes array
303-
recommendations[index].likes.splice(userIndex, 1);
304-
}
305-
updateFile();
306-
}
307-
await i.update({
308-
embeds: [generateGetEmbed(currentIndex, pages, recommendations)],
309-
components: getComponents(currentIndex, recommendations.length),
310-
});
311-
});
312-
313-
// Clears buttons from embed page after timeout on collector
314-
/*eslint-disable */
315-
collector.on("end", (collection) => {
316-
interaction.editReply({ components: [] });
317-
});
318-
} else if (interaction.options.getSubcommand() === "delete") {
319-
const userEntries = [];
320-
userEntries.push(...guide.filter((entry) => entry.authorId === authorId));
321-
if (userEntries.length === 0) {
322-
return await interaction.reply({
323-
content: `There are currently no recommendations for your deletion, add recommendations using the **/travelguide add command**`,
324-
});
325-
}
326-
// Generate an embed listing the user's entries
327-
const userEntriesEmbed = new EmbedBuilder({
328-
title: `Your recommendations`,
329-
description: "Below are your recommendations.",
330-
color: 0x3a76f8,
331-
author: {
332-
name: "CSESoc Bot",
333-
icon_url: "https://i.imgur.com/EE3Q40V.png",
334-
},
335-
fields: userEntries.map((recommendation, index) => ({
336-
name: `${0 + index + 1}. ${recommendation.location}`,
337-
value: `**Description**: ${recommendation.description}
338-
**Season**: ${recommendation.season}
339-
**Likes**: ${recommendation.likes.length}`,
340-
})),
341-
footer: {
342-
text: "CSESoc Bot",
343-
},
344-
timestamp: new Date(),
345-
});
346-
347-
// Send the embed
348-
await interaction.reply({ embeds: [userEntriesEmbed] });
349-
350-
// Prompt for entry index
351-
await interaction.channel.send("Please provide the entry number to delete.");
352-
353-
const collector = interaction.channel.createMessageCollector({
354-
filter: (message) => message.author.id === authorId,
355-
max: 1,
356-
time: 10_000,
357-
});
358-
359-
collector.on("collect", async (message) => {
360-
const entryIndex = parseInt(message.content.trim());
361-
if (isNaN(entryIndex) || entryIndex < 1 || entryIndex > userEntries.length) {
362-
await interaction.followUp("Invalid entry number. No entry was deleted.");
363-
return;
364-
}
365-
// Confirm entry
366-
await interaction.channel.send(
367-
`Type 'Y' to confirm the deletion of index **${message.content}**`,
368-
);
369-
370-
const confirmCollector = interaction.channel.createMessageCollector({
371-
filter: (message) => message.author.id === authorId,
372-
max: 1,
373-
time: 10_000,
374-
});
375-
376-
confirmCollector.on("collect", async (message) => {
377-
const confirmMessage = message.content.trim();
378-
if (confirmMessage === "Y") {
379-
// Delete the entry
380-
const deletedEntry = userEntries[entryIndex - 1];
381-
guide.splice(guide.indexOf(deletedEntry), 1);
382-
updateFile();
383-
384-
// Notify the user about the deletion
385-
await interaction.followUp(
386-
`Entry "${deletedEntry.location}" has been deleted.`,
387-
);
388-
} else {
389-
await interaction.followUp(`No entry has been deleted.`);
390-
}
391-
return;
392-
});
393-
});
394-
395-
collector.on("end", (collected) => {});
396-
} else {
397-
return await interaction.reply({
398-
content: "You do not have permission to execute this command.",
399-
});
400-
}
401-
},
429+
collector.on("end", (collected) => {});
430+
}
431+
432+
module.exports = {
433+
data: baseCommand,
434+
execute: handleInteraction,
402435
};

‎config/travelguide.json

Lines changed: 0 additions & 3 deletions
This file was deleted.

‎events/travelguide_ready.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// @ts-check
2+
const { DBTravelguide } = require("../lib/database/dbtravelguide");
3+
/* eslint-disable */
4+
5+
module.exports = {
6+
name: "ready",
7+
once: true,
8+
async execute() {
9+
const travelguideStorage = new DBTravelguide();
10+
global.travelguideStorage = travelguideStorage;
11+
},
12+
};

‎lib/database/dbtravelguide.js

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
const { Pool } = require("pg");
2+
const yaml = require("js-yaml");
3+
const fs = require("fs");
4+
const { type } = require("os");
5+
6+
class DBTravelguide {
7+
constructor() {
8+
// Loads the db configuration
9+
const details = this.load_db_login();
10+
11+
this.pool = new Pool({
12+
user: details["user"],
13+
password: details["password"],
14+
host: details["host"],
15+
port: details["port"],
16+
database: details["dbname"],
17+
});
18+
19+
const table_name = "travelguide";
20+
21+
// Creates the table if it doesn't exists
22+
(async () => {
23+
const is_check = await this.check_table(table_name);
24+
if (is_check == false) {
25+
await this.create_travelguide_table();
26+
}
27+
})();
28+
}
29+
30+
// Get document, or throw exception on error
31+
load_db_login() {
32+
try {
33+
const doc = yaml.load(fs.readFileSync("./config/database.yml"));
34+
return doc;
35+
} catch (ex) {
36+
console.log(`Something wrong happened in travelguide load_db_login ${ex}`);
37+
}
38+
}
39+
40+
// Checks if the table exists in the db
41+
async check_table(table_name) {
42+
const client = await this.pool.connect();
43+
try {
44+
await client.query("BEGIN");
45+
const values = [table_name];
46+
const result = await client.query(
47+
"SELECT * FROM information_schema.tables WHERE table_name=$1",
48+
values,
49+
);
50+
await client.query("COMMIT");
51+
52+
if (result.rowCount == 0) {
53+
return false;
54+
} else {
55+
return true;
56+
}
57+
} catch (ex) {
58+
console.log(`Something wrong happened in travelguide check_table ${ex}`);
59+
} finally {
60+
await client.query("ROLLBACK");
61+
client.release();
62+
}
63+
}
64+
65+
// Creates a new table for travelguide messages
66+
async create_travelguide_table() {
67+
const client = await this.pool.connect();
68+
try {
69+
await client.query("BEGIN");
70+
const query = `CREATE TABLE TRAVELGUIDE (
71+
REC_ID TEXT PRIMARY KEY,
72+
LOCATION TEXT NOT NULL,
73+
DESCRIPTION TEXT NOT NULL,
74+
SEASON TEXT,
75+
CATEGORY TEXT NOT NULL,
76+
LIKES NUMERIC[],
77+
AUTHOR_ID NUMERIC NOT NULL,
78+
DATE_ADDED TIMESTAMP DEFAULT CURRENT_TIMESTAMP
79+
)`;
80+
await client.query(query);
81+
await client.query("COMMIT");
82+
} catch (ex) {
83+
console.log(`Something wrong happened in travelguide create_travelguide_table${ex}`);
84+
} finally {
85+
await client.query("ROLLBACK");
86+
client.release();
87+
}
88+
}
89+
90+
/**
91+
* Adds new recommendation to the db
92+
* @param {String} recommendationId
93+
* @param {String} location
94+
* @param {String} description
95+
* @param {String} season
96+
* @param {String} category
97+
* @param {Number} authorId
98+
*/
99+
async addRecommendation(recommendationId, location, description, season, category, authorId) {
100+
const client = await this.pool.connect();
101+
try {
102+
await client.query("BEGIN");
103+
104+
const query = `INSERT INTO TRAVELGUIDE (REC_ID, LOCATION, DESCRIPTION, SEASON,
105+
CATEGORY, LIKES, AUTHOR_ID) VALUES ($1,$2,$3,$4,$5,$6,$7)`;
106+
const likes = [];
107+
const values = [
108+
recommendationId,
109+
location,
110+
description,
111+
season,
112+
category,
113+
likes,
114+
authorId,
115+
];
116+
await client.query(query, values);
117+
await client.query("COMMIT");
118+
} catch (ex) {
119+
console.log(`Something wrong happend in travelguide addRecommendation ${ex}`);
120+
} finally {
121+
await client.query("ROLLBACK");
122+
client.release();
123+
}
124+
}
125+
126+
/**
127+
* Adds/removes a userid to/from a recommendation's 'likes'
128+
* @param {Number} userId
129+
* @param {String} recommendationId
130+
*/
131+
async likeRecommendation(userId, recommendationId) {
132+
const client = await this.pool.connect();
133+
try {
134+
await client.query("BEGIN");
135+
136+
const checkQuery = `SELECT LIKES FROM TRAVELGUIDE WHERE REC_ID = $1`;
137+
const checkValues = [recommendationId];
138+
const result = await client.query(checkQuery, checkValues);
139+
140+
if (result.rows.length > 0) {
141+
const likesArray = result.rows[0].likes;
142+
let updateQuery;
143+
const updateValues = [userId, recommendationId];
144+
145+
if (likesArray.includes(parseInt(userId))) {
146+
// Remove the userId from the likes array
147+
updateQuery = `UPDATE TRAVELGUIDE SET LIKES = array_remove(LIKES, $1) WHERE REC_ID = $2`;
148+
} else {
149+
// Add the userId to the likes array
150+
updateQuery = `UPDATE TRAVELGUIDE SET LIKES = array_append(LIKES, $1) WHERE REC_ID = $2`;
151+
}
152+
await client.query(updateQuery, updateValues);
153+
await client.query("COMMIT");
154+
}
155+
} catch (ex) {
156+
console.log(`Something wrong happend in travelguide likeRecommendation${ex}`);
157+
} finally {
158+
await client.query("ROLLBACK");
159+
client.release();
160+
}
161+
}
162+
163+
/**
164+
* Deletes a recommendation from the db
165+
* @param {Number} authorId
166+
* @param {String} recommendationId
167+
*/
168+
async deleteRecommendation(authorId, recommendationId) {
169+
const client = await this.pool.connect();
170+
try {
171+
await client.query("BEGIN");
172+
const values = [authorId, recommendationId];
173+
const query = `DELETE FROM TRAVELGUIDE WHERE AUTHOR_ID=$1 AND REC_ID=$2`;
174+
175+
await client.query(query, values);
176+
await client.query("COMMIT");
177+
console.log("in delete");
178+
} catch (ex) {
179+
console.log(`Something wrong happened in travelguide deleteRecommendation ${ex}`);
180+
} finally {
181+
await client.query("ROLLBACK");
182+
client.release();
183+
}
184+
}
185+
186+
/**
187+
* Gets all the recommendations from an author
188+
* @param {Number} authorid
189+
* @returns Recommendations from given author
190+
*/
191+
async getAuthorRecommendations(authorId) {
192+
const client = await this.pool.connect();
193+
try {
194+
await client.query("BEGIN");
195+
const values = [authorId];
196+
let result = await client.query("SELECT * FROM TRAVELGUIDE WHERE AUTHOR_ID=$1", values);
197+
await client.query("COMMIT");
198+
return result.rows;
199+
} catch (ex) {
200+
console.log(`Something wrong happend in travelguide getAuthorRecommendations${ex}`);
201+
} finally {
202+
await client.query("ROLLBACK");
203+
client.release();
204+
}
205+
}
206+
207+
/**
208+
* Gets recommendations from category/season
209+
* @param {String} category
210+
* @param {String} season
211+
* @returns
212+
*/
213+
async getRecommendations(category, season) {
214+
const client = await this.pool.connect();
215+
try {
216+
await client.query("BEGIN");
217+
let query = "SELECT * FROM TRAVELGUIDE";
218+
219+
const values = [];
220+
let valueIndex = 1;
221+
222+
if (category || season) {
223+
query += " WHERE";
224+
if (category) {
225+
query += ` CATEGORY=$${valueIndex}`;
226+
values.push(category);
227+
valueIndex++;
228+
}
229+
if (season) {
230+
if (category) {
231+
query += " AND";
232+
}
233+
query += ` SEASON=$${valueIndex}`;
234+
values.push(season);
235+
}
236+
}
237+
query += " ORDER BY DATE_ADDED";
238+
239+
const result = await client.query(query, values);
240+
await client.query("COMMIT");
241+
return result.rows;
242+
} catch (ex) {
243+
console.log(`Something wrong happened in travelguide - getRecommendations ${ex}`);
244+
} finally {
245+
await client.query("ROLLBACK");
246+
client.release();
247+
}
248+
}
249+
250+
/**
251+
* Gets a recommendation from location, description and category
252+
* @param {String} location
253+
* @param {String} description
254+
* @param {String} category
255+
* @returns row containing the recommendation
256+
*/
257+
async getRecommendation(location, description, category) {
258+
const client = await this.pool.connect();
259+
try {
260+
await client.query("BEGIN");
261+
const values = [location, description, category];
262+
const result = await client.query(
263+
"SELECT * FROM TRAVELGUIDE WHERE LOCATION=$1 AND DESCRIPTION=$2 AND CATEGORY=$3",
264+
values,
265+
);
266+
await client.query("COMMIT");
267+
268+
return result.rows;
269+
} catch (ex) {
270+
console.log(`Something wrong happend in travelguide getRecommendation ${ex}`);
271+
} finally {
272+
await client.query("ROLLBACK");
273+
client.release();
274+
}
275+
}
276+
}
277+
278+
module.exports = {
279+
DBTravelguide,
280+
};

0 commit comments

Comments
 (0)
Please sign in to comment.