From a2e1fa7a9860de843c4235c2a318c3d3dce83e80 Mon Sep 17 00:00:00 2001 From: Vermium Sifell Date: Mon, 29 May 2023 16:08:22 +0200 Subject: [PATCH] feat: :sparkles: add quote feature Now users can post quotes about each other if guild has enabled it! --- .../20230529133956_add_quotes/migration.sql | 40 +++++++ .../migration.sql | 2 + prisma/schema.prisma | 43 +++++++- src/commands/quote/index.ts | 23 ++++ src/commands/quote/subcommands/post/index.ts | 101 ++++++++++++++++++ src/commands/settings/index.ts | 5 +- .../settings/subcommands/quotes/index.ts | 90 ++++++++++++++++ 7 files changed, 300 insertions(+), 4 deletions(-) create mode 100644 prisma/migrations/20230529133956_add_quotes/migration.sql create mode 100644 prisma/migrations/20230529135410_added_boolean_for_status_of_quotes/migration.sql create mode 100644 src/commands/quote/index.ts create mode 100644 src/commands/quote/subcommands/post/index.ts create mode 100644 src/commands/settings/subcommands/quotes/index.ts diff --git a/prisma/migrations/20230529133956_add_quotes/migration.sql b/prisma/migrations/20230529133956_add_quotes/migration.sql new file mode 100644 index 0000000..ae6d6f7 --- /dev/null +++ b/prisma/migrations/20230529133956_add_quotes/migration.sql @@ -0,0 +1,40 @@ +-- AlterTable +ALTER TABLE `GuildSettings` ADD COLUMN `guildQuotesSettingsId` VARCHAR(191) NULL; + +-- CreateTable +CREATE TABLE `GuildQuotesSettings` ( + `id` VARCHAR(191) NOT NULL, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updatedAt` DATETIME(3) NOT NULL, + `quoteChannelId` VARCHAR(191) NOT NULL, + + UNIQUE INDEX `GuildQuotesSettings_id_key`(`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Quotes` ( + `id` VARCHAR(191) NOT NULL, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updatedAt` DATETIME(3) NOT NULL, + `userId` VARCHAR(191) NOT NULL, + `guildId` VARCHAR(191) NOT NULL, + `message` VARCHAR(191) NOT NULL, + `posterUserId` VARCHAR(191) NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `GuildSettings` ADD CONSTRAINT `GuildSettings_guildQuotesSettingsId_fkey` FOREIGN KEY (`guildQuotesSettingsId`) REFERENCES `GuildQuotesSettings`(`id`) ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `GuildQuotesSettings` ADD CONSTRAINT `GuildQuotesSettings_id_fkey` FOREIGN KEY (`id`) REFERENCES `Guild`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Quotes` ADD CONSTRAINT `Quotes_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Quotes` ADD CONSTRAINT `Quotes_guildId_fkey` FOREIGN KEY (`guildId`) REFERENCES `Guild`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Quotes` ADD CONSTRAINT `Quotes_posterUserId_fkey` FOREIGN KEY (`posterUserId`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20230529135410_added_boolean_for_status_of_quotes/migration.sql b/prisma/migrations/20230529135410_added_boolean_for_status_of_quotes/migration.sql new file mode 100644 index 0000000..e852686 --- /dev/null +++ b/prisma/migrations/20230529135410_added_boolean_for_status_of_quotes/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE `GuildQuotesSettings` ADD COLUMN `status` BOOLEAN NOT NULL DEFAULT false; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7e1ca43..4f1dfe6 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -8,14 +8,17 @@ datasource db { } model Guild { - id String @unique - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @unique + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + guildMembers GuildMember[] guildSettings GuildSettings? guildCreditsSettings GuildCreditsSettings? + guildQuotesSettings GuildQuotesSettings? apiCredentials ApiCredentials[] cooldowns Cooldown[] + Quotes Quotes[] } model User { @@ -27,6 +30,8 @@ model User { cooldowns Cooldown[] userReputation UserReputation? + Quotes Quotes[] @relation(name: "Quotes") + PostedQuotes Quotes[] @relation(name: "PostedQuotes") } model GuildMember { @@ -81,6 +86,8 @@ model GuildSettings { creditsSettings GuildCreditsSettings? @relation(fields: [guildCreditsSettingsId], references: [id], onDelete: Cascade) guildCreditsSettingsId String? + GuildQuotesSettings GuildQuotesSettings? @relation(fields: [guildQuotesSettingsId], references: [id]) + guildQuotesSettingsId String? } model GuildCreditsSettings { @@ -102,6 +109,36 @@ model GuildCreditsSettings { guildSettings GuildSettings[] } +model GuildQuotesSettings { + id String @unique + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + guild Guild @relation(fields: [id], references: [id], onDelete: Cascade) + + status Boolean @default(false) + quoteChannelId String + + guildSettings GuildSettings[] +} + +model Quotes { + id String @id @default(uuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], name: "Quotes") + userId String + + guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade) + guildId String + + message String + + posterUserId String + posterUser User @relation(fields: [posterUserId], references: [id], name: "PostedQuotes") +} + model ApiCredentials { id String @id @default(cuid()) createdAt DateTime @default(now()) diff --git a/src/commands/quote/index.ts b/src/commands/quote/index.ts new file mode 100644 index 0000000..17dfa9a --- /dev/null +++ b/src/commands/quote/index.ts @@ -0,0 +1,23 @@ +import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js"; + +// Subcommands +import { + SubcommandHandlers, + executeSubcommand, +} from "../../handlers/executeSubcommand"; +import * as post from "./subcommands/post"; + +const subcommandHandlers: SubcommandHandlers = { + post: post.execute, +}; + +export const builder = new SlashCommandBuilder() + .setName("quote") + .setDescription("Fun commands.") + + .addSubcommand(post.builder); + +// Execute function +export const execute = async (interaction: ChatInputCommandInteraction) => { + await executeSubcommand(interaction, subcommandHandlers); +}; diff --git a/src/commands/quote/subcommands/post/index.ts b/src/commands/quote/subcommands/post/index.ts new file mode 100644 index 0000000..3963528 --- /dev/null +++ b/src/commands/quote/subcommands/post/index.ts @@ -0,0 +1,101 @@ +import { + ChannelType, + ChatInputCommandInteraction, + EmbedBuilder, + SlashCommandSubcommandBuilder, +} from "discord.js"; +import CooldownManager from "../../../../handlers/CooldownManager"; +import prisma from "../../../../handlers/prisma"; +import generateCooldownName from "../../../../helpers/generateCooldownName"; +import deferReply from "../../../../utils/deferReply"; +import sendResponse from "../../../../utils/sendResponse"; + +const cooldownManager = new CooldownManager(); + +export const builder = (command: SlashCommandSubcommandBuilder) => { + return command + .setName("post") + .setDescription("Post a quote someone said in this server") + .addUserOption((option) => + option + .setName("user") + .setDescription("The user who said this") + .setRequired(true) + ) + .addStringOption((option) => + option + .setName("message") + .setDescription("What the user said") + .setRequired(true) + ); +}; + +export const execute = async ( + interaction: ChatInputCommandInteraction +): Promise => { + await deferReply(interaction, true); + + const { options, guild, user } = interaction; + + const quoteUser = options.getUser("user", true); + const quoteString = options.getString("message", true); + + if (!guild) throw new Error("A guild is required."); + + const guildQuotesSettings = await prisma.guildQuotesSettings.findUnique({ + where: { id: guild.id }, + }); + + if (!guildQuotesSettings) throw new Error("No configuration available."); + + if (guildQuotesSettings.status !== true) + throw new Error("Quotes are disabled in this server."); + + const channel = await interaction.client.channels.fetch( + guildQuotesSettings.quoteChannelId + ); + + if (!channel) throw new Error("No channel found."); + + if (channel.type !== ChannelType.GuildText) + throw new Error("The channel is not a text channel."); + + await prisma.quotes.create({ + data: { + guildId: guild.id, + userId: quoteUser.id, + posterUserId: user.id, + message: quoteString, + }, + }); + + const quoteEmbed = new EmbedBuilder() + .setAuthor({ + name: `Quote of ${quoteUser.username}`, + iconURL: quoteUser.displayAvatarURL(), + }) + .setColor(process.env.EMBED_COLOR_SUCCESS) + .setDescription(quoteString) + .setFooter({ + text: `Posted by ${user.username}`, + iconURL: user.displayAvatarURL(), + }); + + const sentMessage = await channel.send({ embeds: [quoteEmbed] }); + + await sentMessage.react("👍"); + await sentMessage.react("👎"); + + const postEmbed = new EmbedBuilder() + .setColor(process.env.EMBED_COLOR_SUCCESS) + .setDescription("Successfully posted the quote!"); + + await sendResponse(interaction, { embeds: [postEmbed] }); + + await cooldownManager.setCooldown( + await generateCooldownName(interaction), + guild, + user, + 5 * 60 + ); +}; diff --git a/src/commands/settings/index.ts b/src/commands/settings/index.ts index 6b8f461..793ca44 100644 --- a/src/commands/settings/index.ts +++ b/src/commands/settings/index.ts @@ -6,10 +6,12 @@ import { } from "../../handlers/executeSubcommand"; import * as credits from "./subcommands/credits"; import * as ctrlpanel from "./subcommands/ctrlpanel"; +import * as quotes from "./subcommands/quotes"; const subcommandHandlers: SubcommandHandlers = { ctrlpanel: ctrlpanel.execute, credits: credits.execute, + quotes: quotes.execute, }; export const builder = new SlashCommandBuilder() @@ -17,7 +19,8 @@ export const builder = new SlashCommandBuilder() .setDescription("Manage guild configurations.") .setDMPermission(false) .addSubcommand(ctrlpanel.builder) - .addSubcommand(credits.builder); + .addSubcommand(credits.builder) + .addSubcommand(quotes.builder); export const execute = async (interaction: ChatInputCommandInteraction) => { await executeSubcommand(interaction, subcommandHandlers); diff --git a/src/commands/settings/subcommands/quotes/index.ts b/src/commands/settings/subcommands/quotes/index.ts new file mode 100644 index 0000000..4e08a3c --- /dev/null +++ b/src/commands/settings/subcommands/quotes/index.ts @@ -0,0 +1,90 @@ +import { + ChatInputCommandInteraction, + EmbedBuilder, + PermissionsBitField, + SlashCommandSubcommandBuilder, +} from "discord.js"; +import prisma from "../../../../handlers/prisma"; +import checkPermission from "../../../../utils/checkPermission"; +import deferReply from "../../../../utils/deferReply"; +import sendResponse from "../../../../utils/sendResponse"; + +export const builder = (command: SlashCommandSubcommandBuilder) => { + return command + .setName("quotes") + .setDescription(`Configure quotes module`) + .addBooleanOption((option) => + option.setName("status").setDescription("Status").setRequired(true) + ) + .addChannelOption((option) => + option.setName("channel").setDescription("channel").setRequired(true) + ); +}; + +export const execute = async (interaction: ChatInputCommandInteraction) => { + await deferReply(interaction, true); + + checkPermission(interaction, PermissionsBitField.Flags.ManageGuild); + + const { guild, options, user } = interaction; + + const quoteStatus = options.getBoolean("status", true); + const quoteChannel = options.getChannel("channel", true); + + if (!guild) { + throw new Error("Guild not found."); + } + + const upsertGuildQuotesSettings = await prisma.guildQuotesSettings.upsert({ + where: { + id: guild.id, + }, + update: { + quoteChannelId: quoteChannel.id, + status: quoteStatus, + }, + create: { + id: guild.id, + quoteChannelId: quoteChannel.id, + status: quoteStatus, + guildSettings: { + connectOrCreate: { + where: { + id: guild.id, + }, + create: { + id: guild.id, + }, + }, + }, + }, + }); + + const embedSuccess = new EmbedBuilder() + .setAuthor({ + name: "Configuration of Quotes", + }) + .setColor(process.env.EMBED_COLOR_SUCCESS) + .setFooter({ + text: `Successfully configured by ${user.username}`, + iconURL: user.displayAvatarURL(), + }) + .setTimestamp(); + + await sendResponse(interaction, { + embeds: [ + embedSuccess + .setDescription("Configuration updated successfully!") + .addFields({ + name: "Status", + value: `${upsertGuildQuotesSettings.status}`, + inline: true, + }) + .addFields({ + name: "Channel ID", + value: `${upsertGuildQuotesSettings.quoteChannelId}`, + inline: true, + }), + ], + }); +};