From 78da89e69b9af4eff727a4abc35bfdce7863c340 Mon Sep 17 00:00:00 2001 From: Pdzly Date: Mon, 18 Aug 2025 11:05:47 +0200 Subject: [PATCH 1/6] Removed Deprecated function arguments --- src/Config.prod.ts | 6 ++ src/Config.ts | 6 ++ src/config.type.ts | 6 ++ src/modules/suggest/suggest.command.ts | 98 ++++++++++++++++++++++++++ src/modules/suggest/suggest.module.ts | 10 +++ src/modules/suggest/suggest.ts | 88 +++++++++++++++++++++++ src/store/models/Suggestion.ts | 56 +++++++++++++++ src/store/models/SuggestionVotes.ts | 42 +++++++++++ 8 files changed, 312 insertions(+) create mode 100644 src/modules/suggest/suggest.command.ts create mode 100644 src/modules/suggest/suggest.module.ts create mode 100644 src/modules/suggest/suggest.ts create mode 100644 src/store/models/Suggestion.ts create mode 100644 src/store/models/SuggestionVotes.ts diff --git a/src/Config.prod.ts b/src/Config.prod.ts index 9d930f1f..ed41e6dc 100644 --- a/src/Config.prod.ts +++ b/src/Config.prod.ts @@ -49,6 +49,12 @@ export const config: Config = { yesEmojiId: "997496973093502986", noEmojiId: "1012427085798723666", }, + suggest: { + suggestionsChannel: "", + archiveChannel: "", + yesEmojiId: "thumbsup", + noEmojiId: "thumbsdown", + }, pastebin: { url: "https://paste.developerden.org", threshold: 20, diff --git a/src/Config.ts b/src/Config.ts index 678e3651..0a20e442 100644 --- a/src/Config.ts +++ b/src/Config.ts @@ -37,6 +37,12 @@ export const config: Config = { yesEmojiId: "thumbsup", noEmojiId: "thumbsdown", }, + suggest: { + suggestionsChannel: "", + archiveChannel: "", + yesEmojiId: "thumbsup", + noEmojiId: "thumbsdown", + }, pastebin: prodConfig.pastebin, branding: { color: "#ffffff", diff --git a/src/config.type.ts b/src/config.type.ts index 3ddc0818..838781b2 100644 --- a/src/config.type.ts +++ b/src/config.type.ts @@ -20,6 +20,12 @@ export interface Config { introductions?: string; general: string; }; + suggest: { + suggestionsChannel: string; + archiveChannel: string; + yesEmojiId: string; + noEmojiId: string; + }; commands: { daily: Snowflake; }; diff --git a/src/modules/suggest/suggest.command.ts b/src/modules/suggest/suggest.command.ts new file mode 100644 index 00000000..1025ba6e --- /dev/null +++ b/src/modules/suggest/suggest.command.ts @@ -0,0 +1,98 @@ +import { + ActionRowBuilder, + ApplicationCommandOptionType, + ApplicationCommandType, + ButtonBuilder, + ButtonStyle, + ChatInputCommandInteraction, + GuildMember, +} from "discord.js"; +import { Command } from "djs-slash-helper"; +import { config } from "../../Config.js"; +import { createSuggestionEmbed } from "./suggest.js"; + +export const SuggestCommand: Command = { + name: "suggest", + description: "Create a suggestion", + type: ApplicationCommandType.ChatInput, + options: [ + { + type: ApplicationCommandOptionType.String, + name: "suggestion", + description: "The suggestion", + required: true, + }, + { + type: ApplicationCommandOptionType.Attachment, + name: "image", + description: "Image that is relevant to the suggestion", + required: false, + }, + ], + handle: async (interaction: ChatInputCommandInteraction) => { + if (!interaction.member || !interaction.inGuild()) { + await interaction.reply({ + flags: ["Ephemeral"], + content: "We are not in a guild?", + }); + } + + await interaction.deferReply({ + flags: ["Ephemeral"], + }); + // Get the suggestion and optional image + const suggestionText = interaction.options.get("suggestion") + ?.value as string; + + const suggestionImage = interaction.options.getAttachment("image"); + + const suggestionChannel = await interaction.client.channels.fetch( + config.suggest.suggestionsChannel, + ); + + if (!suggestionChannel) { + await interaction.followUp({ + content: "There is no Suggestion channel!", + flags: ["Ephemeral"], + }); + return; + } + if (!suggestionChannel.isSendable() || !suggestionChannel.isTextBased()) { + await interaction.followUp({ + content: + "The suggestion channel is either not writeable or not a text channel!", + flags: ["Ephemeral"], + }); + return; + } + + const suggestionId = interaction.id; + + const embed = createSuggestionEmbed( + suggestionId, + interaction.member as GuildMember, + suggestionText, + suggestionImage, + ); + + const buttons = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId("suggest-ok") + .setStyle(ButtonStyle.Success) + .setEmoji(config.suggest.yesEmojiId), + new ButtonBuilder() + .setCustomId("suggest-no") + .setStyle(ButtonStyle.Danger) + .setEmoji(config.suggest.noEmojiId), + ); + const response = await suggestionChannel.send({ + embeds: [embed], + components: [buttons], + }); + + await interaction.followUp({ + content: `Suggestion with the ID \`${suggestionId} successfully submitted! See [here](${response.url})`, + flags: ["Ephemeral"], + }); + }, +}; diff --git a/src/modules/suggest/suggest.module.ts b/src/modules/suggest/suggest.module.ts new file mode 100644 index 00000000..ba33ccb0 --- /dev/null +++ b/src/modules/suggest/suggest.module.ts @@ -0,0 +1,10 @@ +import Module from "../module.js"; +import { SuggestCommand } from "./suggest.command.js"; + +export const SuggestModule: Module = { + name: "suggest", + commands: [SuggestCommand], + listeners: [], +}; + +export default SuggestModule; diff --git a/src/modules/suggest/suggest.ts b/src/modules/suggest/suggest.ts new file mode 100644 index 00000000..2782a50e --- /dev/null +++ b/src/modules/suggest/suggest.ts @@ -0,0 +1,88 @@ +import { Attachment, EmbedBuilder, GuildMember } from "discord.js"; +import { createStandardEmbed } from "../../util/embeds.js"; +import { fakeMention } from "../../util/users.js"; +import { Suggestion } from "../../store/models/Suggestion.js"; +import { SuggestionVotes } from "../../store/models/SuggestionVotes.js"; + +export const SUGGESTION_ID_EMBED_FIELD_NAME = "Suggestion ID"; +export const SUGGESTION_UPVOTE_EMBED_FIELD_NAME = ":white_check_mark:"; +export const SUGGESTION_DOWNVOTE_EMBED_FIELD_NAME = ":x:"; + +export const createSuggestionEmbed: ( + id: string, + member: GuildMember, + suggestionText: string, + suggestionImage: Attachment | null, +) => EmbedBuilder = ( + id: string, + member: GuildMember, + suggestionText: string, + suggestionImage: Attachment | null, +) => { + const builder = createStandardEmbed(member).addFields([ + { + name: "Submitter", + value: fakeMention(member.user), + inline: true, + }, + { + name: SUGGESTION_ID_EMBED_FIELD_NAME, + value: id, + }, + { + name: "Suggestion", + value: suggestionText, + }, + { + name: "Current Votes", + value: "-------------", + }, + { + name: SUGGESTION_UPVOTE_EMBED_FIELD_NAME, + value: "0", + }, + { + name: SUGGESTION_DOWNVOTE_EMBED_FIELD_NAME, + value: "0", + }, + ]); + + if (suggestionImage) { + builder.setImage(suggestionImage.proxyURL); + } + + return builder; +}; + +export const getSuggestion: (id: bigint) => Promise = async ( + id: bigint, +) => { + return await Suggestion.findOne({ + where: { + id: id, + }, + include: [SuggestionVotes], + }); +}; + +export const createSuggestion: ( + id: bigint, + userId: bigint, + messageId: bigint, + suggestionText: string, + suggestionImage: string | undefined, +) => Promise = async ( + id: bigint, + userId: bigint, + messageId: bigint, + suggestionText: string, + suggestionImageUrl: string | undefined, +) => { + return await Suggestion.create({ + id: id, + suggestionImageUrl: suggestionImageUrl, + userId: userId, + suggestiontText: suggestionText, + messageId: messageId, + }); +}; diff --git a/src/store/models/Suggestion.ts b/src/store/models/Suggestion.ts new file mode 100644 index 00000000..442186b9 --- /dev/null +++ b/src/store/models/Suggestion.ts @@ -0,0 +1,56 @@ +import { + DataTypes, + InferAttributes, + InferCreationAttributes, + Model, +} from "@sequelize/core"; +import { + AllowNull, + Attribute, + ColumnName, + HasMany, + NotNull, + PrimaryKey, + Table, + Unique, +} from "@sequelize/core/decorators-legacy"; +import { RealBigInt } from "../RealBigInt.js"; +import { SuggestionVotes } from "./SuggestionVotes.js"; + +@Table({ + tableName: "Suggestion", +}) +export class Suggestion extends Model< + InferAttributes, + InferCreationAttributes +> { + @Attribute(RealBigInt) + @PrimaryKey + @Unique + @NotNull + declare public id: bigint; + + @Attribute(RealBigInt) + @NotNull + @ColumnName("messageId") + public messageId!: bigint; + + @Attribute(RealBigInt) + @NotNull + @ColumnName("userId") + public userId!: bigint; + + @Attribute(DataTypes.STRING) + @NotNull + @ColumnName("suggestionText") + public suggestiontText!: string; + + @Attribute(DataTypes.STRING) + @AllowNull + @ColumnName("suggestionImageUrl") + public suggestionImageUrl: string | undefined; + + // Define the association + @HasMany(() => SuggestionVotes, "suggestionId") + declare public votes?: SuggestionVotes[]; +} diff --git a/src/store/models/SuggestionVotes.ts b/src/store/models/SuggestionVotes.ts new file mode 100644 index 00000000..aa8575dd --- /dev/null +++ b/src/store/models/SuggestionVotes.ts @@ -0,0 +1,42 @@ +import { + DataTypes, + InferAttributes, + InferCreationAttributes, + Model, +} from "@sequelize/core"; +import { + AllowNull, + Attribute, + BelongsTo, + ColumnName, + NotNull, + Table, +} from "@sequelize/core/decorators-legacy"; +import { RealBigInt } from "../RealBigInt.js"; +import { Suggestion } from "./Suggestion.js"; + +@Table({ + tableName: "SuggestionVotes", +}) +export class SuggestionVotes extends Model< + InferAttributes, + InferCreationAttributes +> { + @Attribute(RealBigInt) + @NotNull + @ColumnName("suggestionId") + public suggestionId!: bigint; + + @Attribute(RealBigInt) + @NotNull + @ColumnName("messageId") + public memberId!: bigint; + + @Attribute(DataTypes.TINYINT) + @AllowNull + @ColumnName("suggestionImageUrl") + public vote!: number; + + @BelongsTo(() => Suggestion, "suggestionId") + declare public suggestion?: Suggestion; +} From e097ca7e9d91a74e54ab6abfb1f6e1b703258d9b Mon Sep 17 00:00:00 2001 From: Pdzly Date: Mon, 18 Aug 2025 17:53:40 +0200 Subject: [PATCH 2/6] Implemented suggestion module with voting mechanism and Docker setup --- .gitignore | 1 + docker-compose.yml | 16 ++ src/Config.ts | 8 +- src/index.ts | 2 + src/modules/suggest/suggest.command.ts | 55 ++++++- src/modules/suggest/suggest.listener.ts | 80 ++++++++++ src/modules/suggest/suggest.module.ts | 3 +- src/modules/suggest/suggest.ts | 148 +++++++++++++++--- src/store/models/Suggestion.ts | 8 +- .../{SuggestionVotes.ts => SuggestionVote.ts} | 15 +- src/store/storage.ts | 4 +- 11 files changed, 294 insertions(+), 46 deletions(-) create mode 100644 docker-compose.yml create mode 100644 src/modules/suggest/suggest.listener.ts rename src/store/models/{SuggestionVotes.ts => SuggestionVote.ts} (69%) diff --git a/.gitignore b/.gitignore index 780c7fc3..98745fbc 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ logs/ eslint-results.sarif .env +!.env.local.example # Yarn new stuff .yarn/* diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..c0377e48 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,16 @@ +services: + postgres: + image: postgres:16-alpine + container_name: devden_db + environment: + POSTGRES_USER: root + POSTGRES_PASSWORD: password + POSTGRES_DB: database + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + restart: unless-stopped + +volumes: + postgres_data: diff --git a/src/Config.ts b/src/Config.ts index 0a20e442..776c2db4 100644 --- a/src/Config.ts +++ b/src/Config.ts @@ -38,10 +38,10 @@ export const config: Config = { noEmojiId: "thumbsdown", }, suggest: { - suggestionsChannel: "", - archiveChannel: "", - yesEmojiId: "thumbsup", - noEmojiId: "thumbsdown", + suggestionsChannel: "1407001821674868746", + archiveChannel: "1407001847239016550", + yesEmojiId: "👍", + noEmojiId: "👎", }, pastebin: prodConfig.pastebin, branding: { diff --git a/src/index.ts b/src/index.ts index 4514b21d..44624f10 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,6 +23,7 @@ import { initStorage } from "./store/storage.js"; import { initSentry } from "./sentry.js"; import { logger } from "./logging.js"; import { startHealthCheck } from "./healthcheck.js"; +import SuggestModule from "./modules/suggest/suggest.module.js"; const client = new Client({ intents: [ @@ -55,6 +56,7 @@ export const moduleManager = new ModuleManager( ShowcaseModule, TokenScannerModule, XpModule, + SuggestModule, ], ); diff --git a/src/modules/suggest/suggest.command.ts b/src/modules/suggest/suggest.command.ts index 1025ba6e..b71fe279 100644 --- a/src/modules/suggest/suggest.command.ts +++ b/src/modules/suggest/suggest.command.ts @@ -2,6 +2,7 @@ import { ActionRowBuilder, ApplicationCommandOptionType, ApplicationCommandType, + Attachment, ButtonBuilder, ButtonStyle, ChatInputCommandInteraction, @@ -9,7 +10,27 @@ import { } from "discord.js"; import { Command } from "djs-slash-helper"; import { config } from "../../Config.js"; -import { createSuggestionEmbed } from "./suggest.js"; +import { + createSuggestion, + createSuggestionEmbed, + SUGGESTION_NO_ID, + SUGGESTION_YES_ID, +} from "./suggest.js"; + +function isEmbeddableImage(attachment: Attachment): boolean { + if (!attachment.contentType) return false; + + // Check for standard image types and GIFs that can be embedded + const embeddableTypes = [ + "image/jpeg", + "image/jpg", + "image/png", + "image/gif", + "image/webp", + ]; + + return embeddableTypes.includes(attachment.contentType.toLowerCase()); +} export const SuggestCommand: Command = { name: "suggest", @@ -37,6 +58,8 @@ export const SuggestCommand: Command = { }); } + const member = interaction.member as GuildMember; + await interaction.deferReply({ flags: ["Ephemeral"], }); @@ -46,6 +69,13 @@ export const SuggestCommand: Command = { const suggestionImage = interaction.options.getAttachment("image"); + if (suggestionImage && !isEmbeddableImage(suggestionImage)) { + await interaction.followUp({ + content: "Your upload needs to be a image!", + }); + return; + } + const suggestionChannel = await interaction.client.channels.fetch( config.suggest.suggestionsChannel, ); @@ -70,18 +100,18 @@ export const SuggestCommand: Command = { const embed = createSuggestionEmbed( suggestionId, - interaction.member as GuildMember, + member, suggestionText, - suggestionImage, + suggestionImage?.proxyURL, ); - + const buttons = new ActionRowBuilder().addComponents( new ButtonBuilder() - .setCustomId("suggest-ok") + .setCustomId(SUGGESTION_YES_ID) .setStyle(ButtonStyle.Success) .setEmoji(config.suggest.yesEmojiId), new ButtonBuilder() - .setCustomId("suggest-no") + .setCustomId(SUGGESTION_NO_ID) .setStyle(ButtonStyle.Danger) .setEmoji(config.suggest.noEmojiId), ); @@ -90,9 +120,22 @@ export const SuggestCommand: Command = { components: [buttons], }); + await response.startThread({ + name: "Suggestion discussion thread", + reason: `User ${member.displayName} created a suggestion`, + }); + await interaction.followUp({ content: `Suggestion with the ID \`${suggestionId} successfully submitted! See [here](${response.url})`, flags: ["Ephemeral"], }); + + await createSuggestion( + BigInt(suggestionId), + BigInt(member.id), + BigInt(response.id), + suggestionText, + suggestionImage?.proxyURL, + ); }, }; diff --git a/src/modules/suggest/suggest.listener.ts b/src/modules/suggest/suggest.listener.ts new file mode 100644 index 00000000..e2a09739 --- /dev/null +++ b/src/modules/suggest/suggest.listener.ts @@ -0,0 +1,80 @@ +import { EventListener } from "../module.js"; +import { GuildMember, Interaction } from "discord.js"; +import { + createSuggestionEmbedFromEntity, + getSuggestionByMessageId, + SUGGESTION_NO_ID, + SUGGESTION_YES_ID, + SuggestionVoteType, + upsertVote, +} from "./suggest.js"; + +const SUGGESTION_BUTTON_MAP: { + [key: string]: SuggestionVoteType; +} = { + "suggestion-no": -1, + "suggestion-yes": 1, +}; + +export const SuggestionButtonListener: EventListener = { + async interactionCreate(client, interaction: Interaction) { + if ( + interaction.isButton() && + interaction.member && + (interaction.customId === SUGGESTION_NO_ID || + interaction.customId === SUGGESTION_YES_ID) + ) { + if (!interaction.message.editable) { + await interaction.reply({ + content: "This suggestion is no longer editable!", + flags: ["Ephemeral"], + }); + return; + } + const member = interaction.member as GuildMember; + + await interaction.deferReply({ flags: ["Ephemeral"] }); + + const votingValue = SUGGESTION_BUTTON_MAP[ + interaction.customId as keyof typeof SUGGESTION_BUTTON_MAP + ] as SuggestionVoteType; + + const suggestion = await getSuggestionByMessageId( + BigInt(interaction.message.id), + ); + + if (suggestion == null) { + await interaction.followUp({ + content: "No Suggestion found for this message", + flags: ["Ephemeral"], + }); + return; + } + + const previousVoteValue = await upsertVote( + suggestion.id, + BigInt(member.id), + votingValue, + ); + + await suggestion.reload(); + await interaction.message.edit({ + embeds: [ + await createSuggestionEmbedFromEntity( + suggestion, + interaction.member as GuildMember, + ), + ], + }); + + let content = `You ${previousVoteValue && previousVoteValue === votingValue ? "already " : ""}voted ${votingValue === 1 ? "**Yes**" : "**No**"} on this suggestion`; + if (previousVoteValue && previousVoteValue !== votingValue) { + content = `You changed your vote from ${previousVoteValue === 1 ? "**Yes**" : "**No**"} to ${votingValue === 1 ? "**Yes**" : "**No**"} on this suggestion`; + } + await interaction.followUp({ + content: content, + flags: ["Ephemeral"], + }); + } + }, +}; diff --git a/src/modules/suggest/suggest.module.ts b/src/modules/suggest/suggest.module.ts index ba33ccb0..29f6d434 100644 --- a/src/modules/suggest/suggest.module.ts +++ b/src/modules/suggest/suggest.module.ts @@ -1,10 +1,11 @@ import Module from "../module.js"; import { SuggestCommand } from "./suggest.command.js"; +import { SuggestionButtonListener } from "./suggest.listener.js"; export const SuggestModule: Module = { name: "suggest", commands: [SuggestCommand], - listeners: [], + listeners: [SuggestionButtonListener], }; export default SuggestModule; diff --git a/src/modules/suggest/suggest.ts b/src/modules/suggest/suggest.ts index 2782a50e..b7d577ed 100644 --- a/src/modules/suggest/suggest.ts +++ b/src/modules/suggest/suggest.ts @@ -1,32 +1,39 @@ -import { Attachment, EmbedBuilder, GuildMember } from "discord.js"; +import { EmbedBuilder, GuildMember } from "discord.js"; import { createStandardEmbed } from "../../util/embeds.js"; -import { fakeMention } from "../../util/users.js"; +import { actualMention } from "../../util/users.js"; import { Suggestion } from "../../store/models/Suggestion.js"; -import { SuggestionVotes } from "../../store/models/SuggestionVotes.js"; +import { SuggestionVote } from "../../store/models/SuggestionVote.js"; -export const SUGGESTION_ID_EMBED_FIELD_NAME = "Suggestion ID"; -export const SUGGESTION_UPVOTE_EMBED_FIELD_NAME = ":white_check_mark:"; -export const SUGGESTION_DOWNVOTE_EMBED_FIELD_NAME = ":x:"; +export const SUGGESTION_ID_FIELD_NAME = "Suggestion ID"; + +export const SUGGESTION_YES_ID = "suggestion-yes"; +export const SUGGESTION_NO_ID = "suggestion-no"; + +export type SuggestionVoteType = 1 | -1; export const createSuggestionEmbed: ( id: string, member: GuildMember, suggestionText: string, - suggestionImage: Attachment | null, + suggestionImage?: string, + upVotes?: number, + downVotes?: number, ) => EmbedBuilder = ( id: string, - member: GuildMember, - suggestionText: string, - suggestionImage: Attachment | null, + member, + suggestionText, + suggestionImage, + upvotes = 0, + downVotes = 0, ) => { const builder = createStandardEmbed(member).addFields([ { name: "Submitter", - value: fakeMention(member.user), + value: actualMention(member), inline: true, }, { - name: SUGGESTION_ID_EMBED_FIELD_NAME, + name: SUGGESTION_ID_FIELD_NAME, value: id, }, { @@ -35,25 +42,37 @@ export const createSuggestionEmbed: ( }, { name: "Current Votes", - value: "-------------", - }, - { - name: SUGGESTION_UPVOTE_EMBED_FIELD_NAME, - value: "0", - }, - { - name: SUGGESTION_DOWNVOTE_EMBED_FIELD_NAME, - value: "0", + value: `------------- + :white_check_mark::\`${upvotes}\` + :x::\`${downVotes}\` + `, }, ]); if (suggestionImage) { - builder.setImage(suggestionImage.proxyURL); + builder.setImage(suggestionImage); } return builder; }; +export const createSuggestionEmbedFromEntity: ( + suggestion: Suggestion, + member: GuildMember, +) => Promise = async (suggestion: Suggestion, member) => { + const upvotes = suggestion.votes?.filter((vote) => vote.vote === 1).length; + const downvotes = suggestion.votes?.filter((vote) => vote.vote === -1).length; + + return createSuggestionEmbed( + suggestion.id.toString(), + member, + suggestion.suggestionText, + suggestion.suggestionImageUrl, + upvotes ?? 0, + downvotes ?? 0, + ); +}; + export const getSuggestion: (id: bigint) => Promise = async ( id: bigint, ) => { @@ -61,7 +80,18 @@ export const getSuggestion: (id: bigint) => Promise = async ( where: { id: id, }, - include: [SuggestionVotes], + include: [SuggestionVote], + }); +}; + +export const getSuggestionByMessageId: ( + messageId: bigint, +) => Promise = async (messageId: bigint) => { + return await Suggestion.findOne({ + where: { + messageId: messageId, + }, + include: [SuggestionVote], }); }; @@ -82,7 +112,77 @@ export const createSuggestion: ( id: id, suggestionImageUrl: suggestionImageUrl, userId: userId, - suggestiontText: suggestionText, + suggestionText: suggestionText, messageId: messageId, }); }; + +export const getVoteForMemberAndSuggestion: ( + suggestionId: bigint, + memberId: bigint, +) => Promise = async ( + suggestionId: bigint, + memberId: bigint, +) => { + return await SuggestionVote.findOne({ + where: { + suggestionId: suggestionId, + memberId: memberId, + }, + }); +}; + +export const upsertVote: ( + suggestionId: bigint, + memberId: bigint, + vote: SuggestionVoteType, +) => Promise = async ( + suggestionId: bigint, + memberId: bigint, + vote: SuggestionVoteType, +) => { + // insert or update vote + const existingVote = await getVoteForMemberAndSuggestion( + suggestionId, + memberId, + ); + if (existingVote) { + const previousVote = existingVote.vote; + if (existingVote.vote === vote) { + return previousVote as SuggestionVoteType; + } + + existingVote.vote = vote; + await existingVote.save(); + return previousVote as SuggestionVoteType; + } else { + await createVote(suggestionId, memberId, vote); + return undefined; + } +}; + +export const createVote: ( + suggestionId: bigint, + memberId: bigint, + vote: SuggestionVoteType, +) => Promise = async ( + suggestionId: bigint, + memberId: bigint, + vote: SuggestionVoteType, +) => { + return await SuggestionVote.create({ + suggestionId: suggestionId, + memberId: memberId, + vote: vote, + }); +}; + +export const getSuggestionVotes: ( + suggestionId: bigint, +) => Promise = async (suggestionId: bigint) => { + return await SuggestionVote.findAll({ + where: { + suggestionId: suggestionId, + }, + }); +}; diff --git a/src/store/models/Suggestion.ts b/src/store/models/Suggestion.ts index 442186b9..f39fdf3a 100644 --- a/src/store/models/Suggestion.ts +++ b/src/store/models/Suggestion.ts @@ -15,7 +15,7 @@ import { Unique, } from "@sequelize/core/decorators-legacy"; import { RealBigInt } from "../RealBigInt.js"; -import { SuggestionVotes } from "./SuggestionVotes.js"; +import { SuggestionVote } from "./SuggestionVote.js"; @Table({ tableName: "Suggestion", @@ -43,7 +43,7 @@ export class Suggestion extends Model< @Attribute(DataTypes.STRING) @NotNull @ColumnName("suggestionText") - public suggestiontText!: string; + public suggestionText!: string; @Attribute(DataTypes.STRING) @AllowNull @@ -51,6 +51,6 @@ export class Suggestion extends Model< public suggestionImageUrl: string | undefined; // Define the association - @HasMany(() => SuggestionVotes, "suggestionId") - declare public votes?: SuggestionVotes[]; + @HasMany(() => SuggestionVote, "suggestionId") + declare public votes?: SuggestionVote[]; } diff --git a/src/store/models/SuggestionVotes.ts b/src/store/models/SuggestionVote.ts similarity index 69% rename from src/store/models/SuggestionVotes.ts rename to src/store/models/SuggestionVote.ts index aa8575dd..92f6166a 100644 --- a/src/store/models/SuggestionVotes.ts +++ b/src/store/models/SuggestionVote.ts @@ -11,30 +11,33 @@ import { ColumnName, NotNull, Table, + Unique, } from "@sequelize/core/decorators-legacy"; import { RealBigInt } from "../RealBigInt.js"; import { Suggestion } from "./Suggestion.js"; @Table({ - tableName: "SuggestionVotes", + tableName: "SuggestionVote", }) -export class SuggestionVotes extends Model< - InferAttributes, - InferCreationAttributes +export class SuggestionVote extends Model< + InferAttributes, + InferCreationAttributes > { @Attribute(RealBigInt) @NotNull @ColumnName("suggestionId") + @Unique("unique_suggestion_member") public suggestionId!: bigint; @Attribute(RealBigInt) @NotNull - @ColumnName("messageId") + @ColumnName("memberId") + @Unique("unique_suggestion_member") public memberId!: bigint; @Attribute(DataTypes.TINYINT) @AllowNull - @ColumnName("suggestionImageUrl") + @ColumnName("vote") public vote!: number; @BelongsTo(() => Suggestion, "suggestionId") diff --git a/src/store/storage.ts b/src/store/storage.ts index 230f5b83..64b0f1ec 100644 --- a/src/store/storage.ts +++ b/src/store/storage.ts @@ -7,6 +7,8 @@ import { AbstractDialect, DialectName, Sequelize } from "@sequelize/core"; import { SqliteDialect } from "@sequelize/sqlite3"; import { ConnectionConfig } from "pg"; import { Bump } from "./models/Bump.js"; +import { Suggestion } from "./models/Suggestion.js"; +import { SuggestionVote } from "./models/SuggestionVote.js"; function sequelizeLog(sql: string, timing?: number) { if (timing) { @@ -54,7 +56,7 @@ export async function initStorage() { } await sequelize.authenticate(); - const models = [DDUser, ColourRoles, FAQ, Bump]; + const models = [DDUser, ColourRoles, FAQ, Bump, Suggestion, SuggestionVote]; sequelize.addModels(models); Bump.belongsTo(DDUser, { From 7758dedfd3753b066bae50c5658b253e704e5cf8 Mon Sep 17 00:00:00 2001 From: Pdzly Date: Mon, 18 Aug 2025 17:57:13 +0200 Subject: [PATCH 3/6] Add .env.local.example template and update .gitignore for better environment configuration --- .env.local.example | 4 ++++ .gitignore | 4 +--- 2 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 .env.local.example diff --git a/.env.local.example b/.env.local.example new file mode 100644 index 00000000..80ee357c --- /dev/null +++ b/.env.local.example @@ -0,0 +1,4 @@ +DDB_HOST=localhost +DDB_PORT=5432 + +DDB_BOT_TOKEN= \ No newline at end of file diff --git a/.gitignore b/.gitignore index 98745fbc..9b980400 100644 --- a/.gitignore +++ b/.gitignore @@ -10,9 +10,6 @@ logs/ .DS_Store eslint-results.sarif -.env -!.env.local.example - # Yarn new stuff .yarn/* !.yarn/cache @@ -23,6 +20,7 @@ eslint-results.sarif !.yarn/versions .env* +!.env.local.example .flaskenv* !.env.project !.env.vault From 78eafae49ad7fc056c3c4955673f4d3f599e5cc0 Mon Sep 17 00:00:00 2001 From: Pdzly Date: Mon, 18 Aug 2025 20:33:04 +0200 Subject: [PATCH 4/6] Add suggestion management and voting review functionality --- src/modules/suggest/suggest.command.ts | 13 +- src/modules/suggest/suggest.listener.ts | 205 +++++++++++++++++++++++- src/modules/suggest/suggest.ts | 108 ++++++++++--- src/store/models/Suggestion.ts | 23 ++- src/util/users.ts | 2 + 5 files changed, 316 insertions(+), 35 deletions(-) diff --git a/src/modules/suggest/suggest.command.ts b/src/modules/suggest/suggest.command.ts index b71fe279..71c6f1cf 100644 --- a/src/modules/suggest/suggest.command.ts +++ b/src/modules/suggest/suggest.command.ts @@ -13,7 +13,9 @@ import { config } from "../../Config.js"; import { createSuggestion, createSuggestionEmbed, + SUGGESTION_MANAGE_ID, SUGGESTION_NO_ID, + SUGGESTION_VIEW_VOTES_ID, SUGGESTION_YES_ID, } from "./suggest.js"; @@ -114,6 +116,15 @@ export const SuggestCommand: Command = { .setCustomId(SUGGESTION_NO_ID) .setStyle(ButtonStyle.Danger) .setEmoji(config.suggest.noEmojiId), + new ButtonBuilder() + .setCustomId(SUGGESTION_VIEW_VOTES_ID) + .setStyle(ButtonStyle.Secondary) + .setEmoji("👁") + .setLabel("View Votes"), + new ButtonBuilder() + .setStyle(ButtonStyle.Secondary) + .setCustomId(SUGGESTION_MANAGE_ID) + .setEmoji("🎛"), ); const response = await suggestionChannel.send({ embeds: [embed], @@ -126,7 +137,7 @@ export const SuggestCommand: Command = { }); await interaction.followUp({ - content: `Suggestion with the ID \`${suggestionId} successfully submitted! See [here](${response.url})`, + content: `Suggestion with the ID \`${suggestionId}\` successfully submitted! See [here](${response.url})`, flags: ["Ephemeral"], }); diff --git a/src/modules/suggest/suggest.listener.ts b/src/modules/suggest/suggest.listener.ts index e2a09739..26ce6304 100644 --- a/src/modules/suggest/suggest.listener.ts +++ b/src/modules/suggest/suggest.listener.ts @@ -1,13 +1,27 @@ import { EventListener } from "../module.js"; -import { GuildMember, Interaction } from "discord.js"; +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + GuildMember, + Interaction, +} from "discord.js"; import { createSuggestionEmbedFromEntity, + createSuggestionManageButtons, + createVotesEmbed, getSuggestionByMessageId, + SUGGESTION_MANAGE_APPROVE_ID, + SUGGESTION_MANAGE_ID, + SUGGESTION_MANAGE_REJECT_ID, SUGGESTION_NO_ID, + SUGGESTION_VIEW_VOTES_ID, SUGGESTION_YES_ID, SuggestionVoteType, upsertVote, } from "./suggest.js"; +import { SuggestionStatus } from "../../store/models/Suggestion.js"; +import { config } from "../../Config.js"; const SUGGESTION_BUTTON_MAP: { [key: string]: SuggestionVoteType; @@ -19,10 +33,15 @@ const SUGGESTION_BUTTON_MAP: { export const SuggestionButtonListener: EventListener = { async interactionCreate(client, interaction: Interaction) { if ( - interaction.isButton() && - interaction.member && - (interaction.customId === SUGGESTION_NO_ID || - interaction.customId === SUGGESTION_YES_ID) + !interaction.isButton() || + !interaction.member || + !interaction.inGuild() + ) + return; + const member = interaction.member as GuildMember; + if ( + interaction.customId === SUGGESTION_NO_ID || + interaction.customId === SUGGESTION_YES_ID ) { if (!interaction.message.editable) { await interaction.reply({ @@ -31,7 +50,6 @@ export const SuggestionButtonListener: EventListener = { }); return; } - const member = interaction.member as GuildMember; await interaction.deferReply({ flags: ["Ephemeral"] }); @@ -75,6 +93,181 @@ export const SuggestionButtonListener: EventListener = { content: content, flags: ["Ephemeral"], }); + } else if (interaction.customId === SUGGESTION_VIEW_VOTES_ID) { + await interaction.deferReply({ flags: ["Ephemeral"] }); + const suggestion = await getSuggestionByMessageId( + BigInt(interaction.message.id), + ); + if (!suggestion) { + await interaction.followUp({ + content: "No Suggestion found for this message", + flags: ["Ephemeral"], + }); + return; + } + const yesVotes = + suggestion.votes?.filter((vote) => vote.vote === 1) || []; + const noVotes = + suggestion.votes?.filter((vote) => vote.vote === -1) || []; + + const embed = createVotesEmbed(member, yesVotes, noVotes); + + await interaction.followUp({ + embeds: [embed], + flags: ["Ephemeral"], + }); + } else if (interaction.customId === SUGGESTION_MANAGE_ID) { + const row = createSuggestionManageButtons(); + + await interaction.reply({ + content: "Manage Suggestion", + components: [row], + flags: ["Ephemeral"], + }); + } else if (interaction.customId === SUGGESTION_MANAGE_APPROVE_ID) { + await interaction.deferReply({ flags: ["Ephemeral"] }); + const initialMessage = await interaction.message.fetchReference(); + const suggestion = await getSuggestionByMessageId( + BigInt(initialMessage.id), + ); + if (!suggestion) { + await interaction.followUp({ + content: "No Suggestion found for this message", + flags: ["Ephemeral"], + }); + return; + } + + suggestion.status = SuggestionStatus.APPROVED; + suggestion.moderatorId = BigInt(member.id); + await suggestion.save(); + + const suggestionArchive = await client.channels.fetch( + config.suggest.archiveChannel, + ); + if (suggestionArchive) { + if ( + !suggestionArchive.isSendable() || + !suggestionArchive.isTextBased() + ) { + await interaction.followUp({ + content: + "The suggestion channel is either not writeable or not a text channel!", + flags: ["Ephemeral"], + }); + return; + } + + try { + const member = await interaction.guild!.members.fetch( + suggestion.memberId.toString(), + ); + + const embed = await createSuggestionEmbedFromEntity( + suggestion, + member, + ); + + const newMessage = await suggestionArchive.send({ + embeds: [embed], + components: [ + new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(SUGGESTION_VIEW_VOTES_ID) + .setStyle(ButtonStyle.Secondary) + .setEmoji("👁") + .setLabel("View Votes"), + ), + ], + }); + if (initialMessage.deletable) await initialMessage.delete(); + suggestion.messageId = BigInt(newMessage.id); + await suggestion.save(); + await interaction.followUp({ + content: "Suggestion approved!", + flags: ["Ephemeral"], + }); + } catch (e) { + console.error(e); + await interaction.followUp({ + content: + "Something went wrong while archiving the suggestion! Please try again later!", + flags: ["Ephemeral"], + }); + } + } + } else if (interaction.customId === SUGGESTION_MANAGE_REJECT_ID) { + const initialMessage = await interaction.message.fetchReference(); + await interaction.deferReply({ flags: ["Ephemeral"] }); + const suggestion = await getSuggestionByMessageId( + BigInt(initialMessage.id), + ); + if (!suggestion) { + await interaction.followUp({ + content: "No Suggestion found for this message", + flags: ["Ephemeral"], + }); + return; + } + + suggestion.status = SuggestionStatus.REJECTED; + suggestion.moderatorId = BigInt(member.id); + await suggestion.save(); + + const suggestionArchive = await client.channels.fetch( + config.suggest.archiveChannel, + ); + if (suggestionArchive) { + if ( + !suggestionArchive.isSendable() || + !suggestionArchive.isTextBased() + ) { + await interaction.followUp({ + content: + "The suggestion channel is either not writeable or not a text channel!", + flags: ["Ephemeral"], + }); + return; + } + + try { + const member = await interaction.guild!.members.fetch( + suggestion.memberId.toString(), + ); + + const embed = await createSuggestionEmbedFromEntity( + suggestion, + member, + ); + + const newMessage = await suggestionArchive.send({ + embeds: [embed], + components: [ + new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(SUGGESTION_VIEW_VOTES_ID) + .setStyle(ButtonStyle.Secondary) + .setEmoji("👁") + .setLabel("View Votes"), + ), + ], + }); + if (initialMessage.deletable) await initialMessage.delete(); + suggestion.messageId = BigInt(newMessage.id); + await suggestion.save(); + await interaction.followUp({ + content: "Suggestion rejected!", + flags: ["Ephemeral"], + }); + } catch (e) { + console.error(e); + await interaction.followUp({ + content: + "Something went wrong while archiving the suggestion! Please try again later!", + flags: ["Ephemeral"], + }); + } + } } }, }; diff --git a/src/modules/suggest/suggest.ts b/src/modules/suggest/suggest.ts index b7d577ed..7a1bee8e 100644 --- a/src/modules/suggest/suggest.ts +++ b/src/modules/suggest/suggest.ts @@ -1,13 +1,26 @@ -import { EmbedBuilder, GuildMember } from "discord.js"; +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + EmbedBuilder, + GuildMember, +} from "discord.js"; import { createStandardEmbed } from "../../util/embeds.js"; -import { actualMention } from "../../util/users.js"; -import { Suggestion } from "../../store/models/Suggestion.js"; +import { actualMention, actualMentionById } from "../../util/users.js"; +import { Suggestion, SuggestionStatus } from "../../store/models/Suggestion.js"; import { SuggestionVote } from "../../store/models/SuggestionVote.js"; +import { config } from "../../Config.js"; export const SUGGESTION_ID_FIELD_NAME = "Suggestion ID"; export const SUGGESTION_YES_ID = "suggestion-yes"; export const SUGGESTION_NO_ID = "suggestion-no"; +export const SUGGESTION_MANAGE_ID = "suggestion-manage"; + +export const SUGGESTION_MANAGE_APPROVE_ID = "suggestion-manage-approve"; +export const SUGGESTION_MANAGE_REJECT_ID = "suggestion-manage-reject"; + +export const SUGGESTION_VIEW_VOTES_ID = "suggestion-view-votes"; export type SuggestionVoteType = 1 | -1; @@ -18,6 +31,8 @@ export const createSuggestionEmbed: ( suggestionImage?: string, upVotes?: number, downVotes?: number, + status?: SuggestionStatus, + moderatorId?: string, ) => EmbedBuilder = ( id: string, member, @@ -25,8 +40,26 @@ export const createSuggestionEmbed: ( suggestionImage, upvotes = 0, downVotes = 0, + status, + moderatorId, ) => { - const builder = createStandardEmbed(member).addFields([ + const builder = createStandardEmbed(member); + + if (status) { + builder.addFields({ + name: "Status", + value: `**${status}**`, + }); + builder.setColor(status === SuggestionStatus.REJECTED ? "Red" : "Green"); + if (moderatorId) { + builder.addFields({ + name: `${status === SuggestionStatus.REJECTED ? "**Denied**" : "**Approved**"} By`, + value: actualMentionById(BigInt(moderatorId)), + }); + } + } + + builder.addFields([ { name: "Submitter", value: actualMention(member), @@ -41,7 +74,7 @@ export const createSuggestionEmbed: ( value: suggestionText, }, { - name: "Current Votes", + name: status === SuggestionStatus.PENDING ? "Current Votes" : "Results", value: `------------- :white_check_mark::\`${upvotes}\` :x::\`${downVotes}\` @@ -70,20 +103,13 @@ export const createSuggestionEmbedFromEntity: ( suggestion.suggestionImageUrl, upvotes ?? 0, downvotes ?? 0, + suggestion.status !== SuggestionStatus.PENDING + ? suggestion.status + : undefined, + suggestion.moderatorId ? suggestion.moderatorId.toString() : undefined, ); }; -export const getSuggestion: (id: bigint) => Promise = async ( - id: bigint, -) => { - return await Suggestion.findOne({ - where: { - id: id, - }, - include: [SuggestionVote], - }); -}; - export const getSuggestionByMessageId: ( messageId: bigint, ) => Promise = async (messageId: bigint) => { @@ -111,9 +137,10 @@ export const createSuggestion: ( return await Suggestion.create({ id: id, suggestionImageUrl: suggestionImageUrl, - userId: userId, + memberId: userId, suggestionText: suggestionText, messageId: messageId, + status: SuggestionStatus.PENDING, }); }; @@ -177,12 +204,43 @@ export const createVote: ( }); }; -export const getSuggestionVotes: ( - suggestionId: bigint, -) => Promise = async (suggestionId: bigint) => { - return await SuggestionVote.findAll({ - where: { - suggestionId: suggestionId, - }, - }); +export const createVotesEmbed: ( + member: GuildMember, + upvotes: SuggestionVote[], + downvotes: SuggestionVote[], +) => EmbedBuilder = (member, upvotes, downvotes) => { + return createStandardEmbed(member) + .setTitle("Suggestion Votes") + .addFields([ + { + name: `${config.suggest.yesEmojiId} Upvotes`, + value: upvotes + .map((vote) => actualMentionById(vote.memberId)) + .join("\n"), + inline: true, + }, + { + name: `${config.suggest.noEmojiId} Downvotes`, + value: downvotes + .map((vote) => actualMentionById(vote.memberId)) + .join("\n"), + inline: true, + }, + ]); }; + +export const createSuggestionManageButtons: () => ActionRowBuilder = + () => { + return new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setStyle(ButtonStyle.Success) + .setCustomId(SUGGESTION_MANAGE_APPROVE_ID) + .setEmoji("✅") + .setLabel("Approve"), + new ButtonBuilder() + .setStyle(ButtonStyle.Danger) + .setCustomId(SUGGESTION_MANAGE_REJECT_ID) + .setEmoji("❌") + .setLabel("Reject/Close"), + ); + }; diff --git a/src/store/models/Suggestion.ts b/src/store/models/Suggestion.ts index f39fdf3a..547276a8 100644 --- a/src/store/models/Suggestion.ts +++ b/src/store/models/Suggestion.ts @@ -8,6 +8,7 @@ import { AllowNull, Attribute, ColumnName, + Default, HasMany, NotNull, PrimaryKey, @@ -17,6 +18,12 @@ import { import { RealBigInt } from "../RealBigInt.js"; import { SuggestionVote } from "./SuggestionVote.js"; +export enum SuggestionStatus { + PENDING = "PENDING", + APPROVED = "APPROVED", + REJECTED = "REJECTED", +} + @Table({ tableName: "Suggestion", }) @@ -37,8 +44,8 @@ export class Suggestion extends Model< @Attribute(RealBigInt) @NotNull - @ColumnName("userId") - public userId!: bigint; + @ColumnName("memberId") + public memberId!: bigint; @Attribute(DataTypes.STRING) @NotNull @@ -50,7 +57,17 @@ export class Suggestion extends Model< @ColumnName("suggestionImageUrl") public suggestionImageUrl: string | undefined; - // Define the association + @Attribute(DataTypes.ENUM(SuggestionStatus)) + @Default(SuggestionStatus.PENDING) + @NotNull + @ColumnName("status") + public status: SuggestionStatus = SuggestionStatus.PENDING; + + @Attribute(RealBigInt) + @AllowNull + @ColumnName("moderatorId") + declare public moderatorId: bigint | undefined; + @HasMany(() => SuggestionVote, "suggestionId") declare public votes?: SuggestionVote[]; } diff --git a/src/util/users.ts b/src/util/users.ts index 44fbae72..2adfd02c 100644 --- a/src/util/users.ts +++ b/src/util/users.ts @@ -26,6 +26,8 @@ export const actualMention = ( user: GuildMember | User | PartialGuildMember, ): string => `<@${user.id}>`; +export const actualMentionById = (id: bigint): string => `<@${id}>`; + export const mentionWithNoPingMessage = (user: GuildMember): string => userShouldBePinged(user) ? `<@${user.id}> (Don't want to be pinged? )` From f367583ff12d2d189817237cce03daf40a028c98 Mon Sep 17 00:00:00 2001 From: Pdzly Date: Mon, 25 Aug 2025 16:40:13 +0200 Subject: [PATCH 5/6] Remove support for suggestion images in the suggestion module --- src/modules/suggest/suggest.command.ts | 39 +------------------------- src/modules/suggest/suggest.ts | 10 ------- src/store/models/Suggestion.ts | 5 ---- 3 files changed, 1 insertion(+), 53 deletions(-) diff --git a/src/modules/suggest/suggest.command.ts b/src/modules/suggest/suggest.command.ts index 71c6f1cf..6e02dc08 100644 --- a/src/modules/suggest/suggest.command.ts +++ b/src/modules/suggest/suggest.command.ts @@ -2,7 +2,6 @@ import { ActionRowBuilder, ApplicationCommandOptionType, ApplicationCommandType, - Attachment, ButtonBuilder, ButtonStyle, ChatInputCommandInteraction, @@ -19,21 +18,6 @@ import { SUGGESTION_YES_ID, } from "./suggest.js"; -function isEmbeddableImage(attachment: Attachment): boolean { - if (!attachment.contentType) return false; - - // Check for standard image types and GIFs that can be embedded - const embeddableTypes = [ - "image/jpeg", - "image/jpg", - "image/png", - "image/gif", - "image/webp", - ]; - - return embeddableTypes.includes(attachment.contentType.toLowerCase()); -} - export const SuggestCommand: Command = { name: "suggest", description: "Create a suggestion", @@ -45,12 +29,6 @@ export const SuggestCommand: Command = { description: "The suggestion", required: true, }, - { - type: ApplicationCommandOptionType.Attachment, - name: "image", - description: "Image that is relevant to the suggestion", - required: false, - }, ], handle: async (interaction: ChatInputCommandInteraction) => { if (!interaction.member || !interaction.inGuild()) { @@ -69,15 +47,6 @@ export const SuggestCommand: Command = { const suggestionText = interaction.options.get("suggestion") ?.value as string; - const suggestionImage = interaction.options.getAttachment("image"); - - if (suggestionImage && !isEmbeddableImage(suggestionImage)) { - await interaction.followUp({ - content: "Your upload needs to be a image!", - }); - return; - } - const suggestionChannel = await interaction.client.channels.fetch( config.suggest.suggestionsChannel, ); @@ -100,12 +69,7 @@ export const SuggestCommand: Command = { const suggestionId = interaction.id; - const embed = createSuggestionEmbed( - suggestionId, - member, - suggestionText, - suggestionImage?.proxyURL, - ); + const embed = createSuggestionEmbed(suggestionId, member, suggestionText); const buttons = new ActionRowBuilder().addComponents( new ButtonBuilder() @@ -146,7 +110,6 @@ export const SuggestCommand: Command = { BigInt(member.id), BigInt(response.id), suggestionText, - suggestionImage?.proxyURL, ); }, }; diff --git a/src/modules/suggest/suggest.ts b/src/modules/suggest/suggest.ts index 7a1bee8e..b581d851 100644 --- a/src/modules/suggest/suggest.ts +++ b/src/modules/suggest/suggest.ts @@ -28,7 +28,6 @@ export const createSuggestionEmbed: ( id: string, member: GuildMember, suggestionText: string, - suggestionImage?: string, upVotes?: number, downVotes?: number, status?: SuggestionStatus, @@ -37,7 +36,6 @@ export const createSuggestionEmbed: ( id: string, member, suggestionText, - suggestionImage, upvotes = 0, downVotes = 0, status, @@ -82,10 +80,6 @@ export const createSuggestionEmbed: ( }, ]); - if (suggestionImage) { - builder.setImage(suggestionImage); - } - return builder; }; @@ -100,7 +94,6 @@ export const createSuggestionEmbedFromEntity: ( suggestion.id.toString(), member, suggestion.suggestionText, - suggestion.suggestionImageUrl, upvotes ?? 0, downvotes ?? 0, suggestion.status !== SuggestionStatus.PENDING @@ -126,17 +119,14 @@ export const createSuggestion: ( userId: bigint, messageId: bigint, suggestionText: string, - suggestionImage: string | undefined, ) => Promise = async ( id: bigint, userId: bigint, messageId: bigint, suggestionText: string, - suggestionImageUrl: string | undefined, ) => { return await Suggestion.create({ id: id, - suggestionImageUrl: suggestionImageUrl, memberId: userId, suggestionText: suggestionText, messageId: messageId, diff --git a/src/store/models/Suggestion.ts b/src/store/models/Suggestion.ts index 547276a8..b5b3120e 100644 --- a/src/store/models/Suggestion.ts +++ b/src/store/models/Suggestion.ts @@ -52,11 +52,6 @@ export class Suggestion extends Model< @ColumnName("suggestionText") public suggestionText!: string; - @Attribute(DataTypes.STRING) - @AllowNull - @ColumnName("suggestionImageUrl") - public suggestionImageUrl: string | undefined; - @Attribute(DataTypes.ENUM(SuggestionStatus)) @Default(SuggestionStatus.PENDING) @NotNull From f3f928f04e2002f641bf61f3d453d2eed1d117a6 Mon Sep 17 00:00:00 2001 From: Pdzly Date: Mon, 1 Sep 2025 17:32:51 +0200 Subject: [PATCH 6/6] Clean up unused imports in `storage.ts` and format `models` array for readability. --- src/store/storage.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/store/storage.ts b/src/store/storage.ts index b28bac80..ad720a8e 100644 --- a/src/store/storage.ts +++ b/src/store/storage.ts @@ -3,14 +3,10 @@ import { type DialectName, Sequelize, } from "@sequelize/core"; -import { SqliteDialect } from "@sequelize/sqlite3"; -import type { ConnectionConfig } from "pg"; import { logger } from "../logging.js"; -import { Bump } from "./models/Bump.js"; import { ColourRoles } from "./models/ColourRoles.js"; import { DDUser } from "./models/DDUser.js"; import { FAQ } from "./models/FAQ.js"; -import { AbstractDialect, DialectName, Sequelize } from "@sequelize/core"; import { SqliteDialect } from "@sequelize/sqlite3"; import { ConnectionConfig } from "pg"; @@ -65,7 +61,15 @@ export async function initStorage() { } await sequelize.authenticate(); - const models = [DDUser, ColourRoles, FAQ, Bump, ModeratorActions, Suggestion, SuggestionVote]; + const models = [ + DDUser, + ColourRoles, + FAQ, + Bump, + ModeratorActions, + Suggestion, + SuggestionVote, + ]; sequelize.addModels(models); Bump.belongsTo(DDUser, {