diff --git a/src/buttons/dislikeButton.ts b/src/buttons/dislikeButton.ts new file mode 100644 index 0000000..df1ebe4 --- /dev/null +++ b/src/buttons/dislikeButton.ts @@ -0,0 +1,25 @@ +import { ButtonBuilder, ButtonInteraction, ButtonStyle } from 'discord.js'; +import { prisma } from '../model'; + +export const data = new ButtonBuilder() + .setCustomId('Dislike') + .setLabel('๐Ÿ‘Ž') + .setStyle(ButtonStyle.Primary); + +export async function execute(interaction: ButtonInteraction) { + const prevEmbed = interaction.message.embeds[0]; + const reviewId = prevEmbed.footer?.text.split(' ').pop(); + await prisma.reviews.update({ + where: { + id: reviewId, + }, + data: { + negativeRating: { + increment: 1, + }, + }, + }); + return interaction.update({ + content: 'Thank you for your vote', + }); +} diff --git a/src/buttons/index.ts b/src/buttons/index.ts index ed8dcea..1928ae8 100644 --- a/src/buttons/index.ts +++ b/src/buttons/index.ts @@ -1,2 +1,6 @@ export * as addChannel from './addChannelButton'; export * as openCreateModal from './openCreateModalButton'; +export * as Like from './likeButton'; +export * as Dislike from './dislikeButton'; +export * as PreviousReview from './previousEmbedButton'; +export * as NextReview from './nextEmbedButton'; diff --git a/src/buttons/likeButton.ts b/src/buttons/likeButton.ts new file mode 100644 index 0000000..dab24d5 --- /dev/null +++ b/src/buttons/likeButton.ts @@ -0,0 +1,25 @@ +import { ButtonBuilder, ButtonInteraction, ButtonStyle } from 'discord.js'; +import { prisma } from '../model'; + +export const data = new ButtonBuilder() + .setCustomId('Like') + .setLabel('๐Ÿ‘') + .setStyle(ButtonStyle.Primary); + +export async function execute(interaction: ButtonInteraction) { + const prevEmbed = interaction.message.embeds[0]; + const reviewId = prevEmbed.footer?.text.split(' ').pop(); + await prisma.reviews.update({ + where: { + id: reviewId, + }, + data: { + positiveRating: { + increment: 1, + }, + }, + }); + return interaction.update({ + content: 'Thank you for your vote', + }); +} diff --git a/src/buttons/nextEmbedButton.ts b/src/buttons/nextEmbedButton.ts new file mode 100644 index 0000000..3aa8db3 --- /dev/null +++ b/src/buttons/nextEmbedButton.ts @@ -0,0 +1,23 @@ +import { ButtonBuilder, ButtonInteraction, ButtonStyle } from 'discord.js'; +import { embedContentBuilder } from '../utils/embedContentBuilder'; +import { reviewEmbedConstructor } from '../embeds'; + +export const data = new ButtonBuilder() + .setCustomId('NextReview') + .setLabel('Next') + .setStyle(ButtonStyle.Primary); + +export async function execute(interaction: ButtonInteraction) { + const move = 1; + const content = await embedContentBuilder(interaction, move); + if (content == undefined) { + return interaction.update({ + content: 'Error while processing reviews!', + }); + } + const embed = reviewEmbedConstructor(content); + return interaction.update({ + content: '', + embeds: [embed], + }); +} diff --git a/src/buttons/previousEmbedButton.ts b/src/buttons/previousEmbedButton.ts new file mode 100644 index 0000000..a2e483a --- /dev/null +++ b/src/buttons/previousEmbedButton.ts @@ -0,0 +1,23 @@ +import { ButtonBuilder, ButtonInteraction, ButtonStyle } from 'discord.js'; +import { embedContentBuilder } from '../utils/embedContentBuilder'; +import { reviewEmbedConstructor } from '../embeds'; + +export const data = new ButtonBuilder() + .setCustomId('PreviousReview') + .setLabel('Previous') + .setStyle(ButtonStyle.Primary); + +export async function execute(interaction: ButtonInteraction) { + const move = -1; + const content = await embedContentBuilder(interaction, move); + if (content == undefined) { + return interaction.update({ + content: 'Error while processing reviews!', + }); + } + const embed = reviewEmbedConstructor(content); + return interaction.update({ + content: '', + embeds: [embed], + }); +} diff --git a/src/commands/index.ts b/src/commands/index.ts index 985ee29..6ecb313 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -10,3 +10,4 @@ export * as config from './config'; export * as channelmanagement from './channelmanagement'; export * as edit from './edit'; export * as channelrole from './channelrole'; +export * as review from './review'; diff --git a/src/commands/review.ts b/src/commands/review.ts new file mode 100644 index 0000000..3d5e12c --- /dev/null +++ b/src/commands/review.ts @@ -0,0 +1,139 @@ +import { + ActionRowBuilder, + ButtonBuilder, + //ButtonInteraction, + ChatInputCommandInteraction, + SlashCommandBuilder, +} from 'discord.js'; +import { prisma } from '../model'; +import { reviewEmbedConstructor } from '../embeds'; +import { Dislike, Like, NextReview, PreviousReview } from '../buttons'; +import { Review } from '../utils'; + +/** + * Edits existing bot message in a channel. + */ +export const data = new SlashCommandBuilder() + .setName('review') + .setDescription('Interacts with review feature of the bot') + .addSubcommand((subcommand) => + subcommand + .setName('get') + .setDescription('Outputs table with reviews for the subject') + .addStringOption((option) => + option + .setName('subject') + .setDescription('Code of the subject (e.g. PV276)') + .setRequired(true) + ) + ) + .addSubcommand((subcommand) => + subcommand + .setName('add') + .setDescription('Adds new review for the subjet') + .addStringOption((option) => + option + .setName('subject') + .setDescription('Code of the subject (e.g. PV276)') + .setRequired(true) + ) + .addStringOption((option) => + option + .setName('text') + .setDescription('Text of the review') + .setRequired(true) + ) + ); + +export async function execute(interaction: ChatInputCommandInteraction) { + await interaction.reply({ + content: 'Processing review request...', + ephemeral: true, + }); + const subjectCode = interaction.options.getString('subject'); + if (interaction.options.getSubcommand() === 'add') { + const userId = interaction.user.id; + const textReview = interaction.options.getString('text'); + if (textReview == undefined || subjectCode == undefined) { + console.log('Error with arguments for create review command.'); + return interaction.editReply({ + content: `Error while creating a review for ${subjectCode}.`, + }); + } + const existingReview = await prisma.reviews.findMany({ + where: { + discordUserId: { + equals: userId, + }, + subjectCode: { + equals: subjectCode, + }, + }, + }); + console.log(existingReview); + if (existingReview.length != 0) { + return interaction.editReply({ + content: `You have already submitted review fro subject ${subjectCode}.`, + }); + } + try { + await prisma.reviews.create({ + data: { + discordUserId: userId, + subjectCode: subjectCode.toUpperCase(), + reviewText: textReview, + }, + }); + } catch (err) { + console.log(`Database error: ${err}`); + return interaction.editReply({ + content: `Error while creating a review for ${subjectCode}.`, + }); + } + return interaction.editReply({ + content: `Review for subject ${subjectCode} created.`, + }); + } else if (interaction.options.getSubcommand() === 'get') { + const submittedReviews = await prisma.reviews.findMany({ + orderBy: { + dateReview: 'asc', + }, + where: { + subjectCode: subjectCode?.toUpperCase(), + }, + }); + const title = `Reviews for course ${subjectCode}`; + const reviewer = await interaction.client.users.fetch( + submittedReviews[0].discordUserId + ); + const review: Review = { + id: submittedReviews[0].id, + invokedUsername: interaction.user.username, + invokedAvatarUrl: interaction.user.displayAvatarURL(), + title: title, + description: `\`\`\`${submittedReviews[0].reviewText}\`\`\``, + positiveRating: submittedReviews[0].positiveRating, + negativeRating: submittedReviews[0].negativeRating, + reviewerUsername: reviewer.username, + reviwerAvatarUrl: reviewer.displayAvatarURL(), + reviewNumber: 1, + numberOfReviews: submittedReviews.length, + }; + const reviewEmbed = reviewEmbedConstructor(review); + const row = new ActionRowBuilder().addComponents( + PreviousReview.data, + NextReview.data, + Like.data, + Dislike.data + ); + return interaction.editReply({ + content: '', + embeds: [reviewEmbed], + components: [row], + }); + } else { + return interaction.editReply({ + content: `Invalid command.`, + }); + } +} diff --git a/src/embeds/index.ts b/src/embeds/index.ts new file mode 100644 index 0000000..cc313ba --- /dev/null +++ b/src/embeds/index.ts @@ -0,0 +1 @@ +export * from './reviewsEmbed'; diff --git a/src/embeds/reviewsEmbed.ts b/src/embeds/reviewsEmbed.ts new file mode 100644 index 0000000..c288c04 --- /dev/null +++ b/src/embeds/reviewsEmbed.ts @@ -0,0 +1,24 @@ +import { EmbedBuilder } from 'discord.js'; +import { Review } from '../utils'; + +export function reviewEmbedConstructor(reviewInfo: Review) { + const reviewEmbed = new EmbedBuilder() + .setColor('#2f00ff') + .setTitle(reviewInfo.title) + .setAuthor({ + name: reviewInfo.invokedUsername, + iconURL: reviewInfo.invokedAvatarUrl, + }) + .setDescription(reviewInfo.description) + .addFields({ + name: 'Rating of the review', + value: `${reviewInfo.positiveRating}๐Ÿ‘|${reviewInfo.negativeRating}๐Ÿ‘Ž`, + }) + .setTimestamp() + .setFooter({ + text: `Reviwer: ${reviewInfo.reviewerUsername} ยท Review number: ${reviewInfo.reviewNumber}/${reviewInfo.numberOfReviews} ยท ID: ${reviewInfo.id}`, + iconURL: reviewInfo.reviwerAvatarUrl, + }); + + return reviewEmbed; +} diff --git a/src/model/schema.prisma b/src/model/schema.prisma index 19f0e57..958a5a6 100644 --- a/src/model/schema.prisma +++ b/src/model/schema.prisma @@ -14,3 +14,13 @@ model Users { status String joinDate DateTime? @default(now()) } + +model Reviews { + id String @id @default(auto()) @map("_id") @database.ObjectId + subjectCode String + reviewText String + discordUserId String + positiveRating Int @default(0) + negativeRating Int @default(0) + dateReview DateTime @default(now()) +} diff --git a/src/utils/Review.ts b/src/utils/Review.ts new file mode 100644 index 0000000..9e5c332 --- /dev/null +++ b/src/utils/Review.ts @@ -0,0 +1,13 @@ +export interface Review { + id: string; + invokedUsername: string; + invokedAvatarUrl: string; + title: string; + description: string; + positiveRating: number; + negativeRating: number; + reviewerUsername: string; + reviwerAvatarUrl: string; + reviewNumber: number; + numberOfReviews: number; +} diff --git a/src/utils/embedContentBuilder.ts b/src/utils/embedContentBuilder.ts new file mode 100644 index 0000000..8a38ae1 --- /dev/null +++ b/src/utils/embedContentBuilder.ts @@ -0,0 +1,54 @@ +import { ButtonInteraction } from 'discord.js'; +import { Review } from './Review'; +import { prisma } from '../model'; + +export async function embedContentBuilder( + interaction: ButtonInteraction, + move: number +) { + const prevEmbed = interaction.message.embeds[0]; + const reviewId = prevEmbed.footer?.text.split(' ').pop(); + const subjectCode = prevEmbed.title?.replace('Reviews for course ', ''); + const submittedReviews = await prisma.reviews.findMany({ + orderBy: { + dateReview: 'asc', + }, + where: { + subjectCode: subjectCode?.toUpperCase(), + }, + }); + if (prevEmbed.title == null) { + return; + } + for (let index = 0; index < submittedReviews.length; index++) { + if (submittedReviews[index].id === reviewId) { + if (index + move >= submittedReviews.length) { + index = -1; + } + if (index + move < 0) { + index = submittedReviews.length; + } + const reviewer = await interaction.client.users.fetch( + submittedReviews[index + move].discordUserId + ); + const review: Review = { + id: submittedReviews[index + move].id, + invokedUsername: interaction.user.username, + invokedAvatarUrl: interaction.user.displayAvatarURL(), + title: prevEmbed.title, + description: `\`\`\`${ + submittedReviews[index + move].reviewText + }\`\`\``, + positiveRating: submittedReviews[index + move].positiveRating, + negativeRating: submittedReviews[index + move].negativeRating, + reviewerUsername: reviewer.username, + reviwerAvatarUrl: reviewer.displayAvatarURL(), + reviewNumber: index + move + 1, + numberOfReviews: submittedReviews.length, + }; + return review; + } + } + console.log('Error occured while processing reviews.'); + return undefined; +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 230b862..a160d6a 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -5,3 +5,4 @@ export { Config, ConfigKey, ConfigValue, ConfigProperties } from './config'; export { Embed } from './embed'; export { SubjectChannels } from './setupSubjectChannels'; export { parseCustomId } from './parseCustomId'; +export { Review } from './Review';