From 4e1401012a3ea4f87d4135e50131f12eca91b9bc Mon Sep 17 00:00:00 2001 From: Gus Libens <27970303+goestav@users.noreply.github.com> Date: Sat, 3 Aug 2024 23:56:24 +0200 Subject: [PATCH 01/32] refactor: add structures for configuration message --- src/structures/configuration-manifest.ts | 126 +++++++++++++++++++++++ src/structures/configuration-message.ts | 39 +++++++ src/structures/database-store.ts | 54 ++++++++++ src/structures/index.ts | 3 + 4 files changed, 222 insertions(+) create mode 100644 src/structures/configuration-manifest.ts create mode 100644 src/structures/configuration-message.ts create mode 100644 src/structures/database-store.ts create mode 100644 src/structures/index.ts diff --git a/src/structures/configuration-manifest.ts b/src/structures/configuration-manifest.ts new file mode 100644 index 0000000..42d34d0 --- /dev/null +++ b/src/structures/configuration-manifest.ts @@ -0,0 +1,126 @@ +import type { InferSelectModel, Table as DrizzleTable } from "drizzle-orm"; +import type { Channel, Role, TextInputStyle } from "discord.js"; +import { DatabaseStore } from "./database-store.ts"; + +interface ConfigurationOptionTypeMap { + text: string; + boolean: boolean; + role: Role; + channel: Channel; + select: string; +} + +export type ConfigurationOptionType = keyof ConfigurationOptionTypeMap; + +type ConfigurationOptionValidateFn = ( + value: T, +) => boolean | string; +type ConfigurationOptionToStoreFn = (value: T) => unknown; +type ConfigurationOptionFromStoreFn = (value: unknown) => T; + +/** Represents a data storage. */ +// biome-ignore lint/suspicious/noExplicitAny: context type will be decided by the store +export abstract class ConfigurationStore { + /** Write data to the store. */ + abstract write(context: W): void | Promise; + /** Read data from the store. */ + abstract read(context: R): unknown | Promise; +} + +/** Helper type to extract the read context from the {@link ConfigurationStore}. */ +export type InferStoreReadContext = Parameters< + T["read"] +>[0]; + +interface PartialConfigurationOption< + T extends ConfigurationOptionType = "text", + U = unknown, +> { + name: string; + description: string; + /** @default 'text' */ + type?: T; + /** Validate the input, validation succeeds when `true` is returned. */ + validate?: ConfigurationOptionValidateFn; + /** Transform value when persisting to the store. */ + toStore?: ConfigurationOptionToStoreFn; + /** Transform value when retrieving from the store. */ + fromStore?: ConfigurationOptionFromStoreFn; + /** The context to pass to the {@link ConfigurationStore}. */ + storeContext: U; +} + +export interface ConfigurationTextOption + extends PartialConfigurationOption<"text", U> { + type: "text"; + placeholder?: string; + /** + * Which type of text input to display in the modal. + * + * @default TextInputStyle.Short + */ + style?: TextInputStyle; +} + +export interface ConfigurationBooleanOption + extends PartialConfigurationOption<"boolean", U> { + type: "boolean"; +} + +export interface ConfigurationRoleOption + extends PartialConfigurationOption<"role", U> { + type: "role"; +} + +export interface ConfigurationChannelOption + extends PartialConfigurationOption<"channel", U> { + type: "channel"; +} + +export interface ConfigurationSelectOption + extends PartialConfigurationOption<"select", U> { + type: "select"; + options: { + name: string; + value: string; + }[]; +} + +export type ConfigurationOption = + | ConfigurationTextOption + | ConfigurationBooleanOption + | ConfigurationRoleOption + | ConfigurationChannelOption + | ConfigurationSelectOption; + +type OmitUnion = T extends object ? Omit : never; + +export type ConfigurationOptionPartial< + T extends string, + U extends ConfigurationStore, +> = OmitUnion>, "storeContext"> & { + column: T; +}; + +/** Create a configuration manifest for {@link DatabaseStore}. */ +export const createDatabaseConfigurationManifest = < + Table extends DrizzleTable, + Option extends ConfigurationOptionPartial< + keyof InferSelectModel, + DatabaseStore + >, +>( + table: Table, + options: Option[], +) => { + return options.map( + (option) => + ({ + ...option, + storeContext: { + table, + columns: [option.column], + }, + }) as ConfigurationOption>, + ); +}; diff --git a/src/structures/configuration-message.ts b/src/structures/configuration-message.ts new file mode 100644 index 0000000..1fe5ded --- /dev/null +++ b/src/structures/configuration-message.ts @@ -0,0 +1,39 @@ +import { + ChatInputCommandInteraction, + type BaseMessageOptions, +} from "discord.js"; +import type { + ConfigurationOption, + ConfigurationStore, + InferStoreReadContext, +} from "./configuration-manifest.ts"; + +/** Represents a configuration message. */ +export class ConfigurationMessage< + Store extends ConfigurationStore, + ManifestOption extends ConfigurationOption>, +> { + #store: ConfigurationStore; + #manifest: ConfigurationOption[]; + + constructor(store: Store, manifest: ManifestOption[]) { + this.#store = store; + this.#manifest = manifest; + } + + /** Reply with the configuration message and listen to component interactions. */ + public initialize(interaction: ChatInputCommandInteraction) { + const messageOptions = this.getMessageOptions(); + + if (interaction.replied || interaction.deferred) { + return interaction.editReply(messageOptions); + } + + return interaction.reply(messageOptions); + } + + /** Get the message options for the configuration message. */ + protected getMessageOptions(): BaseMessageOptions { + return {}; + } +} diff --git a/src/structures/database-store.ts b/src/structures/database-store.ts new file mode 100644 index 0000000..2258f32 --- /dev/null +++ b/src/structures/database-store.ts @@ -0,0 +1,54 @@ +import type { InferInsertModel, InferSelectModel, Table } from "drizzle-orm"; +import { ConfigurationStore } from "./configuration-manifest.ts"; +import db from "../db.ts"; + +interface DatabaseStoreWriteContext { + /** The table to execute queries on. */ + table: T; + /** The table columns mapped to their new values. */ + values: { + [Key in keyof InferInsertModel]: InferInsertModel[Key]; + }[]; +} + +interface DatabaseStoreReadContext { + /** The table to execute queries on. */ + table: T; + /** The table columns to select. */ + columns: (keyof InferSelectModel)[]; +} + +type GetSchemaColumn< + Schema extends object, + T extends PropertyKey, +> = T extends keyof Schema ? Schema[T] : never; +type GetSelectFields = { + [Key in T as Key extends keyof Schema ? Key : never]: GetSchemaColumn< + Schema, + Key + >; +}; + +/** Represents a database store. */ +export class DatabaseStore extends ConfigurationStore< + DatabaseStoreWriteContext, + DatabaseStoreReadContext +> { + async write(context: T): Promise { + await db.insert(context.table).values(context.values); + } + + async read(context: T) { + const selectFields = Object.fromEntries( + context.columns.map((fieldName) => [ + fieldName, + context.table[fieldName as keyof typeof context.table], + ]), + ) as GetSelectFields< + typeof context.table, + (typeof context.columns)[number] + >; + + return db.select(selectFields).from(context.table).get(); + } +} diff --git a/src/structures/index.ts b/src/structures/index.ts new file mode 100644 index 0000000..571aa46 --- /dev/null +++ b/src/structures/index.ts @@ -0,0 +1,3 @@ +export * from "./configuration-manifest.ts"; +export * from "./database-store.ts"; +export * from "./configuration-message.ts"; From abc55d3e77aac4dc623f89af9cea9374bb410f3c Mon Sep 17 00:00:00 2001 From: Gus Libens <27970303+goestav@users.noreply.github.com> Date: Sat, 3 Aug 2024 23:56:47 +0200 Subject: [PATCH 02/32] refactor: add configuration command --- src/commands/config.ts | 157 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 src/commands/config.ts diff --git a/src/commands/config.ts b/src/commands/config.ts new file mode 100644 index 0000000..389cbc5 --- /dev/null +++ b/src/commands/config.ts @@ -0,0 +1,157 @@ +import { PermissionFlagsBits, TextInputStyle, type Channel } from "discord.js"; +import { Config } from "../schemas/config.ts"; +import { + createDatabaseConfigurationManifest, + type ConfigurationOptionPartial, +} from "../structures/index.ts"; +import { LogMode } from "../types/logging.ts"; +import { DatabaseStore, ConfigurationMessage } from "../structures/index.ts"; +import type { Command } from "djs-fsrouter"; +import type { InferSelectModel } from "drizzle-orm"; + +const checkIsValidTextChannel = (channel: Channel) => { + if (channel.isDMBased()) return `${channel} must be a guild channel`; + + if (!channel.isTextBased()) return `${channel} must be a text channel`; + + const clientPermissionsInChannel = channel.permissionsFor( + channel.client.user, + ); + + if (!clientPermissionsInChannel?.has(PermissionFlagsBits.SendMessages)) + return `I do not have permission to send message to ${channel}`; + + return true; +}; + +const LogModeValues = Object.keys(LogMode).filter( + (item) => !Number.isNaN(Number(item)), +); + +const LogModeSelectOptions = Object.entries(LogMode) + .filter(([, value]) => typeof value === "number") + .map(([key, value]) => ({ name: key, value: value.toString() })); + +const store = new DatabaseStore(); + +const manifest = createDatabaseConfigurationManifest(Config, [ + { + name: "Gateway channel", + description: "New members will be welcomed here.", + column: "gatewayChannel", + type: "channel", + validate: checkIsValidTextChannel, + }, + // Gateway join + { + name: "Gateway join title", + description: "Message title when a user joins.", + column: "gatewayChannel", + type: "text", + // TODO: implement string placeholders + placeholder: "Welcome ${mention}!", + }, + { + name: "Gateway join content", + description: "Message content when a user joins.", + column: "gatewayChannel", + type: "text", + // TODO: implement string placeholders + placeholder: "We hope you enjoy your stay!", + style: TextInputStyle.Paragraph, + }, + // Gateway leave + { + name: "Gateway leave title", + description: "Message title when a user leaves.", + column: "gatewayChannel", + type: "text", + // TODO: implement string placeholders + placeholder: "Goodbye ${mention}!", + }, + { + name: "Gateway leave content", + description: "Message content when a user leaves.", + column: "gatewayChannel", + type: "text", + // TODO: implement string placeholders + placeholder: "We are sorry to see you go ${mention}", + style: TextInputStyle.Paragraph, + }, + // Logging + { + name: "Logging mode", + description: "Determines what should be logged.", + column: "loggingMode", + type: "select", + options: LogModeSelectOptions, + validate(value) { + if (!LogModeValues.includes(value)) + return "The provided logging mode is invalid"; + + return true; + }, + toStore(value): number { + return Number.parseInt(value); + }, + fromStore(value): string { + return (value as number).toString(); + }, + }, + { + name: "Logging channel", + description: "Log messages will be sent here.", + column: "loggingChannel", + type: "channel", + validate: checkIsValidTextChannel, + }, + // Suggestion + { + name: "Suggestion channel", + description: "Suggestions will be sent here.", + column: "suggestionChannel", + type: "channel", + validate: checkIsValidTextChannel, + }, + { + name: "Suggestion manager role", + description: "The role that can approve and reject suggestions.", + column: "suggestionManagerRole", + type: "role", + }, + { + name: "Suggestion upvote emoji", + description: "The emoji for upvoting suggestions.", + column: "suggestionUpvoteEmoji", + type: "text", + }, + { + name: "Suggestion downvote emoji", + description: "The emoji for downvoting suggestions.", + column: "suggestionDownvoteEmoji", + type: "text", + }, +] satisfies ConfigurationOptionPartial< + keyof InferSelectModel, + DatabaseStore +>[]); + +const ConfigCommand: Command = { + description: "Configure the bot", + defaultMemberPermissions: PermissionFlagsBits.Administrator, + async run(interaction) { + if (!interaction.inGuild()) { + interaction.reply({ + content: "Run this command in a server to get server info", + ephemeral: true, + }); + return; + } + + const configurationMessage = new ConfigurationMessage(store, manifest); + + await configurationMessage.initialize(interaction); + }, +}; + +export default ConfigCommand; From fb59dfdc6cd83290e1bb921547edb587b067a8d1 Mon Sep 17 00:00:00 2001 From: Gus Libens <27970303+goestav@users.noreply.github.com> Date: Sun, 4 Aug 2024 16:36:58 +0200 Subject: [PATCH 03/32] refactor: add basic settings view --- src/commands/config.ts | 3 +- src/structures/configuration-message.ts | 79 ++++++++++++++++++++++--- src/structures/database-store.ts | 6 +- 3 files changed, 75 insertions(+), 13 deletions(-) diff --git a/src/commands/config.ts b/src/commands/config.ts index 389cbc5..55cba22 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -56,7 +56,6 @@ const manifest = createDatabaseConfigurationManifest(Config, [ description: "Message content when a user joins.", column: "gatewayChannel", type: "text", - // TODO: implement string placeholders placeholder: "We hope you enjoy your stay!", style: TextInputStyle.Paragraph, }, @@ -95,7 +94,7 @@ const manifest = createDatabaseConfigurationManifest(Config, [ return Number.parseInt(value); }, fromStore(value): string { - return (value as number).toString(); + return value ? (value as number).toString() : ""; }, }, { diff --git a/src/structures/configuration-message.ts b/src/structures/configuration-message.ts index 1fe5ded..9478adf 100644 --- a/src/structures/configuration-message.ts +++ b/src/structures/configuration-message.ts @@ -1,20 +1,26 @@ import { + bold, + channelMention, ChatInputCommandInteraction, + roleMention, type BaseMessageOptions, } from "discord.js"; import type { ConfigurationOption, - ConfigurationStore, InferStoreReadContext, } from "./configuration-manifest.ts"; +import type { DatabaseStore } from "./database-store.ts"; +import { Config, type ConfigSelect } from "../schemas/config.ts"; +import db from "../db.ts"; +import { eq, type Table } from "drizzle-orm"; /** Represents a configuration message. */ export class ConfigurationMessage< - Store extends ConfigurationStore, + Store extends DatabaseStore, ManifestOption extends ConfigurationOption>, > { - #store: ConfigurationStore; - #manifest: ConfigurationOption[]; + #store: Store; + #manifest: ConfigurationOption>[]; constructor(store: Store, manifest: ManifestOption[]) { this.#store = store; @@ -22,8 +28,10 @@ export class ConfigurationMessage< } /** Reply with the configuration message and listen to component interactions. */ - public initialize(interaction: ChatInputCommandInteraction) { - const messageOptions = this.getMessageOptions(); + public async initialize(interaction: ChatInputCommandInteraction) { + if (!interaction.inGuild()) return; + + const messageOptions = await this.getMessageOptions(interaction.guildId); if (interaction.replied || interaction.deferred) { return interaction.editReply(messageOptions); @@ -33,7 +41,62 @@ export class ConfigurationMessage< } /** Get the message options for the configuration message. */ - protected getMessageOptions(): BaseMessageOptions { - return {}; + protected async getMessageOptions( + guildId: string, + ): Promise { + let content = ""; + + const databaseValues = db + .select() + .from(Config) + .where(eq(Config.id, guildId)) + .all() + // TEMP: use .all() and select the first row manually, .get() does not work + .at(0); + + if (!databaseValues) + throw new Error("Could not retrieve the configuration"); + + for (const manifestOption of this.#manifest) { + const databaseValue = this.getOptionValue( + manifestOption, + databaseValues[ + manifestOption.storeContext.columns[0] as keyof typeof databaseValues + ], + ); + + content += `${bold(manifestOption.name)} — ${this.formatValue( + manifestOption.type, + databaseValue, + )}\n`; + } + + return { + content: content.trim(), + }; + } + + protected getOptionValue( + manifestOption: ConfigurationOption>, + value: unknown, + ) { + if (manifestOption.fromStore) return manifestOption.fromStore(value); + + return value; + } + + protected formatValue(type: ConfigurationOption["type"], value: unknown) { + const notSetValue = "(Not set)"; + + switch (type) { + case "boolean": + return (value as boolean) ? "Yes" : "No"; + case "channel": + return value ? channelMention(value as string) : notSetValue; + case "role": + return value ? roleMention(value as string) : notSetValue; + default: + return value ? (value as string) : notSetValue; + } } } diff --git a/src/structures/database-store.ts b/src/structures/database-store.ts index 2258f32..0b08e56 100644 --- a/src/structures/database-store.ts +++ b/src/structures/database-store.ts @@ -1,14 +1,13 @@ import type { InferInsertModel, InferSelectModel, Table } from "drizzle-orm"; import { ConfigurationStore } from "./configuration-manifest.ts"; import db from "../db.ts"; +import type { Snowflake } from "discord.js"; interface DatabaseStoreWriteContext { /** The table to execute queries on. */ table: T; /** The table columns mapped to their new values. */ - values: { - [Key in keyof InferInsertModel]: InferInsertModel[Key]; - }[]; + values: InferInsertModel[]; } interface DatabaseStoreReadContext { @@ -16,6 +15,7 @@ interface DatabaseStoreReadContext { table: T; /** The table columns to select. */ columns: (keyof InferSelectModel)[]; + where?: Record; } type GetSchemaColumn< From 29fc6388e25da65e1d2fc4efc0c4a07c7c55430c Mon Sep 17 00:00:00 2001 From: Gus Libens <27970303+goestav@users.noreply.github.com> Date: Wed, 7 Aug 2024 00:37:06 +0200 Subject: [PATCH 04/32] refactor: support different tables --- src/commands/config.ts | 27 ++--- src/structures/configuration-manifest.ts | 126 +++++++++++----------- src/structures/configuration-message.ts | 129 ++++++++++++++++++----- src/structures/database-store.ts | 54 ---------- src/structures/index.ts | 1 - 5 files changed, 170 insertions(+), 167 deletions(-) delete mode 100644 src/structures/database-store.ts diff --git a/src/commands/config.ts b/src/commands/config.ts index 55cba22..aa8dae7 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -1,13 +1,10 @@ import { PermissionFlagsBits, TextInputStyle, type Channel } from "discord.js"; import { Config } from "../schemas/config.ts"; -import { - createDatabaseConfigurationManifest, - type ConfigurationOptionPartial, -} from "../structures/index.ts"; +import { createConfigurationManifest } from "../structures/index.ts"; import { LogMode } from "../types/logging.ts"; -import { DatabaseStore, ConfigurationMessage } from "../structures/index.ts"; +import { ConfigurationMessage } from "../structures/index.ts"; import type { Command } from "djs-fsrouter"; -import type { InferSelectModel } from "drizzle-orm"; +import { eq } from "drizzle-orm"; const checkIsValidTextChannel = (channel: Channel) => { if (channel.isDMBased()) return `${channel} must be a guild channel`; @@ -32,9 +29,7 @@ const LogModeSelectOptions = Object.entries(LogMode) .filter(([, value]) => typeof value === "number") .map(([key, value]) => ({ name: key, value: value.toString() })); -const store = new DatabaseStore(); - -const manifest = createDatabaseConfigurationManifest(Config, [ +const manifest = createConfigurationManifest(Config, [ { name: "Gateway channel", description: "New members will be welcomed here.", @@ -90,10 +85,10 @@ const manifest = createDatabaseConfigurationManifest(Config, [ return true; }, - toStore(value): number { + toDatabase(value): number { return Number.parseInt(value); }, - fromStore(value): string { + fromDatabase(value): string { return value ? (value as number).toString() : ""; }, }, @@ -130,10 +125,7 @@ const manifest = createDatabaseConfigurationManifest(Config, [ column: "suggestionDownvoteEmoji", type: "text", }, -] satisfies ConfigurationOptionPartial< - keyof InferSelectModel, - DatabaseStore ->[]); +]); const ConfigCommand: Command = { description: "Configure the bot", @@ -147,7 +139,10 @@ const ConfigCommand: Command = { return; } - const configurationMessage = new ConfigurationMessage(store, manifest); + const configurationMessage = new ConfigurationMessage(Config, manifest, { + getWhereClause: ({ table, interaction }) => + eq(table.id, interaction.guildId), + }); await configurationMessage.initialize(interaction); }, diff --git a/src/structures/configuration-manifest.ts b/src/structures/configuration-manifest.ts index 42d34d0..4e58e9d 100644 --- a/src/structures/configuration-manifest.ts +++ b/src/structures/configuration-manifest.ts @@ -1,6 +1,5 @@ import type { InferSelectModel, Table as DrizzleTable } from "drizzle-orm"; import type { Channel, Role, TextInputStyle } from "discord.js"; -import { DatabaseStore } from "./database-store.ts"; interface ConfigurationOptionTypeMap { text: string; @@ -15,43 +14,41 @@ export type ConfigurationOptionType = keyof ConfigurationOptionTypeMap; type ConfigurationOptionValidateFn = ( value: T, ) => boolean | string; -type ConfigurationOptionToStoreFn = (value: T) => unknown; -type ConfigurationOptionFromStoreFn = (value: unknown) => T; - -/** Represents a data storage. */ -// biome-ignore lint/suspicious/noExplicitAny: context type will be decided by the store -export abstract class ConfigurationStore { - /** Write data to the store. */ - abstract write(context: W): void | Promise; - /** Read data from the store. */ - abstract read(context: R): unknown | Promise; -} - -/** Helper type to extract the read context from the {@link ConfigurationStore}. */ -export type InferStoreReadContext = Parameters< - T["read"] ->[0]; +type ConfigurationOptionToDatabaseFn = ( + value: U, +) => T; +type ConfigurationOptionFromDatabaseFn = (value: T) => unknown; interface PartialConfigurationOption< - T extends ConfigurationOptionType = "text", - U = unknown, + Type extends ConfigurationOptionType, + Table extends DrizzleTable, + Column extends keyof InferSelectModel
, > { name: string; description: string; /** @default 'text' */ - type?: T; + type?: Type; /** Validate the input, validation succeeds when `true` is returned. */ - validate?: ConfigurationOptionValidateFn; - /** Transform value when persisting to the store. */ - toStore?: ConfigurationOptionToStoreFn; - /** Transform value when retrieving from the store. */ - fromStore?: ConfigurationOptionFromStoreFn; - /** The context to pass to the {@link ConfigurationStore}. */ - storeContext: U; + validate?: ConfigurationOptionValidateFn; + /** Transform the value when persisting to the database. */ + toDatabase?: ConfigurationOptionToDatabaseFn< + InferSelectModel
[Column], + ConfigurationOptionTypeMap[Type] + >; + /** Transform the value when retrieving from the database. */ + fromDatabase?: ConfigurationOptionFromDatabaseFn< + InferSelectModel
[Column] + >; + /** The table to execute queries on. */ + table: Table; + /** The column to execute queries on. */ + column: Column; } -export interface ConfigurationTextOption - extends PartialConfigurationOption<"text", U> { +export interface ConfigurationTextOption< + Table extends DrizzleTable, + Column extends keyof InferSelectModel
, +> extends PartialConfigurationOption<"text", Table, Column> { type: "text"; placeholder?: string; /** @@ -62,23 +59,31 @@ export interface ConfigurationTextOption style?: TextInputStyle; } -export interface ConfigurationBooleanOption - extends PartialConfigurationOption<"boolean", U> { +export interface ConfigurationBooleanOption< + Table extends DrizzleTable, + Column extends keyof InferSelectModel
, +> extends PartialConfigurationOption<"boolean", Table, Column> { type: "boolean"; } -export interface ConfigurationRoleOption - extends PartialConfigurationOption<"role", U> { +export interface ConfigurationRoleOption< + Table extends DrizzleTable, + Column extends keyof InferSelectModel
, +> extends PartialConfigurationOption<"role", Table, Column> { type: "role"; } -export interface ConfigurationChannelOption - extends PartialConfigurationOption<"channel", U> { +export interface ConfigurationChannelOption< + Table extends DrizzleTable, + Column extends keyof InferSelectModel
, +> extends PartialConfigurationOption<"channel", Table, Column> { type: "channel"; } -export interface ConfigurationSelectOption - extends PartialConfigurationOption<"select", U> { +export interface ConfigurationSelectOption< + Table extends DrizzleTable, + Column extends keyof InferSelectModel
, +> extends PartialConfigurationOption<"select", Table, Column> { type: "select"; options: { name: string; @@ -86,41 +91,28 @@ export interface ConfigurationSelectOption }[]; } -export type ConfigurationOption = - | ConfigurationTextOption - | ConfigurationBooleanOption - | ConfigurationRoleOption - | ConfigurationChannelOption - | ConfigurationSelectOption; +export type ConfigurationOption< + Table extends DrizzleTable = DrizzleTable, + Column extends keyof InferSelectModel
= keyof InferSelectModel
, +> = + | ConfigurationTextOption + | ConfigurationBooleanOption + | ConfigurationRoleOption + | ConfigurationChannelOption + | ConfigurationSelectOption; type OmitUnion = T extends object ? Omit : never; -export type ConfigurationOptionPartial< - T extends string, - U extends ConfigurationStore, -> = OmitUnion>, "storeContext"> & { - column: T; -}; - /** Create a configuration manifest for {@link DatabaseStore}. */ -export const createDatabaseConfigurationManifest = < - Table extends DrizzleTable, - Option extends ConfigurationOptionPartial< - keyof InferSelectModel
, - DatabaseStore - >, +export const createConfigurationManifest = < + const Table extends DrizzleTable, + const Column extends keyof InferSelectModel
, >( table: Table, - options: Option[], + options: OmitUnion, "table">[], ) => { - return options.map( - (option) => - ({ - ...option, - storeContext: { - table, - columns: [option.column], - }, - }) as ConfigurationOption>, - ); + return options.map((options) => ({ + table, + ...options, + })); }; diff --git a/src/structures/configuration-message.ts b/src/structures/configuration-message.ts index 9478adf..79dc8fe 100644 --- a/src/structures/configuration-message.ts +++ b/src/structures/configuration-message.ts @@ -2,54 +2,125 @@ import { bold, channelMention, ChatInputCommandInteraction, + inlineCode, + InteractionResponse, + Message, roleMention, type BaseMessageOptions, } from "discord.js"; import type { ConfigurationOption, - InferStoreReadContext, + ConfigurationOptionType, } from "./configuration-manifest.ts"; -import type { DatabaseStore } from "./database-store.ts"; -import { Config, type ConfigSelect } from "../schemas/config.ts"; import db from "../db.ts"; -import { eq, type Table } from "drizzle-orm"; +import { Table as DrizzleTable, SQL, type InferSelectModel } from "drizzle-orm"; +import type { GetSelectTableSelection } from "drizzle-orm/query-builders/select.types"; + +interface GetMessageOptionsContext { + table: T; + interaction: ChatInputCommandInteraction<"cached" | "raw">; +} + +interface ConfigurationMessageOptions { + /** Dynamically create the where clause for the database query. */ + getWhereClause: ( + context: GetMessageOptionsContext, + ) => SQL | ((aliases: GetSelectTableSelection) => SQL); +} /** Represents a configuration message. */ export class ConfigurationMessage< - Store extends DatabaseStore, - ManifestOption extends ConfigurationOption>, + Table extends DrizzleTable, + ManifestOption extends ConfigurationOption< + Table, + keyof InferSelectModel
+ >, > { - #store: Store; - #manifest: ConfigurationOption>[]; + #table: Table; + #manifest: ConfigurationOption
[]; + #options: ConfigurationMessageOptions
; + + /** The {@link InteractionResponse} or {@link Message} for the current configuration message. */ + #reply: Message | InteractionResponse | null = null; + + #initialized = false; - constructor(store: Store, manifest: ManifestOption[]) { - this.#store = store; + constructor( + table: Table, + manifest: ManifestOption[], + options: ConfigurationMessageOptions
, + ) { + this.#table = table; this.#manifest = manifest; + this.#options = options; } /** Reply with the configuration message and listen to component interactions. */ - public async initialize(interaction: ChatInputCommandInteraction) { - if (!interaction.inGuild()) return; + public async initialize( + interaction: ChatInputCommandInteraction, + ): Promise { + if (!interaction.inGuild() || this.#initialized) return; + + const messageOptions = await this.getMessageOptions(interaction); - const messageOptions = await this.getMessageOptions(interaction.guildId); + return this.replyOrEdit(interaction, messageOptions); + } + + /** Stop listening to component interactions and clean up internal state. */ + public async destroy() { + if (!this.#initialized) return; + + this.#reply = null; + } + + /** Reply to a message or edit the reply if the interaction got replied to or is deferred and keep reply in memory. */ + private async replyOrEdit( + interaction: ChatInputCommandInteraction, + messageOptions: BaseMessageOptions, + ): Promise { + let replyPromise: Promise; if (interaction.replied || interaction.deferred) { - return interaction.editReply(messageOptions); + replyPromise = interaction.editReply(messageOptions); + } else { + replyPromise = interaction.reply(messageOptions); + } + + try { + const messageOrInteractionResponse = await replyPromise; + + this.#reply = messageOrInteractionResponse; + } catch (error) { + await interaction.followUp("Something went wrong... Try again later"); + this.destroy(); } + } + + /** Update the reply message. */ + private async updateReply(messageOptions: BaseMessageOptions): Promise { + if (!this.#reply) + throw new Error( + "No internal reply message or interaction response available", + ); - return interaction.reply(messageOptions); + this.#reply = await this.#reply.edit(messageOptions); } /** Get the message options for the configuration message. */ - protected async getMessageOptions( - guildId: string, + private async getMessageOptions( + interaction: ChatInputCommandInteraction<"cached" | "raw">, ): Promise { let content = ""; + const whereClause = this.#options.getWhereClause({ + table: this.#table, + interaction, + }); + const databaseValues = db .select() - .from(Config) - .where(eq(Config.id, guildId)) + .from(this.#table) + .where(whereClause) .all() // TEMP: use .all() and select the first row manually, .get() does not work .at(0); @@ -60,9 +131,7 @@ export class ConfigurationMessage< for (const manifestOption of this.#manifest) { const databaseValue = this.getOptionValue( manifestOption, - databaseValues[ - manifestOption.storeContext.columns[0] as keyof typeof databaseValues - ], + databaseValues[manifestOption.column], ); content += `${bold(manifestOption.name)} — ${this.formatValue( @@ -76,16 +145,18 @@ export class ConfigurationMessage< }; } - protected getOptionValue( - manifestOption: ConfigurationOption>, - value: unknown, - ) { - if (manifestOption.fromStore) return manifestOption.fromStore(value); + /** Get the raw or transformed (return value of {@link ConfigurationOption.fromDatabase|fromDatabase}) database value. */ + private getOptionValue>( + manifestOption: T, + value: InferSelectModel
[T["column"]], + ): unknown { + if (manifestOption.fromDatabase) return manifestOption.fromDatabase(value); return value; } - protected formatValue(type: ConfigurationOption["type"], value: unknown) { + /** Format the value to display in an embed. */ + private formatValue(type: ConfigurationOptionType, value: unknown): string { const notSetValue = "(Not set)"; switch (type) { @@ -96,7 +167,7 @@ export class ConfigurationMessage< case "role": return value ? roleMention(value as string) : notSetValue; default: - return value ? (value as string) : notSetValue; + return value ? inlineCode(value as string) : notSetValue; } } } diff --git a/src/structures/database-store.ts b/src/structures/database-store.ts deleted file mode 100644 index 0b08e56..0000000 --- a/src/structures/database-store.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { InferInsertModel, InferSelectModel, Table } from "drizzle-orm"; -import { ConfigurationStore } from "./configuration-manifest.ts"; -import db from "../db.ts"; -import type { Snowflake } from "discord.js"; - -interface DatabaseStoreWriteContext { - /** The table to execute queries on. */ - table: T; - /** The table columns mapped to their new values. */ - values: InferInsertModel[]; -} - -interface DatabaseStoreReadContext { - /** The table to execute queries on. */ - table: T; - /** The table columns to select. */ - columns: (keyof InferSelectModel)[]; - where?: Record; -} - -type GetSchemaColumn< - Schema extends object, - T extends PropertyKey, -> = T extends keyof Schema ? Schema[T] : never; -type GetSelectFields = { - [Key in T as Key extends keyof Schema ? Key : never]: GetSchemaColumn< - Schema, - Key - >; -}; - -/** Represents a database store. */ -export class DatabaseStore extends ConfigurationStore< - DatabaseStoreWriteContext, - DatabaseStoreReadContext -> { - async write(context: T): Promise { - await db.insert(context.table).values(context.values); - } - - async read(context: T) { - const selectFields = Object.fromEntries( - context.columns.map((fieldName) => [ - fieldName, - context.table[fieldName as keyof typeof context.table], - ]), - ) as GetSelectFields< - typeof context.table, - (typeof context.columns)[number] - >; - - return db.select(selectFields).from(context.table).get(); - } -} diff --git a/src/structures/index.ts b/src/structures/index.ts index 571aa46..36defa7 100644 --- a/src/structures/index.ts +++ b/src/structures/index.ts @@ -1,3 +1,2 @@ export * from "./configuration-manifest.ts"; -export * from "./database-store.ts"; export * from "./configuration-message.ts"; From 1fb0c0c2db09bd29edf024ea2446dda05303c3ff Mon Sep 17 00:00:00 2001 From: Gus Libens <27970303+goestav@users.noreply.github.com> Date: Wed, 7 Aug 2024 01:09:55 +0200 Subject: [PATCH 05/32] refactor: split command up into subcommands --- src/commands/config.ts | 151 ------------------------ src/commands/config/gateway.ts | 75 ++++++++++++ src/commands/config/logging.ts | 68 +++++++++++ src/commands/config/suggestion.ts | 58 +++++++++ src/structures/configuration-message.ts | 25 ++-- src/utils/discordjs.ts | 33 ++++++ src/utils/error.ts | 2 + src/utils/index.ts | 2 + 8 files changed, 250 insertions(+), 164 deletions(-) delete mode 100644 src/commands/config.ts create mode 100644 src/commands/config/gateway.ts create mode 100644 src/commands/config/logging.ts create mode 100644 src/commands/config/suggestion.ts create mode 100644 src/utils/discordjs.ts create mode 100644 src/utils/error.ts diff --git a/src/commands/config.ts b/src/commands/config.ts deleted file mode 100644 index aa8dae7..0000000 --- a/src/commands/config.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { PermissionFlagsBits, TextInputStyle, type Channel } from "discord.js"; -import { Config } from "../schemas/config.ts"; -import { createConfigurationManifest } from "../structures/index.ts"; -import { LogMode } from "../types/logging.ts"; -import { ConfigurationMessage } from "../structures/index.ts"; -import type { Command } from "djs-fsrouter"; -import { eq } from "drizzle-orm"; - -const checkIsValidTextChannel = (channel: Channel) => { - if (channel.isDMBased()) return `${channel} must be a guild channel`; - - if (!channel.isTextBased()) return `${channel} must be a text channel`; - - const clientPermissionsInChannel = channel.permissionsFor( - channel.client.user, - ); - - if (!clientPermissionsInChannel?.has(PermissionFlagsBits.SendMessages)) - return `I do not have permission to send message to ${channel}`; - - return true; -}; - -const LogModeValues = Object.keys(LogMode).filter( - (item) => !Number.isNaN(Number(item)), -); - -const LogModeSelectOptions = Object.entries(LogMode) - .filter(([, value]) => typeof value === "number") - .map(([key, value]) => ({ name: key, value: value.toString() })); - -const manifest = createConfigurationManifest(Config, [ - { - name: "Gateway channel", - description: "New members will be welcomed here.", - column: "gatewayChannel", - type: "channel", - validate: checkIsValidTextChannel, - }, - // Gateway join - { - name: "Gateway join title", - description: "Message title when a user joins.", - column: "gatewayChannel", - type: "text", - // TODO: implement string placeholders - placeholder: "Welcome ${mention}!", - }, - { - name: "Gateway join content", - description: "Message content when a user joins.", - column: "gatewayChannel", - type: "text", - placeholder: "We hope you enjoy your stay!", - style: TextInputStyle.Paragraph, - }, - // Gateway leave - { - name: "Gateway leave title", - description: "Message title when a user leaves.", - column: "gatewayChannel", - type: "text", - // TODO: implement string placeholders - placeholder: "Goodbye ${mention}!", - }, - { - name: "Gateway leave content", - description: "Message content when a user leaves.", - column: "gatewayChannel", - type: "text", - // TODO: implement string placeholders - placeholder: "We are sorry to see you go ${mention}", - style: TextInputStyle.Paragraph, - }, - // Logging - { - name: "Logging mode", - description: "Determines what should be logged.", - column: "loggingMode", - type: "select", - options: LogModeSelectOptions, - validate(value) { - if (!LogModeValues.includes(value)) - return "The provided logging mode is invalid"; - - return true; - }, - toDatabase(value): number { - return Number.parseInt(value); - }, - fromDatabase(value): string { - return value ? (value as number).toString() : ""; - }, - }, - { - name: "Logging channel", - description: "Log messages will be sent here.", - column: "loggingChannel", - type: "channel", - validate: checkIsValidTextChannel, - }, - // Suggestion - { - name: "Suggestion channel", - description: "Suggestions will be sent here.", - column: "suggestionChannel", - type: "channel", - validate: checkIsValidTextChannel, - }, - { - name: "Suggestion manager role", - description: "The role that can approve and reject suggestions.", - column: "suggestionManagerRole", - type: "role", - }, - { - name: "Suggestion upvote emoji", - description: "The emoji for upvoting suggestions.", - column: "suggestionUpvoteEmoji", - type: "text", - }, - { - name: "Suggestion downvote emoji", - description: "The emoji for downvoting suggestions.", - column: "suggestionDownvoteEmoji", - type: "text", - }, -]); - -const ConfigCommand: Command = { - description: "Configure the bot", - defaultMemberPermissions: PermissionFlagsBits.Administrator, - async run(interaction) { - if (!interaction.inGuild()) { - interaction.reply({ - content: "Run this command in a server to get server info", - ephemeral: true, - }); - return; - } - - const configurationMessage = new ConfigurationMessage(Config, manifest, { - getWhereClause: ({ table, interaction }) => - eq(table.id, interaction.guildId), - }); - - await configurationMessage.initialize(interaction); - }, -}; - -export default ConfigCommand; diff --git a/src/commands/config/gateway.ts b/src/commands/config/gateway.ts new file mode 100644 index 0000000..ca32b79 --- /dev/null +++ b/src/commands/config/gateway.ts @@ -0,0 +1,75 @@ +import { PermissionFlagsBits, TextInputStyle } from "discord.js"; +import { Config } from "../../schemas/config.ts"; +import { createConfigurationManifest } from "../../structures/index.ts"; +import { ConfigurationMessage } from "../../structures/index.ts"; +import type { Command } from "djs-fsrouter"; +import { eq } from "drizzle-orm"; +import { checkIsValidTextChannel } from "../../utils/index.ts"; + +const manifest = createConfigurationManifest(Config, [ + { + name: "Gateway channel", + description: "New members will be welcomed here.", + column: "gatewayChannel", + type: "channel", + validate: checkIsValidTextChannel, + }, + // Join + { + name: "Gateway join title", + description: "Message title when a user joins.", + column: "gatewayChannel", + type: "text", + // TODO: implement string placeholders + placeholder: "Welcome ${mention}!", + }, + { + name: "Gateway join content", + description: "Message content when a user joins.", + column: "gatewayChannel", + type: "text", + placeholder: "We hope you enjoy your stay!", + style: TextInputStyle.Paragraph, + }, + // Leave + { + name: "Gateway leave title", + description: "Message title when a user leaves.", + column: "gatewayChannel", + type: "text", + // TODO: implement string placeholders + placeholder: "Goodbye ${mention}!", + }, + { + name: "Gateway leave content", + description: "Message content when a user leaves.", + column: "gatewayChannel", + type: "text", + // TODO: implement string placeholders + placeholder: "We are sorry to see you go ${mention}", + style: TextInputStyle.Paragraph, + }, +]); + +const ConfigCommand: Command = { + description: "Configure the gateway", + defaultMemberPermissions: PermissionFlagsBits.Administrator, + async run(interaction) { + if (!interaction.inGuild()) { + interaction.reply({ + content: "Run this command in a server to get server info", + ephemeral: true, + }); + return; + } + + const configurationMessage = new ConfigurationMessage(Config, manifest, { + getWhereClause: ({ table, interaction }) => + eq(table.id, interaction.guildId), + }); + + await configurationMessage.initialize(interaction); + }, +}; + +export default ConfigCommand; diff --git a/src/commands/config/logging.ts b/src/commands/config/logging.ts new file mode 100644 index 0000000..7404e0c --- /dev/null +++ b/src/commands/config/logging.ts @@ -0,0 +1,68 @@ +import { PermissionFlagsBits, type Channel } from "discord.js"; +import { Config } from "../../schemas/config.ts"; +import { createConfigurationManifest } from "../../structures/index.ts"; +import { ConfigurationMessage } from "../../structures/index.ts"; +import { checkIsValidTextChannel } from "../../utils/index.ts"; +import type { Command } from "djs-fsrouter"; +import { eq } from "drizzle-orm"; +import { LogMode } from "../../types/logging.ts"; + +const LogModeValues = Object.keys(LogMode).filter( + (item) => !Number.isNaN(Number(item)), +); + +const LogModeSelectOptions = Object.entries(LogMode) + .filter(([, value]) => typeof value === "number") + .map(([key, value]) => ({ name: key, value: value.toString() })); + +const manifest = createConfigurationManifest(Config, [ + { + name: "Logging mode", + description: "Determines what should be logged.", + column: "loggingMode", + type: "select", + options: LogModeSelectOptions, + validate(value) { + if (!LogModeValues.includes(value)) + return "The provided logging mode is invalid"; + + return true; + }, + toDatabase(value): number { + return Number.parseInt(value); + }, + fromDatabase(value): string { + return value ? (value as number).toString() : ""; + }, + }, + { + name: "Logging channel", + description: "Log messages will be sent here.", + column: "loggingChannel", + type: "channel", + validate: checkIsValidTextChannel, + }, +]); + +const ConfigCommand: Command = { + description: "Configure suggestion management", + defaultMemberPermissions: PermissionFlagsBits.Administrator, + async run(interaction) { + if (!interaction.inGuild()) { + interaction.reply({ + content: "Run this command in a server to get server info", + ephemeral: true, + }); + return; + } + + const configurationMessage = new ConfigurationMessage(Config, manifest, { + getWhereClause: ({ table, interaction }) => + eq(table.id, interaction.guildId), + }); + + await configurationMessage.initialize(interaction); + }, +}; + +export default ConfigCommand; diff --git a/src/commands/config/suggestion.ts b/src/commands/config/suggestion.ts new file mode 100644 index 0000000..3c62ce7 --- /dev/null +++ b/src/commands/config/suggestion.ts @@ -0,0 +1,58 @@ +import { PermissionFlagsBits } from "discord.js"; +import { Config } from "../../schemas/config.ts"; +import { createConfigurationManifest } from "../../structures/index.ts"; +import { ConfigurationMessage } from "../../structures/index.ts"; +import { checkIsValidTextChannel } from "../../utils/index.ts"; +import type { Command } from "djs-fsrouter"; +import { eq } from "drizzle-orm"; + +const manifest = createConfigurationManifest(Config, [ + { + name: "Suggestion channel", + description: "Suggestions will be sent here.", + column: "suggestionChannel", + type: "channel", + validate: checkIsValidTextChannel, + }, + { + name: "Suggestion manager role", + description: "The role that can approve and reject suggestions.", + column: "suggestionManagerRole", + type: "role", + }, + { + name: "Suggestion upvote emoji", + description: "The emoji for upvoting suggestions.", + column: "suggestionUpvoteEmoji", + type: "text", + }, + { + name: "Suggestion downvote emoji", + description: "The emoji for downvoting suggestions.", + column: "suggestionDownvoteEmoji", + type: "text", + }, +]); + +const ConfigCommand: Command = { + description: "Configure suggestion management", + defaultMemberPermissions: PermissionFlagsBits.Administrator, + async run(interaction) { + if (!interaction.inGuild()) { + interaction.reply({ + content: "Run this command in a server to get server info", + ephemeral: true, + }); + return; + } + + const configurationMessage = new ConfigurationMessage(Config, manifest, { + getWhereClause: ({ table, interaction }) => + eq(table.id, interaction.guildId), + }); + + await configurationMessage.initialize(interaction); + }, +}; + +export default ConfigCommand; diff --git a/src/structures/configuration-message.ts b/src/structures/configuration-message.ts index 79dc8fe..d136e7d 100644 --- a/src/structures/configuration-message.ts +++ b/src/structures/configuration-message.ts @@ -4,6 +4,7 @@ import { ChatInputCommandInteraction, inlineCode, InteractionResponse, + italic, Message, roleMention, type BaseMessageOptions, @@ -29,15 +30,9 @@ interface ConfigurationMessageOptions { } /** Represents a configuration message. */ -export class ConfigurationMessage< - Table extends DrizzleTable, - ManifestOption extends ConfigurationOption< - Table, - keyof InferSelectModel
- >, -> { +export class ConfigurationMessage
{ #table: Table; - #manifest: ConfigurationOption
[]; + #manifest: ConfigurationOption[]; #options: ConfigurationMessageOptions
; /** The {@link InteractionResponse} or {@link Message} for the current configuration message. */ @@ -47,7 +42,7 @@ export class ConfigurationMessage< constructor( table: Table, - manifest: ManifestOption[], + manifest: ConfigurationOption[], options: ConfigurationMessageOptions
, ) { this.#table = table; @@ -134,10 +129,14 @@ export class ConfigurationMessage< databaseValues[manifestOption.column], ); - content += `${bold(manifestOption.name)} — ${this.formatValue( + const nameFormatted = bold(manifestOption.name); + const descriptionFormatted = italic(manifestOption.description); + const valueFormatted = this.formatValue( manifestOption.type, databaseValue, - )}\n`; + ); + + content += `${nameFormatted} — ${valueFormatted}\n-# ${descriptionFormatted}\n\n`; } return { @@ -146,9 +145,9 @@ export class ConfigurationMessage< } /** Get the raw or transformed (return value of {@link ConfigurationOption.fromDatabase|fromDatabase}) database value. */ - private getOptionValue>( + private getOptionValue>( manifestOption: T, - value: InferSelectModel
[T["column"]], + value: InferSelectModel[T["column"]], ): unknown { if (manifestOption.fromDatabase) return manifestOption.fromDatabase(value); diff --git a/src/utils/discordjs.ts b/src/utils/discordjs.ts new file mode 100644 index 0000000..0c866d6 --- /dev/null +++ b/src/utils/discordjs.ts @@ -0,0 +1,33 @@ +import { + PermissionFlagsBits, + type Channel, + type GuildBasedChannel, + type TextBasedChannel, +} from "discord.js"; +import { UserError } from "./error.ts"; + +/** + * Check if a channel is a guild text channel the client can send messages in. + * + * @throws {UserError} + */ +export const checkIsValidTextChannel = ( + channel: Channel, +): channel is GuildBasedChannel & TextBasedChannel => { + if (channel.isDMBased()) + throw new UserError(`${channel} must be a guild channel`); + + if (!channel.isTextBased()) + throw new UserError(`${channel} must be a text channel`); + + const clientPermissionsInChannel = channel.permissionsFor( + channel.client.user, + ); + + if (!clientPermissionsInChannel?.has(PermissionFlagsBits.SendMessages)) + throw new UserError( + `I do not have permission to send message to ${channel}`, + ); + + return true; +}; diff --git a/src/utils/error.ts b/src/utils/error.ts new file mode 100644 index 0000000..30c746d --- /dev/null +++ b/src/utils/error.ts @@ -0,0 +1,2 @@ +/** Represents an error that will be displayed to the user by the client. */ +export class UserError extends Error {} diff --git a/src/utils/index.ts b/src/utils/index.ts index f2cc60e..bc458b5 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,2 +1,4 @@ export * from "./drizzle.ts"; export * from "./common.ts"; +export * from "./discordjs.ts"; +export * from "./error.ts"; From ae3cd5ee96c64203b9014be5e797055520a4a67b Mon Sep 17 00:00:00 2001 From: Gus Libens <27970303+goestav@users.noreply.github.com> Date: Wed, 7 Aug 2024 01:55:11 +0200 Subject: [PATCH 06/32] refactor: add components and move settings to embed --- src/commands/config/gateway.ts | 1 + src/commands/config/logging.ts | 4 +- src/commands/config/suggestion.ts | 6 ++ src/structures/configuration-manifest.ts | 23 ++++- src/structures/configuration-message.ts | 109 ++++++++++++++++++++++- 5 files changed, 138 insertions(+), 5 deletions(-) diff --git a/src/commands/config/gateway.ts b/src/commands/config/gateway.ts index ca32b79..3b10d00 100644 --- a/src/commands/config/gateway.ts +++ b/src/commands/config/gateway.ts @@ -12,6 +12,7 @@ const manifest = createConfigurationManifest(Config, [ description: "New members will be welcomed here.", column: "gatewayChannel", type: "channel", + placeholder: "Select a gateway channel", validate: checkIsValidTextChannel, }, // Join diff --git a/src/commands/config/logging.ts b/src/commands/config/logging.ts index 7404e0c..5db36c5 100644 --- a/src/commands/config/logging.ts +++ b/src/commands/config/logging.ts @@ -13,7 +13,7 @@ const LogModeValues = Object.keys(LogMode).filter( const LogModeSelectOptions = Object.entries(LogMode) .filter(([, value]) => typeof value === "number") - .map(([key, value]) => ({ name: key, value: value.toString() })); + .map(([key, value]) => ({ label: key, value: value.toString() })); const manifest = createConfigurationManifest(Config, [ { @@ -21,6 +21,7 @@ const manifest = createConfigurationManifest(Config, [ description: "Determines what should be logged.", column: "loggingMode", type: "select", + placeholder: "Select a logging mode", options: LogModeSelectOptions, validate(value) { if (!LogModeValues.includes(value)) @@ -40,6 +41,7 @@ const manifest = createConfigurationManifest(Config, [ description: "Log messages will be sent here.", column: "loggingChannel", type: "channel", + placeholder: "Select a logging channel", validate: checkIsValidTextChannel, }, ]); diff --git a/src/commands/config/suggestion.ts b/src/commands/config/suggestion.ts index 3c62ce7..ee69504 100644 --- a/src/commands/config/suggestion.ts +++ b/src/commands/config/suggestion.ts @@ -12,6 +12,7 @@ const manifest = createConfigurationManifest(Config, [ description: "Suggestions will be sent here.", column: "suggestionChannel", type: "channel", + placeholder: "Select a suggestion channel", validate: checkIsValidTextChannel, }, { @@ -19,18 +20,23 @@ const manifest = createConfigurationManifest(Config, [ description: "The role that can approve and reject suggestions.", column: "suggestionManagerRole", type: "role", + placeholder: "Select a manager role", }, { name: "Suggestion upvote emoji", description: "The emoji for upvoting suggestions.", column: "suggestionUpvoteEmoji", type: "text", + label: "Set upvote emoji", + emoji: "👍", }, { name: "Suggestion downvote emoji", description: "The emoji for downvoting suggestions.", column: "suggestionDownvoteEmoji", type: "text", + label: "Set downvote emoji", + emoji: "👎", }, ]); diff --git a/src/structures/configuration-manifest.ts b/src/structures/configuration-manifest.ts index 4e58e9d..4a15b0f 100644 --- a/src/structures/configuration-manifest.ts +++ b/src/structures/configuration-manifest.ts @@ -45,10 +45,24 @@ interface PartialConfigurationOption< column: Column; } +interface ButtonConfigurationOption< + Type extends ConfigurationOptionType, + Table extends DrizzleTable, + Column extends keyof InferSelectModel
, +> extends PartialConfigurationOption { + /** + * The label to display in the button.\ + * **NOTE:** this will fallback to the {@link ConfigurationTextOption.name|name} property if not defined. + */ + label?: string; + /** The emoji to display in the button before the {@link label}. */ + emoji?: string; +} + export interface ConfigurationTextOption< Table extends DrizzleTable, Column extends keyof InferSelectModel
, -> extends PartialConfigurationOption<"text", Table, Column> { +> extends ButtonConfigurationOption<"text", Table, Column> { type: "text"; placeholder?: string; /** @@ -62,7 +76,7 @@ export interface ConfigurationTextOption< export interface ConfigurationBooleanOption< Table extends DrizzleTable, Column extends keyof InferSelectModel
, -> extends PartialConfigurationOption<"boolean", Table, Column> { +> extends ButtonConfigurationOption<"boolean", Table, Column> { type: "boolean"; } @@ -71,6 +85,7 @@ export interface ConfigurationRoleOption< Column extends keyof InferSelectModel
, > extends PartialConfigurationOption<"role", Table, Column> { type: "role"; + placeholder?: string; } export interface ConfigurationChannelOption< @@ -78,6 +93,7 @@ export interface ConfigurationChannelOption< Column extends keyof InferSelectModel
, > extends PartialConfigurationOption<"channel", Table, Column> { type: "channel"; + placeholder?: string; } export interface ConfigurationSelectOption< @@ -85,8 +101,9 @@ export interface ConfigurationSelectOption< Column extends keyof InferSelectModel
, > extends PartialConfigurationOption<"select", Table, Column> { type: "select"; + placeholder?: string; options: { - name: string; + label: string; value: string; }[]; } diff --git a/src/structures/configuration-message.ts b/src/structures/configuration-message.ts index d136e7d..e83c920 100644 --- a/src/structures/configuration-message.ts +++ b/src/structures/configuration-message.ts @@ -1,13 +1,28 @@ import { + ActionRowBuilder, bold, + ButtonBuilder, + ButtonStyle, channelMention, + ChannelSelectMenuBuilder, ChatInputCommandInteraction, + EmbedBuilder, inlineCode, InteractionResponse, italic, Message, roleMention, + RoleSelectMenuBuilder, + StringSelectMenuBuilder, + type ActionRowComponent, + type AnyComponentBuilder, + type APIActionRowComponent, + type APIButtonComponent, + type APIMessageActionRowComponent, + type APISelectMenuComponent, + type APITextInputComponent, type BaseMessageOptions, + type MessageActionRowComponentBuilder, } from "discord.js"; import type { ConfigurationOption, @@ -101,6 +116,91 @@ export class ConfigurationMessage
{ this.#reply = await this.#reply.edit(messageOptions); } + /** Get the action rows with components. */ + private getActionRows(): APIActionRowComponent[] { + const actionRows: ActionRowBuilder[] = [ + new ActionRowBuilder(), + ]; + + for (const manifestOption of this.#manifest) { + const component = this.getActionRowComponent(manifestOption); + + if (["select", "channel", "role"].includes(manifestOption.type)) { + const row = + new ActionRowBuilder().addComponents( + component, + ); + + actionRows.push(row); + } else { + // Ensure that buttons are always in the first action row + actionRows[0].addComponents(component); + } + } + + return actionRows.map((row) => row.toJSON()); + } + + /** Get the component for a manifest option. */ + private getActionRowComponent( + manifestOption: ConfigurationOption, + ): MessageActionRowComponentBuilder { + const customId = `config-message-${manifestOption.name}`; + + switch (manifestOption.type) { + case "text": { + const component = new ButtonBuilder() + .setCustomId(customId) + .setLabel(manifestOption.label ?? manifestOption.name) + .setStyle(ButtonStyle.Primary); + + if (manifestOption.emoji) component.setEmoji(manifestOption.emoji); + + return component; + } + case "boolean": { + const component = new ButtonBuilder() + .setCustomId(customId) + .setLabel(manifestOption.label ?? manifestOption.name); + + if (manifestOption.emoji) component.setEmoji(manifestOption.emoji); + + return component; + } + case "channel": { + const component = new ChannelSelectMenuBuilder() + .setCustomId(customId) + .setMaxValues(1); + + if (manifestOption.placeholder) + component.setPlaceholder(manifestOption.placeholder); + + return component; + } + case "role": { + const component = new RoleSelectMenuBuilder() + .setCustomId(customId) + .setMaxValues(1); + + if (manifestOption.placeholder) + component.setPlaceholder(manifestOption.placeholder); + + return component; + } + case "select": { + const component = new StringSelectMenuBuilder() + .setCustomId(customId) + .addOptions(manifestOption.options) + .setMaxValues(1); + + if (manifestOption.placeholder) + component.setPlaceholder(manifestOption.placeholder); + + return component; + } + } + } + /** Get the message options for the configuration message. */ private async getMessageOptions( interaction: ChatInputCommandInteraction<"cached" | "raw">, @@ -139,8 +239,15 @@ export class ConfigurationMessage
{ content += `${nameFormatted} — ${valueFormatted}\n-# ${descriptionFormatted}\n\n`; } + const embed = new EmbedBuilder() + .setColor("Blue") + // Should be able to configure through the constructor + .setTitle("Configuration") + .setDescription(content.trim()); + return { - content: content.trim(), + embeds: [embed], + components: this.getActionRows(), }; } From 7a055396605218d40ea66950a8539629af7c16b7 Mon Sep 17 00:00:00 2001 From: Gus Libens <27970303+goestav@users.noreply.github.com> Date: Wed, 7 Aug 2024 19:36:21 +0200 Subject: [PATCH 07/32] refactor: remove unused type imports --- src/structures/configuration-message.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/structures/configuration-message.ts b/src/structures/configuration-message.ts index e83c920..c0ea7c5 100644 --- a/src/structures/configuration-message.ts +++ b/src/structures/configuration-message.ts @@ -14,13 +14,8 @@ import { roleMention, RoleSelectMenuBuilder, StringSelectMenuBuilder, - type ActionRowComponent, - type AnyComponentBuilder, type APIActionRowComponent, - type APIButtonComponent, type APIMessageActionRowComponent, - type APISelectMenuComponent, - type APITextInputComponent, type BaseMessageOptions, type MessageActionRowComponentBuilder, } from "discord.js"; From db5823a040be82e19b31ffc6e2bc0bae656012e5 Mon Sep 17 00:00:00 2001 From: Gus Libens <27970303+goestav@users.noreply.github.com> Date: Thu, 8 Aug 2024 01:29:02 +0200 Subject: [PATCH 08/32] refactor: handle text option interactions --- src/commands/config/gateway.ts | 8 +- .../configuration-manifest.ts | 0 .../configuration-message.ts | 180 +++++++++++++++--- src/structures/configuration/index.ts | 2 + .../configuration/interaction-handlers.ts | 96 ++++++++++ src/structures/configuration/utils.ts | 16 ++ src/structures/index.ts | 3 +- 7 files changed, 277 insertions(+), 28 deletions(-) rename src/structures/{ => configuration}/configuration-manifest.ts (100%) rename src/structures/{ => configuration}/configuration-message.ts (60%) create mode 100644 src/structures/configuration/index.ts create mode 100644 src/structures/configuration/interaction-handlers.ts create mode 100644 src/structures/configuration/utils.ts diff --git a/src/commands/config/gateway.ts b/src/commands/config/gateway.ts index 3b10d00..fb9abab 100644 --- a/src/commands/config/gateway.ts +++ b/src/commands/config/gateway.ts @@ -19,7 +19,7 @@ const manifest = createConfigurationManifest(Config, [ { name: "Gateway join title", description: "Message title when a user joins.", - column: "gatewayChannel", + column: "gatewayJoinTitle", type: "text", // TODO: implement string placeholders placeholder: "Welcome ${mention}!", @@ -27,7 +27,7 @@ const manifest = createConfigurationManifest(Config, [ { name: "Gateway join content", description: "Message content when a user joins.", - column: "gatewayChannel", + column: "gatewayJoinContent", type: "text", placeholder: "We hope you enjoy your stay!", style: TextInputStyle.Paragraph, @@ -36,7 +36,7 @@ const manifest = createConfigurationManifest(Config, [ { name: "Gateway leave title", description: "Message title when a user leaves.", - column: "gatewayChannel", + column: "gatewayLeaveTitle", type: "text", // TODO: implement string placeholders placeholder: "Goodbye ${mention}!", @@ -44,7 +44,7 @@ const manifest = createConfigurationManifest(Config, [ { name: "Gateway leave content", description: "Message content when a user leaves.", - column: "gatewayChannel", + column: "gatewayLeaveContent", type: "text", // TODO: implement string placeholders placeholder: "We are sorry to see you go ${mention}", diff --git a/src/structures/configuration-manifest.ts b/src/structures/configuration/configuration-manifest.ts similarity index 100% rename from src/structures/configuration-manifest.ts rename to src/structures/configuration/configuration-manifest.ts diff --git a/src/structures/configuration-message.ts b/src/structures/configuration/configuration-message.ts similarity index 60% rename from src/structures/configuration-message.ts rename to src/structures/configuration/configuration-message.ts index c0ea7c5..3d12e29 100644 --- a/src/structures/configuration-message.ts +++ b/src/structures/configuration/configuration-message.ts @@ -6,37 +6,63 @@ import { channelMention, ChannelSelectMenuBuilder, ChatInputCommandInteraction, + codeBlock, + DiscordjsError, + DiscordjsErrorCodes, EmbedBuilder, inlineCode, InteractionResponse, italic, Message, + MessageComponentInteraction, + ModalBuilder, + ModalSubmitInteraction, roleMention, RoleSelectMenuBuilder, StringSelectMenuBuilder, + TextInputBuilder, + TextInputStyle, type APIActionRowComponent, type APIMessageActionRowComponent, type BaseMessageOptions, + type CollectedInteraction, type MessageActionRowComponentBuilder, + type ModalActionRowComponentBuilder, } from "discord.js"; import type { ConfigurationOption, ConfigurationOptionType, } from "./configuration-manifest.ts"; -import db from "../db.ts"; -import { Table as DrizzleTable, SQL, type InferSelectModel } from "drizzle-orm"; -import type { GetSelectTableSelection } from "drizzle-orm/query-builders/select.types"; +import db from "../../db.ts"; +import { + and, + Table as DrizzleTable, + eq, + SQL, + type InferSelectModel, +} from "drizzle-orm"; +import { Time } from "../../utils.ts"; +import { truncate } from "../../utils/common.ts"; +import { handleInteractionCollect } from "./interaction-handlers.ts"; +import { getCustomId } from "./utils.ts"; interface GetMessageOptionsContext { table: T; - interaction: ChatInputCommandInteraction<"cached" | "raw">; + interaction: + | ChatInputCommandInteraction<"cached" | "raw"> + | CollectedInteraction<"cached" | "raw"> + | MessageComponentInteraction<"cached" | "raw">; } interface ConfigurationMessageOptions { /** Dynamically create the where clause for the database query. */ - getWhereClause: ( - context: GetMessageOptionsContext, - ) => SQL | ((aliases: GetSelectTableSelection) => SQL); + getWhereClause: (context: GetMessageOptionsContext) => SQL; + /** + * How long will this configuration message be interactable. + * + * @default 600_000 // 10 minutes + */ + expiresInMs?: number; } /** Represents a configuration message. */ @@ -50,6 +76,9 @@ export class ConfigurationMessage
{ #initialized = false; + /** The text for configuration options that aren't set */ + #notSetText = italic("(Not set)"); + constructor( table: Table, manifest: ConfigurationOption[], @@ -68,7 +97,9 @@ export class ConfigurationMessage
{ const messageOptions = await this.getMessageOptions(interaction); - return this.replyOrEdit(interaction, messageOptions); + await this.replyOrEdit(interaction, messageOptions); + + await this.initializeListeners(); } /** Stop listening to component interactions and clean up internal state. */ @@ -78,6 +109,94 @@ export class ConfigurationMessage
{ this.#reply = null; } + /** Add the component interaction listeners. */ + private initializeListeners() { + const manifestOptionMap = Object.fromEntries( + this.#manifest.map((option) => [getCustomId(option), option]), + ); + + if (!this.#reply) + throw new Error( + "No internal reply message or interaction response available", + ); + + const collector = this.#reply.createMessageComponentCollector({ + filter: async (interaction) => { + const isValidCustomId = Object.keys(manifestOptionMap).includes( + interaction.customId, + ); + const message = await this.getReplyMessage(); + const isReplyAuthor = message.author.id === interaction.user.id; + + return isValidCustomId || isReplyAuthor; + }, + time: Time.Minute * 10, + }); + + collector.on("collect", async (interaction) => { + if (!interaction.inGuild()) + throw new Error("Interaction happened outside a guild"); + + this.handleInteractionCollect( + interaction, + manifestOptionMap[interaction.customId], + ); + }); + } + + /** Get the {@link Message} for the configuration message reply. */ + private async getReplyMessage() { + if (!this.#reply) + throw new Error( + "No internal reply message or interaction response available", + ); + + return this.#reply instanceof Message ? this.#reply : this.#reply.fetch(); + } + + private async handleInteractionCollect( + interaction: MessageComponentInteraction<"cached" | "raw">, + manifestOption: ConfigurationOption, + ) { + const whereClause = this.#options.getWhereClause({ + table: this.#table, + interaction, + }); + + const { value } = db + .select({ + value: this.#table[manifestOption.column as keyof object], + }) + .from(this.#table) + .where(and(whereClause)) + .all() + // TEMP: use .all() and select the first row manually, .get() does not work + .at(0) as { value: unknown }; + + const handleCollectResult = await handleInteractionCollect( + interaction, + manifestOption, + value, + ); + + if (handleCollectResult === null) return; + + const [followUpInteraction, updatedValue] = handleCollectResult; + + db.update(this.#table) + .set({ + [manifestOption.column as keyof object]: updatedValue + ? updatedValue + : null, + }) + .where(whereClause) + .run(); + + const messageOptions = await this.getMessageOptions(followUpInteraction); + + this.updateReply(messageOptions); + } + /** Reply to a message or edit the reply if the interaction got replied to or is deferred and keep reply in memory. */ private async replyOrEdit( interaction: ChatInputCommandInteraction, @@ -140,7 +259,7 @@ export class ConfigurationMessage
{ private getActionRowComponent( manifestOption: ConfigurationOption, ): MessageActionRowComponentBuilder { - const customId = `config-message-${manifestOption.name}`; + const customId = getCustomId(manifestOption); switch (manifestOption.type) { case "text": { @@ -167,6 +286,8 @@ export class ConfigurationMessage
{ .setCustomId(customId) .setMaxValues(1); + // TODO: set channel type + if (manifestOption.placeholder) component.setPlaceholder(manifestOption.placeholder); @@ -198,7 +319,9 @@ export class ConfigurationMessage
{ /** Get the message options for the configuration message. */ private async getMessageOptions( - interaction: ChatInputCommandInteraction<"cached" | "raw">, + interaction: + | ChatInputCommandInteraction<"cached" | "raw"> + | CollectedInteraction<"cached" | "raw">, ): Promise { let content = ""; @@ -226,12 +349,24 @@ export class ConfigurationMessage
{ const nameFormatted = bold(manifestOption.name); const descriptionFormatted = italic(manifestOption.description); - const valueFormatted = this.formatValue( - manifestOption.type, - databaseValue, - ); - content += `${nameFormatted} — ${valueFormatted}\n-# ${descriptionFormatted}\n\n`; + if ( + manifestOption.type === "text" && + manifestOption.style === TextInputStyle.Paragraph + ) { + const valueFormatted = databaseValue + ? codeBlock(truncate(databaseValue as string, 40)) + : this.#notSetText; + + content += `${nameFormatted}\n-# ${descriptionFormatted}\n${valueFormatted}\n`; + } else { + const valueFormatted = this.formatValue( + manifestOption.type, + databaseValue, + ); + + content += `${nameFormatted} — ${valueFormatted}\n-# ${descriptionFormatted}\n\n`; + } } const embed = new EmbedBuilder() @@ -258,17 +393,18 @@ export class ConfigurationMessage
{ /** Format the value to display in an embed. */ private formatValue(type: ConfigurationOptionType, value: unknown): string { - const notSetValue = "(Not set)"; - switch (type) { - case "boolean": - return (value as boolean) ? "Yes" : "No"; + case "boolean": { + if (typeof value !== "boolean") return this.#notSetText; + + return value ? "Yes" : "No"; + } case "channel": - return value ? channelMention(value as string) : notSetValue; + return value ? channelMention(value as string) : this.#notSetText; case "role": - return value ? roleMention(value as string) : notSetValue; + return value ? roleMention(value as string) : this.#notSetText; default: - return value ? inlineCode(value as string) : notSetValue; + return value ? inlineCode(value as string) : this.#notSetText; } } } diff --git a/src/structures/configuration/index.ts b/src/structures/configuration/index.ts new file mode 100644 index 0000000..36defa7 --- /dev/null +++ b/src/structures/configuration/index.ts @@ -0,0 +1,2 @@ +export * from "./configuration-manifest.ts"; +export * from "./configuration-message.ts"; diff --git a/src/structures/configuration/interaction-handlers.ts b/src/structures/configuration/interaction-handlers.ts new file mode 100644 index 0000000..4a17c34 --- /dev/null +++ b/src/structures/configuration/interaction-handlers.ts @@ -0,0 +1,96 @@ +import { + ActionRowBuilder, + DiscordjsError, + DiscordjsErrorCodes, + ModalBuilder, + ModalSubmitInteraction, + TextInputBuilder, + TextInputStyle, + type CollectedInteraction, + type MessageComponentInteraction, + type ModalActionRowComponentBuilder, +} from "discord.js"; +import type { ConfigurationOption } from "./configuration-manifest.ts"; +import type { Table as DrizzleTable } from "drizzle-orm"; +import { Time } from "../../utils.ts"; +import { getCustomId } from "./utils.ts"; + +/** Handle the interaction for a text option. */ +const handleTextInteractionCollect = async ( + interaction: MessageComponentInteraction, + manifestOption: ConfigurationOption & { type: "text" }, + modalInputCustomId: string, + value: string | null, +) => { + let modalSubmitInteraction: ModalSubmitInteraction<"cached" | "raw">; + const modalCustomId = getCustomId(manifestOption, "modal"); + + // Use a modal to get the updated value + const modalInput = new TextInputBuilder() + .setCustomId(modalInputCustomId) + // TODO: add optional modalLabel property to manifest option to override this fallback + .setLabel(manifestOption.name) + .setValue(value ?? "") + .setPlaceholder(manifestOption.placeholder ?? "") + .setStyle(manifestOption.style ?? TextInputStyle.Short); + + const actionRow = + new ActionRowBuilder().addComponents( + modalInput, + ); + const modal = new ModalBuilder() + .setTitle(manifestOption.name) + .setCustomId(modalCustomId) + .addComponents(actionRow); + + await interaction.showModal(modal); + + try { + return (await interaction.awaitModalSubmit({ + time: Time.Minute * 2, + })) as ModalSubmitInteraction<"cached" | "raw">; + } catch (error) { + // Ignore no interactions due to timeout error + if ( + error instanceof DiscordjsError && + error.code === DiscordjsErrorCodes.InteractionCollectorError && + error.message === + "Collector received no interactions before ending with reason: time" + ) + return null; + + throw error; + } +}; + +/** Handle any kind of message component interaction. */ +export const handleInteractionCollect = async ( + interaction: MessageComponentInteraction, + manifestOption: ConfigurationOption, + value: unknown, +): Promise<[CollectedInteraction<"cached" | "raw">, unknown] | null> => { + let updatedValue: unknown; + let followUpInteraction!: CollectedInteraction<"cached" | "raw">; + + if (manifestOption.type === "text") { + const modalInputCustomId = getCustomId(manifestOption, "modal-input"); + const modalSubmitInteraction = await handleTextInteractionCollect( + interaction, + manifestOption, + modalInputCustomId, + value as string | null, + ); + + // Return early because the component timed out + if (modalSubmitInteraction === null) return null; + + updatedValue = + modalSubmitInteraction?.fields.getTextInputValue(modalInputCustomId); + followUpInteraction = modalSubmitInteraction; + } + + // Silently acknowledge user input interaction + await followUpInteraction.deferUpdate(); + + return [followUpInteraction, updatedValue]; +}; diff --git a/src/structures/configuration/utils.ts b/src/structures/configuration/utils.ts new file mode 100644 index 0000000..9e5febe --- /dev/null +++ b/src/structures/configuration/utils.ts @@ -0,0 +1,16 @@ +import type { Table } from "drizzle-orm"; +import type { ConfigurationOption } from "./configuration-manifest.ts"; + +/** + * Get the custom ID for a manifest option. + * + * @private + */ +export const getCustomId = ( + manifestOption: ConfigurationOption
, + suffix?: string, +) => { + const _suffix = suffix ? `-${suffix}` : ""; + + return `config-message-${manifestOption.column}${_suffix}`; +}; diff --git a/src/structures/index.ts b/src/structures/index.ts index 36defa7..9aa194b 100644 --- a/src/structures/index.ts +++ b/src/structures/index.ts @@ -1,2 +1 @@ -export * from "./configuration-manifest.ts"; -export * from "./configuration-message.ts"; +export * from "./configuration/index.ts"; From 39591405afc694e932450de223f6419754736bab Mon Sep 17 00:00:00 2001 From: Gus Libens <27970303+goestav@users.noreply.github.com> Date: Thu, 8 Aug 2024 11:57:24 +0200 Subject: [PATCH 09/32] refactor: increase formatter line width to 120 characters --- biome.json | 3 +- src/commands/config/gateway.ts | 3 +- src/commands/config/logging.ts | 10 +- src/commands/config/suggestion.ts | 3 +- src/commands/delete-and-warn.ts | 9 +- src/commands/info.ts | 15 +-- src/commands/logging/$info.js | 2 +- src/commands/logging/clear.ts | 36 ++---- src/commands/mdn.ts | 17 +-- src/commands/purge.ts | 4 +- src/components.ts | 21 +--- src/index.ts | 5 +- src/listeners/delete-and-warn.ts | 19 +-- src/listeners/join-leave-message.ts | 25 +--- src/listeners/logging.ts | 19 +-- src/listeners/new-suggestion.ts | 8 +- src/listeners/suggestion-interaction.ts | 93 ++++---------- src/logging.ts | 12 +- src/schemas/config.ts | 5 +- src/schemas/suggestion.ts | 21 +--- .../configuration/configuration-manifest.ts | 47 +++----- .../configuration/configuration-message.ts | 114 ++++-------------- .../configuration/interaction-handlers.ts | 16 +-- src/structures/configuration/utils.ts | 5 +- src/structures/suggestion-util.ts | 22 +--- src/structures/suggestion.ts | 83 +++---------- src/types/env.ts | 22 +--- src/utils.ts | 17 +-- src/utils/discordjs.ts | 25 +--- 29 files changed, 159 insertions(+), 522 deletions(-) diff --git a/biome.json b/biome.json index f21da6d..daf8f2d 100644 --- a/biome.json +++ b/biome.json @@ -10,6 +10,7 @@ } }, "formatter": { - "indentStyle": "tab" + "indentStyle": "tab", + "lineWidth": 120 } } diff --git a/src/commands/config/gateway.ts b/src/commands/config/gateway.ts index fb9abab..14e9533 100644 --- a/src/commands/config/gateway.ts +++ b/src/commands/config/gateway.ts @@ -65,8 +65,7 @@ const ConfigCommand: Command = { } const configurationMessage = new ConfigurationMessage(Config, manifest, { - getWhereClause: ({ table, interaction }) => - eq(table.id, interaction.guildId), + getWhereClause: ({ table, interaction }) => eq(table.id, interaction.guildId), }); await configurationMessage.initialize(interaction); diff --git a/src/commands/config/logging.ts b/src/commands/config/logging.ts index 5db36c5..9b54903 100644 --- a/src/commands/config/logging.ts +++ b/src/commands/config/logging.ts @@ -7,9 +7,7 @@ import type { Command } from "djs-fsrouter"; import { eq } from "drizzle-orm"; import { LogMode } from "../../types/logging.ts"; -const LogModeValues = Object.keys(LogMode).filter( - (item) => !Number.isNaN(Number(item)), -); +const LogModeValues = Object.keys(LogMode).filter((item) => !Number.isNaN(Number(item))); const LogModeSelectOptions = Object.entries(LogMode) .filter(([, value]) => typeof value === "number") @@ -24,8 +22,7 @@ const manifest = createConfigurationManifest(Config, [ placeholder: "Select a logging mode", options: LogModeSelectOptions, validate(value) { - if (!LogModeValues.includes(value)) - return "The provided logging mode is invalid"; + if (!LogModeValues.includes(value)) return "The provided logging mode is invalid"; return true; }, @@ -59,8 +56,7 @@ const ConfigCommand: Command = { } const configurationMessage = new ConfigurationMessage(Config, manifest, { - getWhereClause: ({ table, interaction }) => - eq(table.id, interaction.guildId), + getWhereClause: ({ table, interaction }) => eq(table.id, interaction.guildId), }); await configurationMessage.initialize(interaction); diff --git a/src/commands/config/suggestion.ts b/src/commands/config/suggestion.ts index ee69504..7af4404 100644 --- a/src/commands/config/suggestion.ts +++ b/src/commands/config/suggestion.ts @@ -53,8 +53,7 @@ const ConfigCommand: Command = { } const configurationMessage = new ConfigurationMessage(Config, manifest, { - getWhereClause: ({ table, interaction }) => - eq(table.id, interaction.guildId), + getWhereClause: ({ table, interaction }) => eq(table.id, interaction.guildId), }); await configurationMessage.initialize(interaction); diff --git a/src/commands/delete-and-warn.ts b/src/commands/delete-and-warn.ts index a137ffd..6f51129 100644 --- a/src/commands/delete-and-warn.ts +++ b/src/commands/delete-and-warn.ts @@ -1,8 +1,4 @@ -import { - PermissionFlagsBits, - ApplicationCommandType, - TextInputStyle, -} from "discord.js"; +import { PermissionFlagsBits, ApplicationCommandType, TextInputStyle } from "discord.js"; const { ManageMessages, ModerateMembers } = PermissionFlagsBits; import { modalInput } from "../components.ts"; import type { MessageCommand } from "djs-fsrouter"; @@ -29,8 +25,7 @@ const DeleteAndWarn: MessageCommand = { if (!targetMessage.deletable) { return interaction.reply({ ephemeral: true, - content: - "I do not have the permission to delete messages in this channel.", + content: "I do not have the permission to delete messages in this channel.", }); } const { id, author } = targetMessage; diff --git a/src/commands/info.ts b/src/commands/info.ts index 2a8b65a..dc8d2d0 100644 --- a/src/commands/info.ts +++ b/src/commands/info.ts @@ -1,9 +1,4 @@ -import { - type ChatInputCommandInteraction, - type APIEmbed, - ApplicationCommandType, - channelMention, -} from "discord.js"; +import { type ChatInputCommandInteraction, type APIEmbed, ApplicationCommandType, channelMention } from "discord.js"; import type { Command } from "djs-fsrouter"; export const type = ApplicationCommandType.ChatInput; @@ -35,15 +30,11 @@ JavaScripters is a well known JavaScript focused server with over 10k members`, }, { name: "Rules channel", - value: interaction.guild?.rulesChannelId - ? channelMention(interaction.guild?.rulesChannelId) - : "None", + value: interaction.guild?.rulesChannelId ? channelMention(interaction.guild?.rulesChannelId) : "None", }, { name: "Created", - value: ``, + value: ``, }, ], }; diff --git a/src/commands/logging/$info.js b/src/commands/logging/$info.js index 8a43f03..27bcff0 100644 --- a/src/commands/logging/$info.js +++ b/src/commands/logging/$info.js @@ -2,4 +2,4 @@ export default { description: "Handles logging deleted & edited messages", dmPermission: false, defaultMemberPermissions: "0", -}; \ No newline at end of file +}; diff --git a/src/commands/logging/clear.ts b/src/commands/logging/clear.ts index 7cf31fc..59882aa 100644 --- a/src/commands/logging/clear.ts +++ b/src/commands/logging/clear.ts @@ -1,12 +1,6 @@ import { getConfig } from "../../logging.ts"; -import { - ApplicationCommandOptionType, - ApplicationCommandType, - GuildMessageManager, - Message, - User, -} from "discord.js"; +import { ApplicationCommandOptionType, ApplicationCommandType, GuildMessageManager, Message, User } from "discord.js"; import type { Command } from "djs-fsrouter"; import { deleteColor, editColor } from "../../listeners/logging.ts"; @@ -29,15 +23,10 @@ const Config: Command = { await interaction.deferReply().catch(console.error); const { channel } = getConfig(guild) || {}; - if (!channel) - return interaction - .editReply("Error: No logging channel has been set.") - .catch(console.error); + if (!channel) return interaction.editReply("Error: No logging channel has been set.").catch(console.error); const logs = await guild.channels.fetch(channel); if (!logs || !logs.isTextBased()) - return interaction - .editReply("Error: Could not retrieve the logging channel.") - .catch(console.error); + return interaction.editReply("Error: Could not retrieve the logging channel.").catch(console.error); const target = interaction.options.getUser("user", true); const targetMention = target.toString(); @@ -52,8 +41,7 @@ const Config: Command = { if (member !== me || !embeds.length) return false; const [{ description: embed, color }] = embeds; - if (!embed || (color !== deleteColor && color !== editColor)) - return false; + if (!embed || (color !== deleteColor && color !== editColor)) return false; if (embeds.length > 1) { bulkPurges++; promises.push(purgeBulk(target, message)); @@ -66,15 +54,10 @@ const Config: Command = { if (targetLogs.size) toDelete.push(...targetLogs.values()); } - for (let i = 0; i < toDelete.length; i += 100) - promises.push(logs.bulkDelete(toDelete.slice(i, i + 100))); + for (let i = 0; i < toDelete.length; i += 100) promises.push(logs.bulkDelete(toDelete.slice(i, i + 100))); await Promise.allSettled(promises); - interaction - .editReply( - `Erased ${toDelete.length} logs and purged ${bulkPurges} bulk logs.`, - ) - .catch(console.error); + interaction.editReply(`Erased ${toDelete.length} logs and purged ${bulkPurges} bulk logs.`).catch(console.error); }, }; export default Config; @@ -84,9 +67,7 @@ async function* fetchTill14days(messageManager: GuildMessageManager) { let chunk = await messageManager.fetch({ limit: 100, cache: false }); let last = chunk.last(); while (last) { - yield chunk.filter( - ({ createdTimestamp }) => now - createdTimestamp < _14_DAYS, - ); + yield chunk.filter(({ createdTimestamp }) => now - createdTimestamp < _14_DAYS); if (now - last.createdTimestamp > _14_DAYS) return; @@ -107,6 +88,5 @@ async function* fetchTill14days(messageManager: GuildMessageManager) { */ function purgeBulk({ tag }: User, message: Message) { const embeds = message.embeds.filter(({ author }) => author?.name !== tag); - if (embeds.length !== message.embeds.length) - return embeds.length ? message.edit({ embeds }) : message.delete(); + if (embeds.length !== message.embeds.length) return embeds.length ? message.edit({ embeds }) : message.delete(); } diff --git a/src/commands/mdn.ts b/src/commands/mdn.ts index 7349696..3a0f6bc 100644 --- a/src/commands/mdn.ts +++ b/src/commands/mdn.ts @@ -32,10 +32,7 @@ const Mdn: Command = { const searchResult = await search(searchTerm).catch(console.error); if (!searchResult?.ok) { interaction.respond([]).catch(console.error); - if (searchResult) - console.error( - `Got ${searchResult.status} ${searchResult.statusText} code trying to use CSE`, - ); + if (searchResult) console.error(`Got ${searchResult.status} ${searchResult.statusText} code trying to use CSE`); } else { const { items } = (await searchResult.json()) as { items: SearchResult[]; @@ -58,12 +55,8 @@ const Mdn: Command = { } const crawler = await scrape(url); - const intro = crawler( - ".main-page-content > .section-content:first-of-type > *", - ); - const links = crawler( - ".main-page-content > .section-content:first-of-type a", - ); + const intro = crawler(".main-page-content > .section-content:first-of-type > *"); + const links = crawler(".main-page-content > .section-content:first-of-type a"); Array.prototype.forEach.call(links, makeLinkAbsolute); let title: string = crawler("head title").text(); if (title.endsWith(" | MDN")) title = title.slice(0, -6); @@ -93,9 +86,7 @@ const BASE_URL = `https://www.googleapis.com/customsearch/v1/siterestrict?key=${ function search(term: string, num = 10) { if (!Number.isInteger(num) || num < 1 || num > 10) - throw new RangeError( - `The number of results must be an integer between 1 and 10, inclusive (got ${num}).`, - ); + throw new RangeError(`The number of results must be an integer between 1 and 10, inclusive (got ${num}).`); return fetch(`${BASE_URL}&q=${encodeURIComponent(term)}&num=${num}`); } diff --git a/src/commands/purge.ts b/src/commands/purge.ts index eeae24c..d239757 100644 --- a/src/commands/purge.ts +++ b/src/commands/purge.ts @@ -42,9 +42,7 @@ const Purge: Command = { } = channel; const myself = members.me || (await members.fetchMe()); if (!channel.permissionsFor(myself).has(ManageMessages)) { - return reply( - "Error: I do not have the permission to delete messages in this channel.", - ); + return reply("Error: I do not have the permission to delete messages in this channel."); } let messages = Array.from( diff --git a/src/components.ts b/src/components.ts index ec0cd6d..411fbe3 100644 --- a/src/components.ts +++ b/src/components.ts @@ -15,13 +15,8 @@ import type { APIButtonComponentWithCustomId, } from "discord.js"; -interface ButtonComponent - extends Omit, "style"> { - style?: - | ButtonStyle.Primary - | ButtonStyle.Secondary - | ButtonStyle.Success - | ButtonStyle.Danger; +interface ButtonComponent extends Omit, "style"> { + style?: ButtonStyle.Primary | ButtonStyle.Secondary | ButtonStyle.Success | ButtonStyle.Danger; } export function stringSelectMenu( @@ -32,15 +27,11 @@ export function stringSelectMenu( ): APIActionRowComponent { return { type: ActionRow, - components: [ - { type: StringSelect, custom_id, options, min_values, max_values }, - ], + components: [{ type: StringSelect, custom_id, options, min_values, max_values }], }; } -export function buttonRow( - ...buttons: ButtonComponent[] -): APIActionRowComponent { +export function buttonRow(...buttons: ButtonComponent[]): APIActionRowComponent { return { type: ActionRow, components: buttons.map((button) => ({ @@ -51,9 +42,7 @@ export function buttonRow( }; } -export function modalInput( - input: Omit, -): ActionRowData { +export function modalInput(input: Omit): ActionRowData { return { type: ActionRow, components: [ diff --git a/src/index.ts b/src/index.ts index 3d38533..9a1e4cf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,10 +20,7 @@ const parsedEnv = safeParse( if (!parsedEnv.success) { console.error( `Issues loading environment variables: \n-----------\n${parsedEnv.issues - .map( - (issue) => - `Variable: ${issue?.path?.[0].key}\nInput: ${issue.input}\nError: ${issue.message}\n-----------`, - ) + .map((issue) => `Variable: ${issue?.path?.[0].key}\nInput: ${issue.input}\nError: ${issue.message}\n-----------`) .join("\n")}`, ); exit(1); diff --git a/src/listeners/delete-and-warn.ts b/src/listeners/delete-and-warn.ts index 323d653..fb68173 100644 --- a/src/listeners/delete-and-warn.ts +++ b/src/listeners/delete-and-warn.ts @@ -1,32 +1,23 @@ import type { Listener } from "../types/listener.ts"; import { customId } from "../commands/delete-and-warn.ts"; -export default [ +export default ([ { event: "interactionCreate", async handler(interaction) { - if ( - !interaction.isModalSubmit() || - !interaction.customId.endsWith(customId) || - !interaction.guild - ) - return; + if (!interaction.isModalSubmit() || !interaction.customId.endsWith(customId) || !interaction.guild) return; const [targetId, messageId] = interaction.customId.split("_", 2); const target = await interaction.guild.members.fetch(targetId); const reason = interaction.fields.getField("deletionReason").value; - const targetMessage = await interaction.channel?.messages.fetch( - messageId, - ); + const targetMessage = await interaction.channel?.messages.fetch(messageId); targetMessage?.delete().catch(console.error); if (target.moderatable) { const timeout = parseTime(interaction.fields.getField("timeout").value); if (timeout > 0) target.timeout(timeout, reason).catch(console.error); } target - .send( - `Your message in ${interaction.channel} was deleted for the following reason:\n\`\`\`${reason}\`\`\``, - ) + .send(`Your message in ${interaction.channel} was deleted for the following reason:\n\`\`\`${reason}\`\`\``) .then(() => { interaction .reply({ @@ -45,7 +36,7 @@ export default [ }); }, }, -] as Listener[]; +] as Listener[]); const units: Record = { s: 1_000, diff --git a/src/listeners/join-leave-message.ts b/src/listeners/join-leave-message.ts index 55a4219..e00bf05 100644 --- a/src/listeners/join-leave-message.ts +++ b/src/listeners/join-leave-message.ts @@ -1,22 +1,11 @@ -import { - Colors, - EmbedBuilder, - GuildMember, - type PartialGuildMember, - userMention, -} from "discord.js"; +import { Colors, EmbedBuilder, GuildMember, type PartialGuildMember, userMention } from "discord.js"; import type { Listener } from "../types/listener.ts"; import { getConfig } from "../utils.ts"; import type { ConfigSelect } from "../schemas/config.ts"; -const getTargetChannel = async ( - dbConfig: ConfigSelect, - member: GuildMember | PartialGuildMember, -) => { +const getTargetChannel = async (dbConfig: ConfigSelect, member: GuildMember | PartialGuildMember) => { if (!dbConfig?.gatewayChannel) return undefined; - const targetChannel = await member.guild.channels.fetch( - dbConfig.gatewayChannel, - ); + const targetChannel = await member.guild.channels.fetch(dbConfig.gatewayChannel); if (!targetChannel?.isTextBased()) { console.error("Gateway channel is not a text channel!"); @@ -27,12 +16,8 @@ const getTargetChannel = async ( }; const getEmbed = async (dbConfig: ConfigSelect, isLeaveEmbed?: boolean) => { - const title = isLeaveEmbed - ? dbConfig?.gatewayLeaveTitle - : dbConfig?.gatewayJoinTitle; - const description = isLeaveEmbed - ? dbConfig?.gatewayLeaveContent - : dbConfig?.gatewayJoinContent; + const title = isLeaveEmbed ? dbConfig?.gatewayLeaveTitle : dbConfig?.gatewayJoinTitle; + const description = isLeaveEmbed ? dbConfig?.gatewayLeaveContent : dbConfig?.gatewayJoinContent; return new EmbedBuilder({ color: isLeaveEmbed ? Colors.Red : Colors.Green, diff --git a/src/listeners/logging.ts b/src/listeners/logging.ts index 1608d53..9d268e6 100644 --- a/src/listeners/logging.ts +++ b/src/listeners/logging.ts @@ -6,7 +6,7 @@ import type { Listener } from "../types/listener.ts"; export const deleteColor = 0xdd4444; export const editColor = 0xdd6d0c; -export default [ +export default ([ { event: "messageDelete", async handler(message) { @@ -17,10 +17,7 @@ export default [ const embed = { ...msgDeletionEmbed(message), description: - `**🗑️ Message from ${message.author} deleted in ${message.channel}**\n\n${message.content}`.substring( - 0, - 2056, - ), + `**🗑️ Message from ${message.author} deleted in ${message.channel}**\n\n${message.content}`.substring(0, 2056), timestamp: new Date().toISOString(), }; channel.send({ embeds: [embed] }).catch(console.error); @@ -30,17 +27,11 @@ export default [ event: "messageDeleteBulk", async handler(messages) { const first = messages.first(); - const channel = await getLogChannel( - first?.guild || null, - LogMode.DELETES, - ); + const channel = await getLogChannel(first?.guild || null, LogMode.DELETES); if (!channel?.isTextBased()) return; const embeds = messages - .filter( - (message): message is Message => - !message.partial && !shouldIgnore(message), - ) + .filter((message): message is Message => !message.partial && !shouldIgnore(message)) .map(msgDeletionEmbed); if (!embeds.length) return; @@ -95,7 +86,7 @@ export default [ event: "roleDelete", handler: unwhitelistRole, }, -] as Listener[]; +] as Listener[]); /** * Tells if the logging system should ignore the given message. diff --git a/src/listeners/new-suggestion.ts b/src/listeners/new-suggestion.ts index 2e8c277..105899a 100644 --- a/src/listeners/new-suggestion.ts +++ b/src/listeners/new-suggestion.ts @@ -5,13 +5,7 @@ import { getConfig } from "../utils.ts"; export default ({ event: "messageCreate", async handler(message) { - if ( - !message.inGuild() || - !message.deletable || - !message.member || - message.author.bot - ) - return; + if (!message.inGuild() || !message.deletable || !message.member || message.author.bot) return; const dbConfig = getConfig.get({ guildId: message.guildId }); diff --git a/src/listeners/suggestion-interaction.ts b/src/listeners/suggestion-interaction.ts index 0dce148..32242a0 100644 --- a/src/listeners/suggestion-interaction.ts +++ b/src/listeners/suggestion-interaction.ts @@ -1,22 +1,7 @@ -import { - ActionRowBuilder, - TextInputBuilder, - ModalBuilder, - TextInputStyle, - inlineCode, -} from "discord.js"; +import { ActionRowBuilder, TextInputBuilder, ModalBuilder, TextInputStyle, inlineCode } from "discord.js"; import type { Listener } from "../types/listener.ts"; -import { - Time, - capitalizeFirstLetter, - getConfig, - getKeyByValue, - hyperlink, -} from "../utils.ts"; -import type { - SuggestionStatus, - UpdatedSuggestionStatus, -} from "../schemas/suggestion.ts"; +import { Time, capitalizeFirstLetter, getConfig, getKeyByValue, hyperlink } from "../utils.ts"; +import type { SuggestionStatus, UpdatedSuggestionStatus } from "../schemas/suggestion.ts"; import { BUTTON_ID, BUTTON_ID_STATUS_MAP, @@ -48,41 +33,27 @@ export default ([ const config = getConfig.get({ guildId: interaction.guildId }); - if ( - config?.suggestionManagerRole && - !interaction.member.roles.cache.has(config.suggestionManagerRole) - ) { + if (config?.suggestionManagerRole && !interaction.member.roles.cache.has(config.suggestionManagerRole)) { return interaction.reply({ - content: `You're missing the manager role and ${inlineCode( - "ManageGuild", - )} permission`, + content: `You're missing the manager role and ${inlineCode("ManageGuild")} permission`, ephemeral: true, }); } - if ( - !config?.suggestionManagerRole && - !interaction.member.permissions.has("ManageGuild") - ) { + if (!config?.suggestionManagerRole && !interaction.member.permissions.has("ManageGuild")) { return interaction.reply({ content: `You're missing the ${inlineCode("ManageGuild")} permission`, ephemeral: true, }); } - const suggestion = await Suggestion.createFromMessage( - interaction.message, - ); + const suggestion = await Suggestion.createFromMessage(interaction.message); - const status = getKeyByValue( - BUTTON_ID, - interaction.customId as UpdatedSuggestionStatus, - ) as SuggestionStatus | undefined; + const status = getKeyByValue(BUTTON_ID, interaction.customId as UpdatedSuggestionStatus) as + | SuggestionStatus + | undefined; - if (!status) - throw new Error( - `Could not map button ID "${interaction.customId}" to a valid suggestion status`, - ); + if (!status) throw new Error(`Could not map button ID "${interaction.customId}" to a valid suggestion status`); const textInput = new TextInputBuilder() .setCustomId(MODAL_INPUT_ID) @@ -91,14 +62,10 @@ export default ([ .setPlaceholder("Leave empty if no reason necessary...") .setMaxLength(Suggestion.MAX_REASON_LENGTH) .setRequired(false); - const actionRow = new ActionRowBuilder().addComponents( - textInput, - ); + const actionRow = new ActionRowBuilder().addComponents(textInput); const modal = new ModalBuilder() .setCustomId(MODAL_ID) - .setTitle( - `${capitalizeFirstLetter(getStatusAsVerb(status))} suggestion`, - ) + .setTitle(`${capitalizeFirstLetter(getStatusAsVerb(status))} suggestion`) .addComponents(actionRow); await interaction.showModal(modal); @@ -106,24 +73,16 @@ export default ([ const modalInteraction = await interaction.awaitModalSubmit({ time: Time.Minute * 10, }); - const inputReason = - modalInteraction.fields.getTextInputValue(MODAL_INPUT_ID); + const inputReason = modalInteraction.fields.getTextInputValue(MODAL_INPUT_ID); - await suggestion.setStatus( - modalInteraction.user, - status, - inputReason || undefined, - config, - ); + await suggestion.setStatus(modalInteraction.user, status, inputReason || undefined, config); - const statusString = - BUTTON_ID_STATUS_MAP[interaction.customId as SuggestionButtonId]; + const statusString = BUTTON_ID_STATUS_MAP[interaction.customId as SuggestionButtonId]; await modalInteraction.reply({ - content: `You set the status of ${hyperlink( - "this suggestion", - interaction.message.url, - )} to ${inlineCode(statusString.toLowerCase())}`, + content: `You set the status of ${hyperlink("this suggestion", interaction.message.url)} to ${inlineCode( + statusString.toLowerCase(), + )}`, ephemeral: true, }); }, @@ -143,26 +102,18 @@ export default ([ return; const config = getConfig.get({ guildId: interaction.guildId }); - const suggestion = await Suggestion.createFromMessage( - interaction.message, - ); + const suggestion = await Suggestion.createFromMessage(interaction.message); if (interaction.customId === VOTE_BUTTON_ID.UPVOTE) { await suggestion.upvote(interaction.user.id, config); await interaction.reply({ - content: `Successfully upvoted ${hyperlink( - "this", - interaction.message.url, - )} suggestion`, + content: `Successfully upvoted ${hyperlink("this", interaction.message.url)} suggestion`, ephemeral: true, }); } else { await suggestion.downvote(interaction.user.id, config); await interaction.reply({ - content: `Successfully downvoted ${hyperlink( - "this", - interaction.message.url, - )} suggestion`, + content: `Successfully downvoted ${hyperlink("this", interaction.message.url)} suggestion`, ephemeral: true, }); } diff --git a/src/logging.ts b/src/logging.ts index 5980178..f1b0671 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -6,10 +6,7 @@ import type { TextChannel, Guild, Role } from "discord.js"; import { eq, and, sql } from "drizzle-orm"; const { placeholder } = sql; -export function setLogging( - { id }: Guild, - config: LogMode.NONE | { mode: LogMode; channel: TextChannel }, -) { +export function setLogging({ id }: Guild, config: LogMode.NONE | { mode: LogMode; channel: TextChannel }) { let loggingMode = LogMode.NONE; let loggingChannel = null; if (config) { @@ -66,11 +63,6 @@ const whitelistRole_add = db const whitelistRole_remove = db .delete(LoggingWhitelist) - .where( - and( - eq(LoggingWhitelist.guildId, placeholder("guildId")), - eq(LoggingWhitelist.roleId, placeholder("id")), - ), - ) + .where(and(eq(LoggingWhitelist.guildId, placeholder("guildId")), eq(LoggingWhitelist.roleId, placeholder("id")))) .returning() .prepare(); diff --git a/src/schemas/config.ts b/src/schemas/config.ts index 5e1f1cc..1e3e3f8 100644 --- a/src/schemas/config.ts +++ b/src/schemas/config.ts @@ -12,10 +12,7 @@ export const Config = sqliteTable("guildConfig", { gatewayLeaveTitle: text("gatewayLeaveTitle"), gatewayLeaveContent: text("gatewayLeaveContent"), - loggingMode: integer("loggingMode", { mode: "number" }) - .$type() - .notNull() - .default(LogMode.NONE), + loggingMode: integer("loggingMode", { mode: "number" }).$type().notNull().default(LogMode.NONE), loggingChannel: text("loggingChannel").default(""), suggestionChannel: text("suggestionChannel"), diff --git a/src/schemas/suggestion.ts b/src/schemas/suggestion.ts index 1c5a512..fd1a605 100644 --- a/src/schemas/suggestion.ts +++ b/src/schemas/suggestion.ts @@ -9,15 +9,10 @@ export const SUGGESTION_STATUS = { REJECTED: "REJECTED", } as const satisfies Record; -export type SuggestionStatus = - (typeof SUGGESTION_STATUS)[keyof typeof SUGGESTION_STATUS]; +export type SuggestionStatus = (typeof SUGGESTION_STATUS)[keyof typeof SUGGESTION_STATUS]; export type UpdatedSuggestionStatus = Exclude; -const SUGGESTION_STATUS_VALUES = Object.values(SUGGESTION_STATUS) as [ - "POSTED", - "ACCEPTED", - "REJECTED", -]; +const SUGGESTION_STATUS_VALUES = Object.values(SUGGESTION_STATUS) as ["POSTED", "ACCEPTED", "REJECTED"]; export const Suggestion = sqliteTable("suggestion", { id: int("id").primaryKey({ autoIncrement: true }).notNull(), @@ -31,21 +26,15 @@ export const Suggestion = sqliteTable("suggestion", { messageId: text("messageId").notNull(), userId: text("userId").notNull(), - status: text("status", { enum: SUGGESTION_STATUS_VALUES }) - .default("POSTED") - .notNull(), + status: text("status", { enum: SUGGESTION_STATUS_VALUES }).default("POSTED").notNull(), statusReason: text("statusReason"), statusUserId: text("statusUserId"), upvotedBy: stringSet("upvotedBy"), downvotedBy: stringSet("downvotedBy"), - updatedAt: int("updatedAt", { mode: "timestamp" }) - .default(sql`(strftime('%s', 'now'))`) - .notNull(), - createdAt: int("createdAt", { mode: "timestamp" }) - .default(sql`(strftime('%s', 'now'))`) - .notNull(), + updatedAt: int("updatedAt", { mode: "timestamp" }).default(sql`(strftime('%s', 'now'))`).notNull(), + createdAt: int("createdAt", { mode: "timestamp" }).default(sql`(strftime('%s', 'now'))`).notNull(), }); export type SuggestionSelect = InferSelectModel; diff --git a/src/structures/configuration/configuration-manifest.ts b/src/structures/configuration/configuration-manifest.ts index 4a15b0f..ea3ba0a 100644 --- a/src/structures/configuration/configuration-manifest.ts +++ b/src/structures/configuration/configuration-manifest.ts @@ -11,12 +11,8 @@ interface ConfigurationOptionTypeMap { export type ConfigurationOptionType = keyof ConfigurationOptionTypeMap; -type ConfigurationOptionValidateFn = ( - value: T, -) => boolean | string; -type ConfigurationOptionToDatabaseFn = ( - value: U, -) => T; +type ConfigurationOptionValidateFn = (value: T) => boolean | string; +type ConfigurationOptionToDatabaseFn = (value: U) => T; type ConfigurationOptionFromDatabaseFn = (value: T) => unknown; interface PartialConfigurationOption< @@ -31,14 +27,9 @@ interface PartialConfigurationOption< /** Validate the input, validation succeeds when `true` is returned. */ validate?: ConfigurationOptionValidateFn; /** Transform the value when persisting to the database. */ - toDatabase?: ConfigurationOptionToDatabaseFn< - InferSelectModel
[Column], - ConfigurationOptionTypeMap[Type] - >; + toDatabase?: ConfigurationOptionToDatabaseFn[Column], ConfigurationOptionTypeMap[Type]>; /** Transform the value when retrieving from the database. */ - fromDatabase?: ConfigurationOptionFromDatabaseFn< - InferSelectModel
[Column] - >; + fromDatabase?: ConfigurationOptionFromDatabaseFn[Column]>; /** The table to execute queries on. */ table: Table; /** The column to execute queries on. */ @@ -59,10 +50,8 @@ interface ButtonConfigurationOption< emoji?: string; } -export interface ConfigurationTextOption< - Table extends DrizzleTable, - Column extends keyof InferSelectModel
, -> extends ButtonConfigurationOption<"text", Table, Column> { +export interface ConfigurationTextOption
> + extends ButtonConfigurationOption<"text", Table, Column> { type: "text"; placeholder?: string; /** @@ -73,33 +62,25 @@ export interface ConfigurationTextOption< style?: TextInputStyle; } -export interface ConfigurationBooleanOption< - Table extends DrizzleTable, - Column extends keyof InferSelectModel
, -> extends ButtonConfigurationOption<"boolean", Table, Column> { +export interface ConfigurationBooleanOption
> + extends ButtonConfigurationOption<"boolean", Table, Column> { type: "boolean"; } -export interface ConfigurationRoleOption< - Table extends DrizzleTable, - Column extends keyof InferSelectModel
, -> extends PartialConfigurationOption<"role", Table, Column> { +export interface ConfigurationRoleOption
> + extends PartialConfigurationOption<"role", Table, Column> { type: "role"; placeholder?: string; } -export interface ConfigurationChannelOption< - Table extends DrizzleTable, - Column extends keyof InferSelectModel
, -> extends PartialConfigurationOption<"channel", Table, Column> { +export interface ConfigurationChannelOption
> + extends PartialConfigurationOption<"channel", Table, Column> { type: "channel"; placeholder?: string; } -export interface ConfigurationSelectOption< - Table extends DrizzleTable, - Column extends keyof InferSelectModel
, -> extends PartialConfigurationOption<"select", Table, Column> { +export interface ConfigurationSelectOption
> + extends PartialConfigurationOption<"select", Table, Column> { type: "select"; placeholder?: string; options: { diff --git a/src/structures/configuration/configuration-message.ts b/src/structures/configuration/configuration-message.ts index 3d12e29..175c0d0 100644 --- a/src/structures/configuration/configuration-message.ts +++ b/src/structures/configuration/configuration-message.ts @@ -29,18 +29,9 @@ import { type MessageActionRowComponentBuilder, type ModalActionRowComponentBuilder, } from "discord.js"; -import type { - ConfigurationOption, - ConfigurationOptionType, -} from "./configuration-manifest.ts"; +import type { ConfigurationOption, ConfigurationOptionType } from "./configuration-manifest.ts"; import db from "../../db.ts"; -import { - and, - Table as DrizzleTable, - eq, - SQL, - type InferSelectModel, -} from "drizzle-orm"; +import { and, Table as DrizzleTable, eq, SQL, type InferSelectModel } from "drizzle-orm"; import { Time } from "../../utils.ts"; import { truncate } from "../../utils/common.ts"; import { handleInteractionCollect } from "./interaction-handlers.ts"; @@ -90,9 +81,7 @@ export class ConfigurationMessage
{ } /** Reply with the configuration message and listen to component interactions. */ - public async initialize( - interaction: ChatInputCommandInteraction, - ): Promise { + public async initialize(interaction: ChatInputCommandInteraction): Promise { if (!interaction.inGuild() || this.#initialized) return; const messageOptions = await this.getMessageOptions(interaction); @@ -111,20 +100,13 @@ export class ConfigurationMessage
{ /** Add the component interaction listeners. */ private initializeListeners() { - const manifestOptionMap = Object.fromEntries( - this.#manifest.map((option) => [getCustomId(option), option]), - ); + const manifestOptionMap = Object.fromEntries(this.#manifest.map((option) => [getCustomId(option), option])); - if (!this.#reply) - throw new Error( - "No internal reply message or interaction response available", - ); + if (!this.#reply) throw new Error("No internal reply message or interaction response available"); const collector = this.#reply.createMessageComponentCollector({ filter: async (interaction) => { - const isValidCustomId = Object.keys(manifestOptionMap).includes( - interaction.customId, - ); + const isValidCustomId = Object.keys(manifestOptionMap).includes(interaction.customId); const message = await this.getReplyMessage(); const isReplyAuthor = message.author.id === interaction.user.id; @@ -134,22 +116,15 @@ export class ConfigurationMessage
{ }); collector.on("collect", async (interaction) => { - if (!interaction.inGuild()) - throw new Error("Interaction happened outside a guild"); + if (!interaction.inGuild()) throw new Error("Interaction happened outside a guild"); - this.handleInteractionCollect( - interaction, - manifestOptionMap[interaction.customId], - ); + this.handleInteractionCollect(interaction, manifestOptionMap[interaction.customId]); }); } /** Get the {@link Message} for the configuration message reply. */ private async getReplyMessage() { - if (!this.#reply) - throw new Error( - "No internal reply message or interaction response available", - ); + if (!this.#reply) throw new Error("No internal reply message or interaction response available"); return this.#reply instanceof Message ? this.#reply : this.#reply.fetch(); } @@ -173,11 +148,7 @@ export class ConfigurationMessage
{ // TEMP: use .all() and select the first row manually, .get() does not work .at(0) as { value: unknown }; - const handleCollectResult = await handleInteractionCollect( - interaction, - manifestOption, - value, - ); + const handleCollectResult = await handleInteractionCollect(interaction, manifestOption, value); if (handleCollectResult === null) return; @@ -185,9 +156,7 @@ export class ConfigurationMessage
{ db.update(this.#table) .set({ - [manifestOption.column as keyof object]: updatedValue - ? updatedValue - : null, + [manifestOption.column as keyof object]: updatedValue ? updatedValue : null, }) .where(whereClause) .run(); @@ -222,28 +191,20 @@ export class ConfigurationMessage
{ /** Update the reply message. */ private async updateReply(messageOptions: BaseMessageOptions): Promise { - if (!this.#reply) - throw new Error( - "No internal reply message or interaction response available", - ); + if (!this.#reply) throw new Error("No internal reply message or interaction response available"); this.#reply = await this.#reply.edit(messageOptions); } /** Get the action rows with components. */ private getActionRows(): APIActionRowComponent[] { - const actionRows: ActionRowBuilder[] = [ - new ActionRowBuilder(), - ]; + const actionRows: ActionRowBuilder[] = [new ActionRowBuilder()]; for (const manifestOption of this.#manifest) { const component = this.getActionRowComponent(manifestOption); if (["select", "channel", "role"].includes(manifestOption.type)) { - const row = - new ActionRowBuilder().addComponents( - component, - ); + const row = new ActionRowBuilder().addComponents(component); actionRows.push(row); } else { @@ -256,9 +217,7 @@ export class ConfigurationMessage
{ } /** Get the component for a manifest option. */ - private getActionRowComponent( - manifestOption: ConfigurationOption, - ): MessageActionRowComponentBuilder { + private getActionRowComponent(manifestOption: ConfigurationOption): MessageActionRowComponentBuilder { const customId = getCustomId(manifestOption); switch (manifestOption.type) { @@ -282,24 +241,18 @@ export class ConfigurationMessage
{ return component; } case "channel": { - const component = new ChannelSelectMenuBuilder() - .setCustomId(customId) - .setMaxValues(1); + const component = new ChannelSelectMenuBuilder().setCustomId(customId).setMaxValues(1); // TODO: set channel type - if (manifestOption.placeholder) - component.setPlaceholder(manifestOption.placeholder); + if (manifestOption.placeholder) component.setPlaceholder(manifestOption.placeholder); return component; } case "role": { - const component = new RoleSelectMenuBuilder() - .setCustomId(customId) - .setMaxValues(1); + const component = new RoleSelectMenuBuilder().setCustomId(customId).setMaxValues(1); - if (manifestOption.placeholder) - component.setPlaceholder(manifestOption.placeholder); + if (manifestOption.placeholder) component.setPlaceholder(manifestOption.placeholder); return component; } @@ -309,8 +262,7 @@ export class ConfigurationMessage
{ .addOptions(manifestOption.options) .setMaxValues(1); - if (manifestOption.placeholder) - component.setPlaceholder(manifestOption.placeholder); + if (manifestOption.placeholder) component.setPlaceholder(manifestOption.placeholder); return component; } @@ -319,9 +271,7 @@ export class ConfigurationMessage
{ /** Get the message options for the configuration message. */ private async getMessageOptions( - interaction: - | ChatInputCommandInteraction<"cached" | "raw"> - | CollectedInteraction<"cached" | "raw">, + interaction: ChatInputCommandInteraction<"cached" | "raw"> | CollectedInteraction<"cached" | "raw">, ): Promise { let content = ""; @@ -338,32 +288,20 @@ export class ConfigurationMessage
{ // TEMP: use .all() and select the first row manually, .get() does not work .at(0); - if (!databaseValues) - throw new Error("Could not retrieve the configuration"); + if (!databaseValues) throw new Error("Could not retrieve the configuration"); for (const manifestOption of this.#manifest) { - const databaseValue = this.getOptionValue( - manifestOption, - databaseValues[manifestOption.column], - ); + const databaseValue = this.getOptionValue(manifestOption, databaseValues[manifestOption.column]); const nameFormatted = bold(manifestOption.name); const descriptionFormatted = italic(manifestOption.description); - if ( - manifestOption.type === "text" && - manifestOption.style === TextInputStyle.Paragraph - ) { - const valueFormatted = databaseValue - ? codeBlock(truncate(databaseValue as string, 40)) - : this.#notSetText; + if (manifestOption.type === "text" && manifestOption.style === TextInputStyle.Paragraph) { + const valueFormatted = databaseValue ? codeBlock(truncate(databaseValue as string, 40)) : this.#notSetText; content += `${nameFormatted}\n-# ${descriptionFormatted}\n${valueFormatted}\n`; } else { - const valueFormatted = this.formatValue( - manifestOption.type, - databaseValue, - ); + const valueFormatted = this.formatValue(manifestOption.type, databaseValue); content += `${nameFormatted} — ${valueFormatted}\n-# ${descriptionFormatted}\n\n`; } diff --git a/src/structures/configuration/interaction-handlers.ts b/src/structures/configuration/interaction-handlers.ts index 4a17c34..943f4bf 100644 --- a/src/structures/configuration/interaction-handlers.ts +++ b/src/structures/configuration/interaction-handlers.ts @@ -34,14 +34,8 @@ const handleTextInteractionCollect = async ( .setPlaceholder(manifestOption.placeholder ?? "") .setStyle(manifestOption.style ?? TextInputStyle.Short); - const actionRow = - new ActionRowBuilder().addComponents( - modalInput, - ); - const modal = new ModalBuilder() - .setTitle(manifestOption.name) - .setCustomId(modalCustomId) - .addComponents(actionRow); + const actionRow = new ActionRowBuilder().addComponents(modalInput); + const modal = new ModalBuilder().setTitle(manifestOption.name).setCustomId(modalCustomId).addComponents(actionRow); await interaction.showModal(modal); @@ -54,8 +48,7 @@ const handleTextInteractionCollect = async ( if ( error instanceof DiscordjsError && error.code === DiscordjsErrorCodes.InteractionCollectorError && - error.message === - "Collector received no interactions before ending with reason: time" + error.message === "Collector received no interactions before ending with reason: time" ) return null; @@ -84,8 +77,7 @@ export const handleInteractionCollect = async ( // Return early because the component timed out if (modalSubmitInteraction === null) return null; - updatedValue = - modalSubmitInteraction?.fields.getTextInputValue(modalInputCustomId); + updatedValue = modalSubmitInteraction?.fields.getTextInputValue(modalInputCustomId); followUpInteraction = modalSubmitInteraction; } diff --git a/src/structures/configuration/utils.ts b/src/structures/configuration/utils.ts index 9e5febe..947f472 100644 --- a/src/structures/configuration/utils.ts +++ b/src/structures/configuration/utils.ts @@ -6,10 +6,7 @@ import type { ConfigurationOption } from "./configuration-manifest.ts"; * * @private */ -export const getCustomId = ( - manifestOption: ConfigurationOption
, - suffix?: string, -) => { +export const getCustomId = (manifestOption: ConfigurationOption
, suffix?: string) => { const _suffix = suffix ? `-${suffix}` : ""; return `config-message-${manifestOption.column}${_suffix}`; diff --git a/src/structures/suggestion-util.ts b/src/structures/suggestion-util.ts index b7c5b2b..d8070a6 100644 --- a/src/structures/suggestion-util.ts +++ b/src/structures/suggestion-util.ts @@ -1,8 +1,5 @@ import { ButtonStyle, type Interaction, ButtonInteraction } from "discord.js"; -import { - type SuggestionStatus, - type UpdatedSuggestionStatus, -} from "../schemas/suggestion.ts"; +import { type SuggestionStatus, type UpdatedSuggestionStatus } from "../schemas/suggestion.ts"; export type SuggestionButtonId = (typeof BUTTON_ID)[keyof typeof BUTTON_ID]; @@ -42,27 +39,18 @@ export const getStatusAsVerb = (status: SuggestionStatus) => { }; /** Check if the interaction is a valid suggestion button interaction. */ -export const isValidStatusButtonInteraction = ( - interaction: Interaction, -): interaction is ButtonInteraction => { +export const isValidStatusButtonInteraction = (interaction: Interaction): interaction is ButtonInteraction => { const validButtonIds = Object.values(BUTTON_ID); - return ( - interaction.isButton() && - validButtonIds.includes(interaction.customId as SuggestionButtonId) - ); + return interaction.isButton() && validButtonIds.includes(interaction.customId as SuggestionButtonId); }; /** Check if the interaction is a valid suggestion vote button interaction. */ -export const isValidVoteButtonInteraction = ( - interaction: Interaction, -): interaction is ButtonInteraction => { +export const isValidVoteButtonInteraction = (interaction: Interaction): interaction is ButtonInteraction => { const validButtonIds = Object.values(VOTE_BUTTON_ID); return ( interaction.isButton() && - validButtonIds.includes( - interaction.customId as (typeof VOTE_BUTTON_ID)[keyof typeof VOTE_BUTTON_ID], - ) + validButtonIds.includes(interaction.customId as (typeof VOTE_BUTTON_ID)[keyof typeof VOTE_BUTTON_ID]) ); }; diff --git a/src/structures/suggestion.ts b/src/structures/suggestion.ts index 1fd6f03..a5228b7 100644 --- a/src/structures/suggestion.ts +++ b/src/structures/suggestion.ts @@ -24,12 +24,7 @@ import { and, eq, sql } from "drizzle-orm"; import type { ConfigSelect } from "../schemas/config.ts"; import { client } from "../client.ts"; import { capitalizeFirstLetter, getConfig } from "../utils.ts"; -import { - BUTTON_ID, - STATUS_BUTTON_STYLE_MAP, - VOTE_BUTTON_ID, - getStatusAsVerb, -} from "./suggestion-util.ts"; +import { BUTTON_ID, STATUS_BUTTON_STYLE_MAP, VOTE_BUTTON_ID, getStatusAsVerb } from "./suggestion-util.ts"; import { truncate } from "../utils/common.ts"; export const SUGGESTION_USER_ALREADY_VOTED = "UserAlreadyVoted"; @@ -70,10 +65,7 @@ export class Suggestion { */ public static readonly MAX_REASON_LENGTH = 2000; - constructor( - protected data: SuggestionSelect, - private dbConfig: ConfigSelect, - ) {} + constructor(protected data: SuggestionSelect, private dbConfig: ConfigSelect) {} /** Upvote the suggestion. */ public async upvote(userId: string, dbConfig?: ConfigSelect): Promise { @@ -101,10 +93,7 @@ export class Suggestion { } /** Downvote the suggestion. */ - public async downvote( - userId: string, - dbConfig?: ConfigSelect, - ): Promise { + public async downvote(userId: string, dbConfig?: ConfigSelect): Promise { if (!this.canVote) return; const upvotes = new Set(this.data?.upvotedBy); @@ -129,20 +118,9 @@ export class Suggestion { } /** Remove the user's vote for the suggestion. */ - public async removeVote( - userId: string, - dbConfig?: ConfigSelect, - ): Promise { - const upvotes = new Set( - [...(this.data.upvotedBy ?? [])].filter( - (voteUserId) => voteUserId !== userId, - ), - ); - const downvotes = new Set( - [...(this.data.downvotedBy ?? [])].filter( - (voteUserId) => voteUserId !== userId, - ), - ); + public async removeVote(userId: string, dbConfig?: ConfigSelect): Promise { + const upvotes = new Set([...(this.data.upvotedBy ?? [])].filter((voteUserId) => voteUserId !== userId)); + const downvotes = new Set([...(this.data.downvotedBy ?? [])].filter((voteUserId) => voteUserId !== userId)); const updatedSuggestion = await db .update(DbSuggestion) @@ -191,9 +169,7 @@ export class Suggestion { /** Update the suggestion's message */ protected async updateMessage(dbConfig?: ConfigSelect) { const channel = await client.channels.fetch(this.data.channelId); - const suggestionMessage = channel?.isTextBased() - ? await channel?.messages.fetch(this.data.messageId) - : null; + const suggestionMessage = channel?.isTextBased() ? await channel?.messages.fetch(this.data.messageId) : null; const messageOptions = await Suggestion.getMessageOptions(this); // Ensure the message can be edited @@ -201,15 +177,11 @@ export class Suggestion { await suggestionMessage.edit(messageOptions); - const isThreadUnlocked = - suggestionMessage?.thread && !suggestionMessage?.thread.locked; + const isThreadUnlocked = suggestionMessage?.thread && !suggestionMessage?.thread.locked; // Lock thread if suggestion is accepted/rejected if (this.hasUpdatedStatus && isThreadUnlocked) { - await suggestionMessage.thread.setLocked( - true, - "Suggestion got accepted or rejected", - ); + await suggestionMessage.thread.setLocked(true, "Suggestion got accepted or rejected"); } } @@ -283,12 +255,7 @@ export class Suggestion { } /** Create a new suggestion. */ - public static async create({ - description, - channel, - member, - dbConfig, - }: CreateSuggestionOptions): Promise { + public static async create({ description, channel, member, dbConfig }: CreateSuggestionOptions): Promise { const embed = new EmbedBuilder({ title: `Loading suggestion from ${member.user.username}...`, }); @@ -323,12 +290,7 @@ export class Suggestion { } /** Create the {@link Suggestion} instance from an existing suggestion {@link Message}. */ - public static async createFromMessage({ - id, - guildId, - channelId, - url, - }: Message) { + public static async createFromMessage({ id, guildId, channelId, url }: Message) { // TEMP: use .all() and select the first row manually, .get() does not work const foundSuggestion = ( await FIND_BY_MESSAGE_STATEMENT.all({ @@ -339,13 +301,9 @@ export class Suggestion { const dbConfig = getConfig.get({ guildId }); - if (!dbConfig) - throw new Error(`No config in the database for guild with ID ${guildId}`); + if (!dbConfig) throw new Error(`No config in the database for guild with ID ${guildId}`); - if (!foundSuggestion) - throw new Error( - `Could not find a suggestion associated with message ${url}`, - ); + if (!foundSuggestion) throw new Error(`Could not find a suggestion associated with message ${url}`); return new Suggestion(foundSuggestion, dbConfig); } @@ -353,9 +311,7 @@ export class Suggestion { /** Get the message options for this suggestion. */ private static async getMessageOptions(suggestion: Suggestion) { const user = await client.users.fetch(suggestion.userId); - const statusUser = suggestion.statusUserId - ? await client.users.fetch(suggestion.statusUserId) - : null; + const statusUser = suggestion.statusUserId ? await client.users.fetch(suggestion.statusUserId) : null; const hasUpdatedStatus = Boolean(statusUser); const fields: EmbedData["fields"] | undefined = statusUser @@ -375,11 +331,7 @@ export class Suggestion { const embed = new EmbedBuilder({ color: - suggestion.status === "ACCEPTED" - ? Colors.Green - : suggestion.status === "REJECTED" - ? Colors.Red - : Colors.White, + suggestion.status === "ACCEPTED" ? Colors.Green : suggestion.status === "REJECTED" ? Colors.Red : Colors.White, description: suggestion.description ?? undefined, fields, author, @@ -398,10 +350,7 @@ export class Suggestion { embeds: [embed], components: [ new ActionRowBuilder({ - components: [ - getButtonBuilder("ACCEPTED"), - getButtonBuilder("REJECTED"), - ], + components: [getButtonBuilder("ACCEPTED"), getButtonBuilder("REJECTED")], }), new ActionRowBuilder({ components: [ diff --git a/src/types/env.ts b/src/types/env.ts index d1e03dc..8bb81df 100644 --- a/src/types/env.ts +++ b/src/types/env.ts @@ -1,28 +1,12 @@ -import { - number, - object, - string, - minValue, - coerce, - optional, - picklist, - transform, -} from "valibot"; +import { number, object, string, minValue, coerce, optional, picklist, transform } from "valibot"; const env = object({ TOKEN: string(), GUILD: string(), CSE_KEY: string(), CSE_CSX: string(), - SCRAPE_CACHE: optional( - coerce(number([minValue(0, "Must not be negative")]), Number), - ), - ORM_DEBUG: optional( - transform( - picklist(["true", "false"], "Must be true or false"), - (input) => input === "true", - ), - ), + SCRAPE_CACHE: optional(coerce(number([minValue(0, "Must not be negative")]), Number)), + ORM_DEBUG: optional(transform(picklist(["true", "false"], "Must be true or false"), (input) => input === "true")), NODE_ENV: optional(picklist(["production", "development"])), TZ: optional(string()), }); diff --git a/src/utils.ts b/src/utils.ts index 4f5e609..2cf9664 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -30,10 +30,8 @@ getConfig.get = (placeholderValues?: Record | undefined) => { * Formats the content and the URL into a masked URL without embed. * @see {@link djsHyperlink}. */ -export const hyperlink = ( - content: C, - url: U, -) => djsHyperlink(content, hideLinkEmbed(url)); +export const hyperlink = (content: C, url: U) => + djsHyperlink(content, hideLinkEmbed(url)); /** * Capitalizes the first letter of a string. @@ -41,9 +39,8 @@ export const hyperlink = ( * @example * capitalizeFirstLetter('hello world') // "Hello world" */ -export const capitalizeFirstLetter = ( - value: T, -): Capitalize => (value[0].toUpperCase() + value.slice(1)) as Capitalize; +export const capitalizeFirstLetter = (value: T): Capitalize => + (value[0].toUpperCase() + value.slice(1)) as Capitalize; /** * Utility for human readable time values. @@ -68,7 +65,5 @@ export enum Time { * @example * getKeyByValue({ a: 1, b: 2, c: 3 }, 2) // "b" */ -export const getKeyByValue = ( - object: Record, - value: T, -) => Object.keys(object).find((key) => object[key] === value); +export const getKeyByValue = (object: Record, value: T) => + Object.keys(object).find((key) => object[key] === value); diff --git a/src/utils/discordjs.ts b/src/utils/discordjs.ts index 0c866d6..a2ac3b8 100644 --- a/src/utils/discordjs.ts +++ b/src/utils/discordjs.ts @@ -1,9 +1,4 @@ -import { - PermissionFlagsBits, - type Channel, - type GuildBasedChannel, - type TextBasedChannel, -} from "discord.js"; +import { PermissionFlagsBits, type Channel, type GuildBasedChannel, type TextBasedChannel } from "discord.js"; import { UserError } from "./error.ts"; /** @@ -11,23 +6,15 @@ import { UserError } from "./error.ts"; * * @throws {UserError} */ -export const checkIsValidTextChannel = ( - channel: Channel, -): channel is GuildBasedChannel & TextBasedChannel => { - if (channel.isDMBased()) - throw new UserError(`${channel} must be a guild channel`); +export const checkIsValidTextChannel = (channel: Channel): channel is GuildBasedChannel & TextBasedChannel => { + if (channel.isDMBased()) throw new UserError(`${channel} must be a guild channel`); - if (!channel.isTextBased()) - throw new UserError(`${channel} must be a text channel`); + if (!channel.isTextBased()) throw new UserError(`${channel} must be a text channel`); - const clientPermissionsInChannel = channel.permissionsFor( - channel.client.user, - ); + const clientPermissionsInChannel = channel.permissionsFor(channel.client.user); if (!clientPermissionsInChannel?.has(PermissionFlagsBits.SendMessages)) - throw new UserError( - `I do not have permission to send message to ${channel}`, - ); + throw new UserError(`I do not have permission to send message to ${channel}`); return true; }; From ae857c4e842ea6b3b6c4932ac31336a8f0c44b99 Mon Sep 17 00:00:00 2001 From: Gus Libens <27970303+goestav@users.noreply.github.com> Date: Thu, 8 Aug 2024 11:58:29 +0200 Subject: [PATCH 10/32] refactor: remove unused imports --- src/structures/configuration/configuration-message.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/structures/configuration/configuration-message.ts b/src/structures/configuration/configuration-message.ts index 175c0d0..a6526ec 100644 --- a/src/structures/configuration/configuration-message.ts +++ b/src/structures/configuration/configuration-message.ts @@ -7,31 +7,25 @@ import { ChannelSelectMenuBuilder, ChatInputCommandInteraction, codeBlock, - DiscordjsError, - DiscordjsErrorCodes, EmbedBuilder, inlineCode, InteractionResponse, italic, Message, MessageComponentInteraction, - ModalBuilder, - ModalSubmitInteraction, roleMention, RoleSelectMenuBuilder, StringSelectMenuBuilder, - TextInputBuilder, TextInputStyle, type APIActionRowComponent, type APIMessageActionRowComponent, type BaseMessageOptions, type CollectedInteraction, type MessageActionRowComponentBuilder, - type ModalActionRowComponentBuilder, } from "discord.js"; import type { ConfigurationOption, ConfigurationOptionType } from "./configuration-manifest.ts"; import db from "../../db.ts"; -import { and, Table as DrizzleTable, eq, SQL, type InferSelectModel } from "drizzle-orm"; +import { and, Table as DrizzleTable, SQL, type InferSelectModel } from "drizzle-orm"; import { Time } from "../../utils.ts"; import { truncate } from "../../utils/common.ts"; import { handleInteractionCollect } from "./interaction-handlers.ts"; From 83fe92ec6678889bd2a80a838c3b3f102e96bdf4 Mon Sep 17 00:00:00 2001 From: Gus Libens <27970303+goestav@users.noreply.github.com> Date: Fri, 9 Aug 2024 22:15:57 +0200 Subject: [PATCH 11/32] refactor: simplify switch logic for the configuration component --- .../configuration/configuration-message.ts | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/src/structures/configuration/configuration-message.ts b/src/structures/configuration/configuration-message.ts index a6526ec..97bbc8a 100644 --- a/src/structures/configuration/configuration-message.ts +++ b/src/structures/configuration/configuration-message.ts @@ -235,30 +235,24 @@ export class ConfigurationMessage
{ return component; } case "channel": { - const component = new ChannelSelectMenuBuilder().setCustomId(customId).setMaxValues(1); - + return new ChannelSelectMenuBuilder() + .setCustomId(customId) + .setPlaceholder(manifestOption.placeholder ?? "") + .setMaxValues(1); // TODO: set channel type - - if (manifestOption.placeholder) component.setPlaceholder(manifestOption.placeholder); - - return component; } case "role": { - const component = new RoleSelectMenuBuilder().setCustomId(customId).setMaxValues(1); - - if (manifestOption.placeholder) component.setPlaceholder(manifestOption.placeholder); - - return component; + return new RoleSelectMenuBuilder() + .setCustomId(customId) + .setPlaceholder(manifestOption.placeholder ?? "") + .setMaxValues(1); } case "select": { - const component = new StringSelectMenuBuilder() + return new StringSelectMenuBuilder() .setCustomId(customId) + .setPlaceholder(manifestOption.placeholder ?? "") .addOptions(manifestOption.options) .setMaxValues(1); - - if (manifestOption.placeholder) component.setPlaceholder(manifestOption.placeholder); - - return component; } } } From 7953d1c7cbfdf843527be1284b5e73e6954ad065 Mon Sep 17 00:00:00 2001 From: Gus Libens <27970303+goestav@users.noreply.github.com> Date: Fri, 9 Aug 2024 22:32:04 +0200 Subject: [PATCH 12/32] refactor: add required property to configuration manifest option --- src/structures/configuration/configuration-manifest.ts | 2 ++ src/structures/configuration/interaction-handlers.ts | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/structures/configuration/configuration-manifest.ts b/src/structures/configuration/configuration-manifest.ts index ea3ba0a..ef0dba7 100644 --- a/src/structures/configuration/configuration-manifest.ts +++ b/src/structures/configuration/configuration-manifest.ts @@ -24,6 +24,8 @@ interface PartialConfigurationOption< description: string; /** @default 'text' */ type?: Type; + /** @default false */ + required?: boolean; /** Validate the input, validation succeeds when `true` is returned. */ validate?: ConfigurationOptionValidateFn; /** Transform the value when persisting to the database. */ diff --git a/src/structures/configuration/interaction-handlers.ts b/src/structures/configuration/interaction-handlers.ts index 943f4bf..ced0008 100644 --- a/src/structures/configuration/interaction-handlers.ts +++ b/src/structures/configuration/interaction-handlers.ts @@ -22,7 +22,6 @@ const handleTextInteractionCollect = async ( modalInputCustomId: string, value: string | null, ) => { - let modalSubmitInteraction: ModalSubmitInteraction<"cached" | "raw">; const modalCustomId = getCustomId(manifestOption, "modal"); // Use a modal to get the updated value @@ -32,7 +31,8 @@ const handleTextInteractionCollect = async ( .setLabel(manifestOption.name) .setValue(value ?? "") .setPlaceholder(manifestOption.placeholder ?? "") - .setStyle(manifestOption.style ?? TextInputStyle.Short); + .setStyle(manifestOption.style ?? TextInputStyle.Short) + .setRequired(manifestOption.required ?? false); const actionRow = new ActionRowBuilder().addComponents(modalInput); const modal = new ModalBuilder().setTitle(manifestOption.name).setCustomId(modalCustomId).addComponents(actionRow); From 3bbaf16f5f17c557b22ed363736088d808b74122 Mon Sep 17 00:00:00 2001 From: Gus Libens <27970303+goestav@users.noreply.github.com> Date: Sat, 10 Aug 2024 00:46:03 +0200 Subject: [PATCH 13/32] refactor: make not set text more consistent for paragraph text options --- src/structures/configuration/configuration-message.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/structures/configuration/configuration-message.ts b/src/structures/configuration/configuration-message.ts index 97bbc8a..f297922 100644 --- a/src/structures/configuration/configuration-message.ts +++ b/src/structures/configuration/configuration-message.ts @@ -285,11 +285,13 @@ export class ConfigurationMessage
{ const descriptionFormatted = italic(manifestOption.description); if (manifestOption.type === "text" && manifestOption.style === TextInputStyle.Paragraph) { - const valueFormatted = databaseValue ? codeBlock(truncate(databaseValue as string, 40)) : this.#notSetText; + const valueFormatted = databaseValue + ? codeBlock(truncate(databaseValue as string, 40)) + : `${this.#notSetText}\n`; content += `${nameFormatted}\n-# ${descriptionFormatted}\n${valueFormatted}\n`; } else { - const valueFormatted = this.formatValue(manifestOption.type, databaseValue); + const valueFormatted = this.formatValue(manifestOption, databaseValue); content += `${nameFormatted} — ${valueFormatted}\n-# ${descriptionFormatted}\n\n`; } @@ -318,8 +320,8 @@ export class ConfigurationMessage
{ } /** Format the value to display in an embed. */ - private formatValue(type: ConfigurationOptionType, value: unknown): string { - switch (type) { + private formatValue(manifestOption: ConfigurationOption, value: unknown): string { + switch (manifestOption.type) { case "boolean": { if (typeof value !== "boolean") return this.#notSetText; From 513926fb4ecdb990831cee1bf75140b53b527952 Mon Sep 17 00:00:00 2001 From: Gus Libens <27970303+goestav@users.noreply.github.com> Date: Sat, 17 Aug 2024 14:45:57 +0200 Subject: [PATCH 14/32] refactor: use string select menu for configuration --- src/commands/config/gateway.ts | 2 +- src/commands/config/logging.ts | 2 +- src/commands/config/suggestion.ts | 2 +- .../configuration/configuration-message.ts | 302 ++++++------------ .../configuration/interaction-handlers.ts | 40 +-- 5 files changed, 111 insertions(+), 237 deletions(-) diff --git a/src/commands/config/gateway.ts b/src/commands/config/gateway.ts index 14e9533..d70c9e6 100644 --- a/src/commands/config/gateway.ts +++ b/src/commands/config/gateway.ts @@ -64,7 +64,7 @@ const ConfigCommand: Command = { return; } - const configurationMessage = new ConfigurationMessage(Config, manifest, { + const configurationMessage = new ConfigurationMessage(manifest, { getWhereClause: ({ table, interaction }) => eq(table.id, interaction.guildId), }); diff --git a/src/commands/config/logging.ts b/src/commands/config/logging.ts index 9b54903..f748812 100644 --- a/src/commands/config/logging.ts +++ b/src/commands/config/logging.ts @@ -55,7 +55,7 @@ const ConfigCommand: Command = { return; } - const configurationMessage = new ConfigurationMessage(Config, manifest, { + const configurationMessage = new ConfigurationMessage(manifest, { getWhereClause: ({ table, interaction }) => eq(table.id, interaction.guildId), }); diff --git a/src/commands/config/suggestion.ts b/src/commands/config/suggestion.ts index 7af4404..8b8c00c 100644 --- a/src/commands/config/suggestion.ts +++ b/src/commands/config/suggestion.ts @@ -52,7 +52,7 @@ const ConfigCommand: Command = { return; } - const configurationMessage = new ConfigurationMessage(Config, manifest, { + const configurationMessage = new ConfigurationMessage(manifest, { getWhereClause: ({ table, interaction }) => eq(table.id, interaction.guildId), }); diff --git a/src/structures/configuration/configuration-message.ts b/src/structures/configuration/configuration-message.ts index f297922..91548cd 100644 --- a/src/structures/configuration/configuration-message.ts +++ b/src/structures/configuration/configuration-message.ts @@ -1,47 +1,38 @@ import { ActionRowBuilder, - bold, - ButtonBuilder, - ButtonStyle, - channelMention, - ChannelSelectMenuBuilder, ChatInputCommandInteraction, - codeBlock, - EmbedBuilder, - inlineCode, + ComponentType, + DiscordjsErrorCodes, + DiscordjsTypeError, InteractionResponse, - italic, Message, MessageComponentInteraction, - roleMention, - RoleSelectMenuBuilder, StringSelectMenuBuilder, - TextInputStyle, - type APIActionRowComponent, - type APIMessageActionRowComponent, type BaseMessageOptions, type CollectedInteraction, - type MessageActionRowComponentBuilder, + type InteractionReplyOptions, } from "discord.js"; -import type { ConfigurationOption, ConfigurationOptionType } from "./configuration-manifest.ts"; +import type { ConfigurationOption } from "./configuration-manifest.ts"; import db from "../../db.ts"; -import { and, Table as DrizzleTable, SQL, type InferSelectModel } from "drizzle-orm"; +import { and, Table as DrizzleTable, SQL } from "drizzle-orm"; import { Time } from "../../utils.ts"; -import { truncate } from "../../utils/common.ts"; -import { handleInteractionCollect } from "./interaction-handlers.ts"; -import { getCustomId } from "./utils.ts"; +import { promptNewConfigurationOptionValue } from "./interaction-handlers.ts"; -interface GetMessageOptionsContext { - table: T; +enum InteractionCustomId { + MainMenu = "config-main-menu", +} + +interface GetMessageOptionsContext
{ + table: Table; interaction: | ChatInputCommandInteraction<"cached" | "raw"> | CollectedInteraction<"cached" | "raw"> | MessageComponentInteraction<"cached" | "raw">; } -interface ConfigurationMessageOptions { +interface ConfigurationMessageOptions
{ /** Dynamically create the where clause for the database query. */ - getWhereClause: (context: GetMessageOptionsContext) => SQL; + getWhereClause: (context: GetMessageOptionsContext
) => SQL; /** * How long will this configuration message be interactable. * @@ -51,9 +42,14 @@ interface ConfigurationMessageOptions { } /** Represents a configuration message. */ -export class ConfigurationMessage
{ - #table: Table; - #manifest: ConfigurationOption[]; +export class ConfigurationMessage< + Option extends ConfigurationOption, + Table extends DrizzleTable = Option["table"], +> { + /** How long the configuration message should stay alive. */ + public readonly TIMEOUT = Time.Minute * 10; + + #manifest: Option[]; #options: ConfigurationMessageOptions
; /** The {@link InteractionResponse} or {@link Message} for the current configuration message. */ @@ -61,15 +57,7 @@ export class ConfigurationMessage
{ #initialized = false; - /** The text for configuration options that aren't set */ - #notSetText = italic("(Not set)"); - - constructor( - table: Table, - manifest: ConfigurationOption[], - options: ConfigurationMessageOptions
, - ) { - this.#table = table; + constructor(manifest: Option[], options: ConfigurationMessageOptions
) { this.#manifest = manifest; this.#options = options; } @@ -78,11 +66,11 @@ export class ConfigurationMessage
{ public async initialize(interaction: ChatInputCommandInteraction): Promise { if (!interaction.inGuild() || this.#initialized) return; - const messageOptions = await this.getMessageOptions(interaction); + const messageOptions = await this.getMainMenuMessageOptions(); await this.replyOrEdit(interaction, messageOptions); - await this.initializeListeners(); + this.initializeListeners(); } /** Stop listening to component interactions and clean up internal state. */ @@ -94,25 +82,30 @@ export class ConfigurationMessage
{ /** Add the component interaction listeners. */ private initializeListeners() { - const manifestOptionMap = Object.fromEntries(this.#manifest.map((option) => [getCustomId(option), option])); - if (!this.#reply) throw new Error("No internal reply message or interaction response available"); + const manifestOptionMap = Object.fromEntries( + this.#manifest.map((option) => [option.name, option as ConfigurationOption
]), + ); + const collector = this.#reply.createMessageComponentCollector({ + componentType: ComponentType.StringSelect, filter: async (interaction) => { - const isValidCustomId = Object.keys(manifestOptionMap).includes(interaction.customId); const message = await this.getReplyMessage(); - const isReplyAuthor = message.author.id === interaction.user.id; + const isReplyAuthor = message.interaction?.user.id === interaction.user.id; + const isReplyMessage = interaction.message.id === message.id; + + if (!Object.keys(manifestOptionMap).includes(interaction.values[0])) return false; - return isValidCustomId || isReplyAuthor; + return isReplyAuthor && isReplyMessage; }, - time: Time.Minute * 10, + time: this.TIMEOUT, }); collector.on("collect", async (interaction) => { if (!interaction.inGuild()) throw new Error("Interaction happened outside a guild"); - this.handleInteractionCollect(interaction, manifestOptionMap[interaction.customId]); + await this.handleInteractionCollect(interaction, manifestOptionMap[interaction.values[0]]); }); } @@ -120,44 +113,61 @@ export class ConfigurationMessage
{ private async getReplyMessage() { if (!this.#reply) throw new Error("No internal reply message or interaction response available"); - return this.#reply instanceof Message ? this.#reply : this.#reply.fetch(); + return this.#reply instanceof Message ? this.#reply : await this.#reply.fetch(); } private async handleInteractionCollect( interaction: MessageComponentInteraction<"cached" | "raw">, - manifestOption: ConfigurationOption, + manifestOption: ConfigurationOption
, ) { const whereClause = this.#options.getWhereClause({ - table: this.#table, + table: manifestOption.table, interaction, }); - const { value } = db + const { value: currentValue } = db .select({ - value: this.#table[manifestOption.column as keyof object], + value: manifestOption.table[manifestOption.column as keyof object], }) - .from(this.#table) + .from(manifestOption.table) .where(and(whereClause)) .all() // TEMP: use .all() and select the first row manually, .get() does not work .at(0) as { value: unknown }; - const handleCollectResult = await handleInteractionCollect(interaction, manifestOption, value); + let newValue: unknown; - if (handleCollectResult === null) return; + try { + newValue = await promptNewConfigurationOptionValue(interaction, manifestOption, currentValue); + } catch (error) { + if (error instanceof DiscordjsTypeError) { + if ( + error.code === DiscordjsErrorCodes.InteractionCollectorError && + error.message === "Collector received no interactions before ending with reason: time" + ) { + // Exit early because an interaction collector timed out + return; + } + } - const [followUpInteraction, updatedValue] = handleCollectResult; + if ( + !(error instanceof DiscordjsTypeError && error.code === DiscordjsErrorCodes.ModalSubmitInteractionFieldNotFound) + ) + throw error; - db.update(this.#table) - .set({ - [manifestOption.column as keyof object]: updatedValue ? updatedValue : null, - }) + // User did not provide a value, meaning the database value should be set to NULL + newValue = null; + } + + db.update(manifestOption.table) + .set({ [manifestOption.column as keyof object]: newValue ? newValue : null }) .where(whereClause) .run(); - const messageOptions = await this.getMessageOptions(followUpInteraction); - - this.updateReply(messageOptions); + await interaction.followUp({ + content: "Value updated successfully", + ephemeral: true, + }); } /** Reply to a message or edit the reply if the interaction got replied to or is deferred and keep reply in memory. */ @@ -165,6 +175,8 @@ export class ConfigurationMessage
{ interaction: ChatInputCommandInteraction, messageOptions: BaseMessageOptions, ): Promise { + if (!interaction.isRepliable) return; + let replyPromise: Promise; if (interaction.replied || interaction.deferred) { @@ -178,161 +190,35 @@ export class ConfigurationMessage
{ this.#reply = messageOrInteractionResponse; } catch (error) { - await interaction.followUp("Something went wrong... Try again later"); - this.destroy(); - } - } - - /** Update the reply message. */ - private async updateReply(messageOptions: BaseMessageOptions): Promise { - if (!this.#reply) throw new Error("No internal reply message or interaction response available"); - - this.#reply = await this.#reply.edit(messageOptions); - } - - /** Get the action rows with components. */ - private getActionRows(): APIActionRowComponent[] { - const actionRows: ActionRowBuilder[] = [new ActionRowBuilder()]; - - for (const manifestOption of this.#manifest) { - const component = this.getActionRowComponent(manifestOption); - - if (["select", "channel", "role"].includes(manifestOption.type)) { - const row = new ActionRowBuilder().addComponents(component); - - actionRows.push(row); - } else { - // Ensure that buttons are always in the first action row - actionRows[0].addComponents(component); - } - } - - return actionRows.map((row) => row.toJSON()); - } - - /** Get the component for a manifest option. */ - private getActionRowComponent(manifestOption: ConfigurationOption): MessageActionRowComponentBuilder { - const customId = getCustomId(manifestOption); - - switch (manifestOption.type) { - case "text": { - const component = new ButtonBuilder() - .setCustomId(customId) - .setLabel(manifestOption.label ?? manifestOption.name) - .setStyle(ButtonStyle.Primary); + const action = interaction.replied ? "followUp" : "reply"; - if (manifestOption.emoji) component.setEmoji(manifestOption.emoji); + await interaction[action]({ + content: "Something went wrong... Try again later", + ephemeral: true, + }); - return component; - } - case "boolean": { - const component = new ButtonBuilder() - .setCustomId(customId) - .setLabel(manifestOption.label ?? manifestOption.name); - - if (manifestOption.emoji) component.setEmoji(manifestOption.emoji); - - return component; - } - case "channel": { - return new ChannelSelectMenuBuilder() - .setCustomId(customId) - .setPlaceholder(manifestOption.placeholder ?? "") - .setMaxValues(1); - // TODO: set channel type - } - case "role": { - return new RoleSelectMenuBuilder() - .setCustomId(customId) - .setPlaceholder(manifestOption.placeholder ?? "") - .setMaxValues(1); - } - case "select": { - return new StringSelectMenuBuilder() - .setCustomId(customId) - .setPlaceholder(manifestOption.placeholder ?? "") - .addOptions(manifestOption.options) - .setMaxValues(1); - } + this.destroy(); } } - /** Get the message options for the configuration message. */ - private async getMessageOptions( - interaction: ChatInputCommandInteraction<"cached" | "raw"> | CollectedInteraction<"cached" | "raw">, - ): Promise { - let content = ""; - - const whereClause = this.#options.getWhereClause({ - table: this.#table, - interaction, - }); - - const databaseValues = db - .select() - .from(this.#table) - .where(whereClause) - .all() - // TEMP: use .all() and select the first row manually, .get() does not work - .at(0); - - if (!databaseValues) throw new Error("Could not retrieve the configuration"); - - for (const manifestOption of this.#manifest) { - const databaseValue = this.getOptionValue(manifestOption, databaseValues[manifestOption.column]); - - const nameFormatted = bold(manifestOption.name); - const descriptionFormatted = italic(manifestOption.description); - - if (manifestOption.type === "text" && manifestOption.style === TextInputStyle.Paragraph) { - const valueFormatted = databaseValue - ? codeBlock(truncate(databaseValue as string, 40)) - : `${this.#notSetText}\n`; - - content += `${nameFormatted}\n-# ${descriptionFormatted}\n${valueFormatted}\n`; - } else { - const valueFormatted = this.formatValue(manifestOption, databaseValue); - - content += `${nameFormatted} — ${valueFormatted}\n-# ${descriptionFormatted}\n\n`; - } + /** Get the main menu message options. */ + private getMainMenuMessageOptions(): InteractionReplyOptions { + const selectMenu = new StringSelectMenuBuilder() + .setCustomId(InteractionCustomId.MainMenu) + .setPlaceholder("Select a configuration option") + .setMaxValues(1); + + for (const [index, { name }] of this.#manifest.entries()) { + selectMenu.addOptions({ + label: `${index + 1}. ${name}`, + value: name, + }); } - const embed = new EmbedBuilder() - .setColor("Blue") - // Should be able to configure through the constructor - .setTitle("Configuration") - .setDescription(content.trim()); - return { - embeds: [embed], - components: this.getActionRows(), + content: "Select which configuration option you want to view/edit:", + components: [new ActionRowBuilder().addComponents(selectMenu)], + ephemeral: true, }; } - - /** Get the raw or transformed (return value of {@link ConfigurationOption.fromDatabase|fromDatabase}) database value. */ - private getOptionValue>( - manifestOption: T, - value: InferSelectModel[T["column"]], - ): unknown { - if (manifestOption.fromDatabase) return manifestOption.fromDatabase(value); - - return value; - } - - /** Format the value to display in an embed. */ - private formatValue(manifestOption: ConfigurationOption, value: unknown): string { - switch (manifestOption.type) { - case "boolean": { - if (typeof value !== "boolean") return this.#notSetText; - - return value ? "Yes" : "No"; - } - case "channel": - return value ? channelMention(value as string) : this.#notSetText; - case "role": - return value ? roleMention(value as string) : this.#notSetText; - default: - return value ? inlineCode(value as string) : this.#notSetText; - } - } } diff --git a/src/structures/configuration/interaction-handlers.ts b/src/structures/configuration/interaction-handlers.ts index ced0008..f90e009 100644 --- a/src/structures/configuration/interaction-handlers.ts +++ b/src/structures/configuration/interaction-handlers.ts @@ -1,7 +1,7 @@ import { ActionRowBuilder, - DiscordjsError, DiscordjsErrorCodes, + DiscordjsTypeError, ModalBuilder, ModalSubmitInteraction, TextInputBuilder, @@ -15,8 +15,8 @@ import type { Table as DrizzleTable } from "drizzle-orm"; import { Time } from "../../utils.ts"; import { getCustomId } from "./utils.ts"; -/** Handle the interaction for a text option. */ -const handleTextInteractionCollect = async ( +/** Get the user input through a modal. */ +const getModalInput = async ( interaction: MessageComponentInteraction, manifestOption: ConfigurationOption & { type: "text" }, modalInputCustomId: string, @@ -39,50 +39,38 @@ const handleTextInteractionCollect = async ( await interaction.showModal(modal); - try { - return (await interaction.awaitModalSubmit({ - time: Time.Minute * 2, - })) as ModalSubmitInteraction<"cached" | "raw">; - } catch (error) { - // Ignore no interactions due to timeout error - if ( - error instanceof DiscordjsError && - error.code === DiscordjsErrorCodes.InteractionCollectorError && - error.message === "Collector received no interactions before ending with reason: time" - ) - return null; + const modalSubmitInteraction = (await interaction.awaitModalSubmit({ + filter: (interaction) => interaction.customId === modalCustomId, + time: Time.Minute * 2, + })) as ModalSubmitInteraction<"cached" | "raw">; - throw error; - } + return [modalSubmitInteraction, modalSubmitInteraction.fields.getTextInputValue(modalInputCustomId)] as const; }; -/** Handle any kind of message component interaction. */ -export const handleInteractionCollect = async ( +/** Get the new value for a configuration option. */ +export const promptNewConfigurationOptionValue = async ( interaction: MessageComponentInteraction, manifestOption: ConfigurationOption, value: unknown, -): Promise<[CollectedInteraction<"cached" | "raw">, unknown] | null> => { +): Promise => { let updatedValue: unknown; let followUpInteraction!: CollectedInteraction<"cached" | "raw">; if (manifestOption.type === "text") { const modalInputCustomId = getCustomId(manifestOption, "modal-input"); - const modalSubmitInteraction = await handleTextInteractionCollect( + const [modalSubmitInteraction, newValue] = await getModalInput( interaction, manifestOption, modalInputCustomId, value as string | null, ); - // Return early because the component timed out - if (modalSubmitInteraction === null) return null; - - updatedValue = modalSubmitInteraction?.fields.getTextInputValue(modalInputCustomId); followUpInteraction = modalSubmitInteraction; + updatedValue = newValue; } // Silently acknowledge user input interaction await followUpInteraction.deferUpdate(); - return [followUpInteraction, updatedValue]; + return updatedValue; }; From 135779c7134b4b221c8ac68087911acefa30545c Mon Sep 17 00:00:00 2001 From: Gus Libens <27970303+goestav@users.noreply.github.com> Date: Sun, 18 Aug 2024 20:06:32 +0200 Subject: [PATCH 15/32] refactor: rename interaction-handlers to prompt-user-input --- src/structures/configuration/configuration-message.ts | 2 +- .../{interaction-handlers.ts => prompt-user-input.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/structures/configuration/{interaction-handlers.ts => prompt-user-input.ts} (100%) diff --git a/src/structures/configuration/configuration-message.ts b/src/structures/configuration/configuration-message.ts index 91548cd..5bd2d59 100644 --- a/src/structures/configuration/configuration-message.ts +++ b/src/structures/configuration/configuration-message.ts @@ -16,7 +16,7 @@ import type { ConfigurationOption } from "./configuration-manifest.ts"; import db from "../../db.ts"; import { and, Table as DrizzleTable, SQL } from "drizzle-orm"; import { Time } from "../../utils.ts"; -import { promptNewConfigurationOptionValue } from "./interaction-handlers.ts"; +import { promptNewConfigurationOptionValue } from "./prompt-user-input.ts"; enum InteractionCustomId { MainMenu = "config-main-menu", diff --git a/src/structures/configuration/interaction-handlers.ts b/src/structures/configuration/prompt-user-input.ts similarity index 100% rename from src/structures/configuration/interaction-handlers.ts rename to src/structures/configuration/prompt-user-input.ts From 9c1a7eba4400e974a70ef1574ec71e261512faf2 Mon Sep 17 00:00:00 2001 From: Gus Libens <27970303+goestav@users.noreply.github.com> Date: Sun, 18 Aug 2024 20:07:10 +0200 Subject: [PATCH 16/32] refactor: rename getModalInput to promptModalValue --- src/structures/configuration/prompt-user-input.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structures/configuration/prompt-user-input.ts b/src/structures/configuration/prompt-user-input.ts index f90e009..4fc32f3 100644 --- a/src/structures/configuration/prompt-user-input.ts +++ b/src/structures/configuration/prompt-user-input.ts @@ -16,7 +16,7 @@ import { Time } from "../../utils.ts"; import { getCustomId } from "./utils.ts"; /** Get the user input through a modal. */ -const getModalInput = async ( +const promptModalValue = async ( interaction: MessageComponentInteraction, manifestOption: ConfigurationOption & { type: "text" }, modalInputCustomId: string, @@ -58,7 +58,7 @@ export const promptNewConfigurationOptionValue = async ( if (manifestOption.type === "text") { const modalInputCustomId = getCustomId(manifestOption, "modal-input"); - const [modalSubmitInteraction, newValue] = await getModalInput( + const [modalSubmitInteraction, newValue] = await promptModalValue( interaction, manifestOption, modalInputCustomId, From 9dab68ea647dce78b13f0a68b8373746cacdc51b Mon Sep 17 00:00:00 2001 From: Gus Libens <27970303+goestav@users.noreply.github.com> Date: Sun, 18 Aug 2024 20:07:57 +0200 Subject: [PATCH 17/32] refactor: remove unused imports --- src/structures/configuration/prompt-user-input.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/structures/configuration/prompt-user-input.ts b/src/structures/configuration/prompt-user-input.ts index 4fc32f3..bb9d86a 100644 --- a/src/structures/configuration/prompt-user-input.ts +++ b/src/structures/configuration/prompt-user-input.ts @@ -1,7 +1,5 @@ import { ActionRowBuilder, - DiscordjsErrorCodes, - DiscordjsTypeError, ModalBuilder, ModalSubmitInteraction, TextInputBuilder, From 0c0b9e3876645a95f62f90102568e65736c17d61 Mon Sep 17 00:00:00 2001 From: Gus Libens <27970303+goestav@users.noreply.github.com> Date: Sun, 18 Aug 2024 22:29:13 +0200 Subject: [PATCH 18/32] refactor: add find placeholder utility --- package.json | 3 +- src/utils/placeholder.test.ts | 39 +++++++++++++++++ src/utils/placeholder.ts | 80 +++++++++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 src/utils/placeholder.test.ts create mode 100644 src/utils/placeholder.ts diff --git a/package.json b/package.json index 778b086..ba73103 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "@biomejs/biome": "^1.4.1", "better-sqlite3": "^9.2.2", "bun-types": "^1.0.20", - "drizzle-kit": "^0.20.9" + "drizzle-kit": "^0.20.9", + "vitest": "^2.0.5" }, "author": "Jacob Jackson", "license": "GPL-3.0-or-later", diff --git a/src/utils/placeholder.test.ts b/src/utils/placeholder.test.ts new file mode 100644 index 0000000..21a9562 --- /dev/null +++ b/src/utils/placeholder.test.ts @@ -0,0 +1,39 @@ +import { describe, it, expect } from "vitest"; +import { findPlaceholders } from "./placeholder.ts"; + +describe("Utils - Placeholder", () => { + describe(`${findPlaceholders.name}()`, () => { + it.each([ + ["Hello, [name]!", [["name", 7, 12]]], + // biome-ignore format: more readable when inline + ["Hello, [yourName]! I'm [myName].", [["yourName", 7, 16], ["myName", 23, 30]]], + // biome-ignore format: more readable when inline + ["Hello, [[yourName]! I'm [myName]].",[["yourName", 8, 17],["myName", 24, 31]]], + ])('Should find the placeholders for the string "%s"', (text, expected) => { + const actual = findPlaceholders(text); + + expect(actual).toEqual(expected); + }); + + it("Should not find any placeholders when none are present", () => { + const actual = findPlaceholders("Hello you!"); + + expect(actual).toEqual([]); + }); + + it("Should only find the deepest nested placeholder", () => { + const actual = findPlaceholders("Hello [te[name]xt]!"); + + expect(actual).toEqual([["name", 9, 14]]); + }); + + it.each([ + ["Hello, [[name]]!", []], + ["Hello, [yourName]! I'm [[myName]].", [["yourName", 7, 16]]], + ])('Should ignore escaped placeholders for the string "%s"', (text, expected) => { + const actual = findPlaceholders(text); + + expect(actual).toEqual(expected); + }); + }); +}); diff --git a/src/utils/placeholder.ts b/src/utils/placeholder.ts new file mode 100644 index 0000000..ae92b72 --- /dev/null +++ b/src/utils/placeholder.ts @@ -0,0 +1,80 @@ +const START_DELIMITER = "["; +const END_DELIMITER = "]"; + +/** Check if a character is part of the latin alphabet. */ +const isLatinLetter = (character: string) => character.toUpperCase() !== character.toLowerCase(); + +/** + * Find all placeholders in a string. + * + * @example + * findPlaceholders('Hello [name]!') // [["name", 6, 11]] + * + * @example + * findPlaceholders('Hello [[name]]!') // [] + */ +export const findPlaceholders = (text: string) => { + /** + * The placeholders mapped as the following nested array structure:\ + * `0`: placeholder name\ + * `1`: offset of the placeholder start delimiter\ + * `2`: offset of the placeholder end delimiter + */ + const placeholderMap: [string, number, number][] = []; + + let placeholderStarted = false; + let isEscaped = false; + let delimiterStartOffset = -1; + let buffer = ""; + + const resetState = () => { + placeholderStarted = false; + isEscaped = false; + delimiterStartOffset = -1; + buffer = ""; + }; + + for (let i = 0; i < text.length; i++) { + const nextCharacter = text[i + 1]; + const currentCharacter = text[i]; + const previousCharacter = text[i - 1]; + + // Reset state when nested delimiters exist + if (placeholderStarted && currentCharacter === START_DELIMITER) resetState(); + + // Placeholder starts + if (currentCharacter === START_DELIMITER && nextCharacter !== START_DELIMITER) { + if (previousCharacter === START_DELIMITER) isEscaped = true; + + delimiterStartOffset = i; + placeholderStarted = true; + continue; + } + + // Placeholder ends + if (buffer && currentCharacter === END_DELIMITER) { + // Escaped placeholders should not be processed + if (isEscaped && nextCharacter === END_DELIMITER) { + resetState(); + continue; + } + + placeholderMap.push([buffer, delimiterStartOffset, i]); + resetState(); + continue; + } + + if (placeholderStarted) { + // Non alphabetic character + if (!isLatinLetter(currentCharacter)) { + resetState(); + continue; + } + + // Normal character (placeholder name) + buffer += currentCharacter; + } + } + + return placeholderMap; +}; From 08d6ca4ca0e99f15016d34debe91c3e9711c32f4 Mon Sep 17 00:00:00 2001 From: Gus Libens <27970303+goestav@users.noreply.github.com> Date: Fri, 23 Aug 2024 14:17:34 +0200 Subject: [PATCH 19/32] refactor: add placeholder replace utility function --- src/utils/placeholder.test.ts | 34 ++++++++++++- src/utils/placeholder.ts | 93 ++++++++++++++++++++++++++--------- 2 files changed, 104 insertions(+), 23 deletions(-) diff --git a/src/utils/placeholder.test.ts b/src/utils/placeholder.test.ts index 21a9562..779fec0 100644 --- a/src/utils/placeholder.test.ts +++ b/src/utils/placeholder.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { findPlaceholders } from "./placeholder.ts"; +import { findPlaceholders, replacePlaceholders } from "./placeholder.ts"; describe("Utils - Placeholder", () => { describe(`${findPlaceholders.name}()`, () => { @@ -36,4 +36,36 @@ describe("Utils - Placeholder", () => { expect(actual).toEqual(expected); }); }); + + describe(`${replacePlaceholders.name}()`, () => { + it.each([ + ["Hello, [name]!", { name: "you" }, "Hello, you!"], + ["Hello, [yourName]! I'm [myName]", { yourName: "you", myName: "me" }, "Hello, you! I'm me"], + ])('Should replace the placeholders for the string "%s"', (text, placeholderMap, expected) => { + const actual = replacePlaceholders(text, placeholderMap); + + expect(actual).toBe(expected); + }); + + it("Should not replace any placeholders when none are present", () => { + const actual = replacePlaceholders("Hello you!", { name: "you" }); + + expect(actual).toBe("Hello you!"); + }); + + it("Should only find the deepest nested placeholder", () => { + const actual = replacePlaceholders("Hello [te[name]xt]!", { name: "you" }); + + expect(actual).toBe("Hello [teyouxt]!"); + }); + + it.each([ + ["Hello, [[name]]!", { name: "you" }, "Hello, [[name]]!"], + ["Hello, [yourName]! I'm [[myName]].", { yourName: "you", myName: "me" }, "Hello, you! I'm [[myName]]."], + ])('Should ignore escaped placeholders for the string "%s"', (text, placeholderMap, expected) => { + const actual = replacePlaceholders(text, placeholderMap); + + expect(actual).toBe(expected); + }); + }); }); diff --git a/src/utils/placeholder.ts b/src/utils/placeholder.ts index ae92b72..cafefdc 100644 --- a/src/utils/placeholder.ts +++ b/src/utils/placeholder.ts @@ -4,28 +4,25 @@ const END_DELIMITER = "]"; /** Check if a character is part of the latin alphabet. */ const isLatinLetter = (character: string) => character.toUpperCase() !== character.toLowerCase(); -/** - * Find all placeholders in a string. - * - * @example - * findPlaceholders('Hello [name]!') // [["name", 6, 11]] - * - * @example - * findPlaceholders('Hello [[name]]!') // [] - */ -export const findPlaceholders = (text: string) => { - /** - * The placeholders mapped as the following nested array structure:\ - * `0`: placeholder name\ - * `1`: offset of the placeholder start delimiter\ - * `2`: offset of the placeholder end delimiter - */ - const placeholderMap: [string, number, number][] = []; +interface PlaceholderReplaceHookContext { + /** The offset of the placeholder start delimiter. */ + startOffset: number; + /** The offset of the placeholder end delimiter. */ + endOffset: number; + /** The placeholder name. */ + name: string; +} + +/** The hook that replaces the placeholder or does nothing with it. */ +type PlaceholderReplaceHook = (context: PlaceholderReplaceHookContext) => string | null; +/** Iterate over all placeholder values and replace with the result of the hook (or ignore if `null`). */ +const placeholderIterateExecute = (text: string, hook: PlaceholderReplaceHook) => { let placeholderStarted = false; let isEscaped = false; let delimiterStartOffset = -1; let buffer = ""; + let finalText = text; const resetState = () => { placeholderStarted = false; @@ -34,10 +31,10 @@ export const findPlaceholders = (text: string) => { buffer = ""; }; - for (let i = 0; i < text.length; i++) { - const nextCharacter = text[i + 1]; - const currentCharacter = text[i]; - const previousCharacter = text[i - 1]; + for (let i = 0; i < finalText.length; i++) { + const nextCharacter = finalText[i + 1]; + const currentCharacter = finalText[i]; + const previousCharacter = finalText[i - 1]; // Reset state when nested delimiters exist if (placeholderStarted && currentCharacter === START_DELIMITER) resetState(); @@ -59,7 +56,16 @@ export const findPlaceholders = (text: string) => { continue; } - placeholderMap.push([buffer, delimiterStartOffset, i]); + const placeholderValue = hook({ startOffset: delimiterStartOffset, endOffset: i, name: buffer }); + + // Replace the placeholder + if (placeholderValue !== null) { + finalText = replaceInString(finalText, placeholderValue, delimiterStartOffset, i + 1); + + // Replacing a placeholder causes the character positions to shift, so we need to fix these + i = delimiterStartOffset + placeholderValue.length; + } + resetState(); continue; } @@ -76,5 +82,48 @@ export const findPlaceholders = (text: string) => { } } + return finalText; +}; + +/** Replace a certain character range inside a string with another string. */ +const replaceInString = (input: string, replaceWith: string, startOffset: number, endOffset: number) => { + return input.substring(0, startOffset) + replaceWith + input.substring(endOffset); +}; + +/** Replace placeholders in a string. */ +export const replacePlaceholders = (text: string, placeholderMapping: Record) => { + const finalText = placeholderIterateExecute(text, ({ name }) => { + if (!(name in placeholderMapping)) return null; + + return placeholderMapping[name]; + }); + + return finalText; +}; + +/** + * Find all placeholders in a string. + * + * @example + * findPlaceholders('Hello [name]!') // [["name", 6, 11]] + * + * @example + * findPlaceholders('Hello [[name]]!') // [] + */ +export const findPlaceholders = (text: string) => { + /** + * The placeholders mapped as the following nested array structure:\ + * `0`: placeholder name\ + * `1`: offset of the placeholder start delimiter\ + * `2`: offset of the placeholder end delimiter + */ + const placeholderMap: [string, number, number][] = []; + + placeholderIterateExecute(text, ({ name, startOffset, endOffset }) => { + placeholderMap.push([name, startOffset, endOffset]); + + return null; + }); + return placeholderMap; }; From 014823f3b7b49fd357377a0ac603d5aea4fde082 Mon Sep 17 00:00:00 2001 From: Gus Libens <27970303+goestav@users.noreply.github.com> Date: Fri, 23 Aug 2024 14:20:44 +0200 Subject: [PATCH 20/32] test: add test for invalid characters inside placeholder name --- src/utils/placeholder.test.ts | 7 +++++-- src/utils/placeholder.ts | 10 +++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/utils/placeholder.test.ts b/src/utils/placeholder.test.ts index 779fec0..95e9fc3 100644 --- a/src/utils/placeholder.test.ts +++ b/src/utils/placeholder.test.ts @@ -30,7 +30,8 @@ describe("Utils - Placeholder", () => { it.each([ ["Hello, [[name]]!", []], ["Hello, [yourName]! I'm [[myName]].", [["yourName", 7, 16]]], - ])('Should ignore escaped placeholders for the string "%s"', (text, expected) => { + ["Hello, [your:name]! I'm [[myName]].", []], + ])('Should ignore invalid placeholders for the string "%s"', (text, expected) => { const actual = findPlaceholders(text); expect(actual).toEqual(expected); @@ -62,7 +63,9 @@ describe("Utils - Placeholder", () => { it.each([ ["Hello, [[name]]!", { name: "you" }, "Hello, [[name]]!"], ["Hello, [yourName]! I'm [[myName]].", { yourName: "you", myName: "me" }, "Hello, you! I'm [[myName]]."], - ])('Should ignore escaped placeholders for the string "%s"', (text, placeholderMap, expected) => { + // biome-ignore format: more readable when inline + ["Hello, [your:name]! I'm [[myName]].", { 'your:name': "you", myName: "me" }, "Hello, [your:name]! I'm [[myName]]."], + ])('Should ignore invalid placeholders for the string "%s"', (text, placeholderMap, expected) => { const actual = replacePlaceholders(text, placeholderMap); expect(actual).toBe(expected); diff --git a/src/utils/placeholder.ts b/src/utils/placeholder.ts index cafefdc..e421780 100644 --- a/src/utils/placeholder.ts +++ b/src/utils/placeholder.ts @@ -4,6 +4,11 @@ const END_DELIMITER = "]"; /** Check if a character is part of the latin alphabet. */ const isLatinLetter = (character: string) => character.toUpperCase() !== character.toLowerCase(); +/** Replace a certain character range inside a string with another string. */ +const replaceInString = (input: string, replaceWith: string, startOffset: number, endOffset: number) => { + return input.substring(0, startOffset) + replaceWith + input.substring(endOffset); +}; + interface PlaceholderReplaceHookContext { /** The offset of the placeholder start delimiter. */ startOffset: number; @@ -85,11 +90,6 @@ const placeholderIterateExecute = (text: string, hook: PlaceholderReplaceHook) = return finalText; }; -/** Replace a certain character range inside a string with another string. */ -const replaceInString = (input: string, replaceWith: string, startOffset: number, endOffset: number) => { - return input.substring(0, startOffset) + replaceWith + input.substring(endOffset); -}; - /** Replace placeholders in a string. */ export const replacePlaceholders = (text: string, placeholderMapping: Record) => { const finalText = placeholderIterateExecute(text, ({ name }) => { From ea5098f7bd2776df5248b81b5df78bfd0aa26aee Mon Sep 17 00:00:00 2001 From: Gus Libens <27970303+goestav@users.noreply.github.com> Date: Fri, 23 Aug 2024 14:38:48 +0200 Subject: [PATCH 21/32] refactor: update input placeholders for text configuration options --- src/commands/config/gateway.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/commands/config/gateway.ts b/src/commands/config/gateway.ts index d70c9e6..270f43a 100644 --- a/src/commands/config/gateway.ts +++ b/src/commands/config/gateway.ts @@ -21,8 +21,7 @@ const manifest = createConfigurationManifest(Config, [ description: "Message title when a user joins.", column: "gatewayJoinTitle", type: "text", - // TODO: implement string placeholders - placeholder: "Welcome ${mention}!", + placeholder: "Welcome [mention]!", }, { name: "Gateway join content", @@ -38,16 +37,14 @@ const manifest = createConfigurationManifest(Config, [ description: "Message title when a user leaves.", column: "gatewayLeaveTitle", type: "text", - // TODO: implement string placeholders - placeholder: "Goodbye ${mention}!", + placeholder: "Goodbye [mention]!", }, { name: "Gateway leave content", description: "Message content when a user leaves.", column: "gatewayLeaveContent", type: "text", - // TODO: implement string placeholders - placeholder: "We are sorry to see you go ${mention}", + placeholder: "We are sorry to see you go [mention]", style: TextInputStyle.Paragraph, }, ]); From 4bfb77f91caae403a76339259501523bc148ba31 Mon Sep 17 00:00:00 2001 From: Gus Libens <27970303+goestav@users.noreply.github.com> Date: Sat, 7 Dec 2024 23:46:10 +0100 Subject: [PATCH 22/32] refactor: configuration command WIP --- .../configuration/configuration-manifest.ts | 4 +- .../configuration/configuration-message.ts | 33 ++-- .../configuration/prompt-user-input.ts | 159 +++++++++++++++--- src/utils/format.ts | 2 + src/utils/index.ts | 1 + 5 files changed, 160 insertions(+), 39 deletions(-) create mode 100644 src/utils/format.ts diff --git a/src/structures/configuration/configuration-manifest.ts b/src/structures/configuration/configuration-manifest.ts index ef0dba7..19b5abe 100644 --- a/src/structures/configuration/configuration-manifest.ts +++ b/src/structures/configuration/configuration-manifest.ts @@ -1,5 +1,5 @@ import type { InferSelectModel, Table as DrizzleTable } from "drizzle-orm"; -import type { Channel, Role, TextInputStyle } from "discord.js"; +import type { Channel, ChannelType, Role, TextInputStyle } from "discord.js"; interface ConfigurationOptionTypeMap { text: string; @@ -79,6 +79,8 @@ export interface ConfigurationChannelOption
With escaped delimitersWith escaped delimitersWith escaped delimiters
{ type: "channel"; placeholder?: string; + /** @default [ChannelType.GuildText] */ + channelTypes?: ChannelType[]; } export interface ConfigurationSelectOption
> diff --git a/src/structures/configuration/configuration-message.ts b/src/structures/configuration/configuration-message.ts index 5bd2d59..d9a11cc 100644 --- a/src/structures/configuration/configuration-message.ts +++ b/src/structures/configuration/configuration-message.ts @@ -16,7 +16,7 @@ import type { ConfigurationOption } from "./configuration-manifest.ts"; import db from "../../db.ts"; import { and, Table as DrizzleTable, SQL } from "drizzle-orm"; import { Time } from "../../utils.ts"; -import { promptNewConfigurationOptionValue } from "./prompt-user-input.ts"; +import { promptNewConfigurationOptionValue, type UpdateValueHookContext } from "./prompt-user-input.ts"; enum InteractionCustomId { MainMenu = "config-main-menu", @@ -137,8 +137,27 @@ export class ConfigurationMessage< let newValue: unknown; + const hook = async ({ interaction, type, value }: UpdateValueHookContext) => { + db.update(manifestOption.table) + .set({ [manifestOption.column as keyof object]: newValue ? newValue : null }) + .where(whereClause) + .run(); + + const messageContent = "Value updated successfully"; + + if (type === "channel") { + await interaction.deferReply(); + return; + } + + await interaction.reply({ + content: messageContent, + ephemeral: true, + }); + }; + try { - newValue = await promptNewConfigurationOptionValue(interaction, manifestOption, currentValue); + await promptNewConfigurationOptionValue(interaction, manifestOption, currentValue, hook); } catch (error) { if (error instanceof DiscordjsTypeError) { if ( @@ -158,16 +177,6 @@ export class ConfigurationMessage< // User did not provide a value, meaning the database value should be set to NULL newValue = null; } - - db.update(manifestOption.table) - .set({ [manifestOption.column as keyof object]: newValue ? newValue : null }) - .where(whereClause) - .run(); - - await interaction.followUp({ - content: "Value updated successfully", - ephemeral: true, - }); } /** Reply to a message or edit the reply if the interaction got replied to or is deferred and keep reply in memory. */ diff --git a/src/structures/configuration/prompt-user-input.ts b/src/structures/configuration/prompt-user-input.ts index bb9d86a..9266a71 100644 --- a/src/structures/configuration/prompt-user-input.ts +++ b/src/structures/configuration/prompt-user-input.ts @@ -1,24 +1,65 @@ import { ActionRowBuilder, + bold, + ButtonBuilder, + ButtonStyle, ModalBuilder, ModalSubmitInteraction, TextInputBuilder, TextInputStyle, + type InteractionReplyOptions, type CollectedInteraction, type MessageComponentInteraction, type ModalActionRowComponentBuilder, + ComponentType, + ButtonInteraction, + ChannelSelectMenuBuilder, + ChannelSelectMenuInteraction, + ChannelType, } from "discord.js"; import type { ConfigurationOption } from "./configuration-manifest.ts"; -import type { Table as DrizzleTable } from "drizzle-orm"; +import { sql, type Table as DrizzleTable } from "drizzle-orm"; import { Time } from "../../utils.ts"; import { getCustomId } from "./utils.ts"; +import { smallText } from "../../utils/index.ts"; -/** Get the user input through a modal. */ -const promptModalValue = async ( +/** The context to provide for the update value hook. */ +export interface UpdateValueHookContext { + /** The last interaction. */ + interaction: CollectedInteraction<"cached" | "raw">; + /** The new value to set. */ + value: unknown; + /** The {@link ConfigurationOption.type|configuration type}. */ + type: ConfigurationOption["type"]; +} + +/** A hook to update a database value. */ +type UpdateValueHook = (context: UpdateValueHookContext) => Promise; + +/** Get the new value for a configuration option. */ +export const promptNewConfigurationOptionValue = async ( + interaction: MessageComponentInteraction, + manifestOption: ConfigurationOption, + value: unknown, + updateValueHook: UpdateValueHook, +) => { + if (manifestOption.type === "text") { + const modalInputCustomId = getCustomId(manifestOption, "modal-input"); + await promptTextValue(interaction, manifestOption, modalInputCustomId, value as string | null, updateValueHook); + } else if (manifestOption.type === "channel") { + await promptChannelValue(interaction, manifestOption, value as string | null, updateValueHook); + } else if (manifestOption.type === "boolean") { + await promptBooleanValue(interaction, manifestOption, value as boolean | null, updateValueHook); + } +}; + +/** Get the user input for a text option. */ +const promptTextValue = async ( interaction: MessageComponentInteraction, manifestOption: ConfigurationOption & { type: "text" }, modalInputCustomId: string, value: string | null, + updateValueHook: UpdateValueHook, ) => { const modalCustomId = getCustomId(manifestOption, "modal"); @@ -42,33 +83,99 @@ const promptModalValue = async ( time: Time.Minute * 2, })) as ModalSubmitInteraction<"cached" | "raw">; - return [modalSubmitInteraction, modalSubmitInteraction.fields.getTextInputValue(modalInputCustomId)] as const; + await updateValueHook({ + interaction: modalSubmitInteraction, + value: modalSubmitInteraction.fields.getTextInputValue(modalInputCustomId), + type: manifestOption.type, + }); }; -/** Get the new value for a configuration option. */ -export const promptNewConfigurationOptionValue = async ( +/** Get the user input for a channel option. */ +const promptChannelValue = async ( interaction: MessageComponentInteraction, - manifestOption: ConfigurationOption, - value: unknown, -): Promise => { - let updatedValue: unknown; - let followUpInteraction!: CollectedInteraction<"cached" | "raw">; + manifestOption: ConfigurationOption & { type: "channel" }, + value: string | null, + updateValueHook: UpdateValueHook, +) => { + const selectMenuCustomId = getCustomId(manifestOption); + const formattedDescription = manifestOption.description + .split("\n") + .map((text) => smallText(text)) + .join("\n"); + const selectMenu = new ChannelSelectMenuBuilder() + .setCustomId(selectMenuCustomId) + .setPlaceholder(manifestOption.placeholder ?? "Select a channel") + .setChannelTypes(manifestOption.channelTypes ?? [ChannelType.GuildText]) + .setMinValues(0) + .setMaxValues(1); - if (manifestOption.type === "text") { - const modalInputCustomId = getCustomId(manifestOption, "modal-input"); - const [modalSubmitInteraction, newValue] = await promptModalValue( - interaction, - manifestOption, - modalInputCustomId, - value as string | null, - ); - - followUpInteraction = modalSubmitInteraction; - updatedValue = newValue; - } + if (value) selectMenu.setDefaultChannels(value); + + const messageOptions = { + content: `${bold(manifestOption.name)}\n${formattedDescription}`, + components: [new ActionRowBuilder().addComponents(selectMenu)], + ephemeral: true, + fetchReply: true, + } as const satisfies InteractionReplyOptions; + + const followUpMessage = await interaction.reply(messageOptions); + + const collector = followUpMessage.createMessageComponentCollector({ + componentType: ComponentType.ChannelSelect, + filter: (interaction) => { + if (!interaction.inGuild()) return false; + + return interaction.customId === selectMenuCustomId; + }, + time: Time.Minute * 2, + }); + + collector.on("collect", async (interaction: ChannelSelectMenuInteraction<"cached" | "raw">) => { + await updateValueHook({ interaction, value: interaction.values.at(0) ?? null, type: manifestOption.type }); + }); +}; + +/** Get the user input for a boolean option. */ +const promptBooleanValue = async ( + interaction: MessageComponentInteraction, + manifestOption: ConfigurationOption & { type: "boolean" }, + value: boolean | null, + updateValueHook: UpdateValueHook, +) => { + const buttonCustomId = getCustomId(manifestOption); + const formattedDescription = manifestOption.description + .split("\n") + .map((text) => smallText(text)) + .join("\n"); + const button = new ButtonBuilder() + .setCustomId(buttonCustomId) + .setLabel(manifestOption.label ?? (value ? "Disable" : "Enable")) + .setStyle(value ? ButtonStyle.Danger : ButtonStyle.Success); + + if (manifestOption.emoji) button.setEmoji(manifestOption.emoji); - // Silently acknowledge user input interaction - await followUpInteraction.deferUpdate(); + const messageOptions: InteractionReplyOptions = { + content: ` + ${bold(manifestOption.name)} + ${formattedDescription} + `, + components: [new ActionRowBuilder().addComponents(button)], + ephemeral: true, + }; + + const followUpInteraction = await interaction.reply(messageOptions); + + const collector = followUpInteraction.createMessageComponentCollector({ + componentType: ComponentType.Button, + filter: (interaction) => { + if (!interaction.inGuild()) return false; + + return interaction.customId === buttonCustomId; + }, + time: Time.Minute * 2, + }); - return updatedValue; + collector.on("collect", async (interaction: ButtonInteraction<"cached" | "raw">) => { + await updateValueHook({ interaction, value: sql`NOT ${manifestOption.column}`, type: manifestOption.type }); + }); }; diff --git a/src/utils/format.ts b/src/utils/format.ts new file mode 100644 index 0000000..7020ff5 --- /dev/null +++ b/src/utils/format.ts @@ -0,0 +1,2 @@ +/** Format text as small text. */ +export const smallText = (text: T): `-# ${T}` => `-# ${text}`; diff --git a/src/utils/index.ts b/src/utils/index.ts index bc458b5..58a8b0c 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -2,3 +2,4 @@ export * from "./drizzle.ts"; export * from "./common.ts"; export * from "./discordjs.ts"; export * from "./error.ts"; +export * from "./format.ts"; From fa46e2650c60afcdf1c9b0ea7e68703ccae841f3 Mon Sep 17 00:00:00 2001 From: Gus Libens <27970303+goestav@users.noreply.github.com> Date: Sat, 7 Dec 2024 23:50:21 +0100 Subject: [PATCH 23/32] build: bump better-sqlite3 dependency --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ba73103..b02f591 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "devDependencies": { "@biomejs/biome": "^1.4.1", - "better-sqlite3": "^9.2.2", + "better-sqlite3": "^11.6.0", "bun-types": "^1.0.20", "drizzle-kit": "^0.20.9", "vitest": "^2.0.5" From 94b082b05836725834a87fccff47f379a830ae76 Mon Sep 17 00:00:00 2001 From: Gus Libens <27970303+goestav@users.noreply.github.com> Date: Sat, 7 Dec 2024 23:53:18 +0100 Subject: [PATCH 24/32] build: bump discord.js dependency --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b02f591..0833952 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "dependencies": { "bufferutil": "^4.0.8", "cheerio": "^1.0.0-rc.12", - "discord.js": "^14.14.1", + "discord.js": "^14.16.3", "djs-fsrouter": "^0.0.12", "drizzle-orm": "^0.29.2", "entities-decode": "^2.0.0", From 4b07c49d45779c809014cf7a24ce6943ebe87592 Mon Sep 17 00:00:00 2001 From: Gus Libens <27970303+goestav@users.noreply.github.com> Date: Sat, 7 Dec 2024 23:54:40 +0100 Subject: [PATCH 25/32] refactor: favor `subtext` from discord.js over custom `smallText` utility function --- src/structures/configuration/prompt-user-input.ts | 6 +++--- src/utils/format.ts | 2 -- 2 files changed, 3 insertions(+), 5 deletions(-) delete mode 100644 src/utils/format.ts diff --git a/src/structures/configuration/prompt-user-input.ts b/src/structures/configuration/prompt-user-input.ts index 9266a71..4e70dcb 100644 --- a/src/structures/configuration/prompt-user-input.ts +++ b/src/structures/configuration/prompt-user-input.ts @@ -16,12 +16,12 @@ import { ChannelSelectMenuBuilder, ChannelSelectMenuInteraction, ChannelType, + subtext, } from "discord.js"; import type { ConfigurationOption } from "./configuration-manifest.ts"; import { sql, type Table as DrizzleTable } from "drizzle-orm"; import { Time } from "../../utils.ts"; import { getCustomId } from "./utils.ts"; -import { smallText } from "../../utils/index.ts"; /** The context to provide for the update value hook. */ export interface UpdateValueHookContext { @@ -100,7 +100,7 @@ const promptChannelValue = async ( const selectMenuCustomId = getCustomId(manifestOption); const formattedDescription = manifestOption.description .split("\n") - .map((text) => smallText(text)) + .map((text) => subtext(text)) .join("\n"); const selectMenu = new ChannelSelectMenuBuilder() .setCustomId(selectMenuCustomId) @@ -145,7 +145,7 @@ const promptBooleanValue = async ( const buttonCustomId = getCustomId(manifestOption); const formattedDescription = manifestOption.description .split("\n") - .map((text) => smallText(text)) + .map((text) => subtext(text)) .join("\n"); const button = new ButtonBuilder() .setCustomId(buttonCustomId) diff --git a/src/utils/format.ts b/src/utils/format.ts deleted file mode 100644 index 7020ff5..0000000 --- a/src/utils/format.ts +++ /dev/null @@ -1,2 +0,0 @@ -/** Format text as small text. */ -export const smallText = (text: T): `-# ${T}` => `-# ${text}`; From 747049351e9b09bc87c596e781602d194007fc12 Mon Sep 17 00:00:00 2001 From: Gus Libens <27970303+goestav@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:16:13 +0100 Subject: [PATCH 26/32] refactor: track bun lockfile for version control --- .gitignore | 1 - bun.lockb | Bin 0 -> 372676 bytes 2 files changed, 1 deletion(-) create mode 100755 bun.lockb diff --git a/.gitignore b/.gitignore index 062cec5..7e4d985 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ .DS_Store node_modules -bun.lockb .env.* !.env.example diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..1427b969aca7aa877888879f4272da2d14adb130 GIT binary patch literal 372676 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 OfB*e|1q}b+{QN)cJz#(U literal 0 HcmV?d00001 From 5b21c17e82dc2f6e4a4e54bbf26fe42c21ca8de9 Mon Sep 17 00:00:00 2001 From: Gus Libens <27970303+goestav@users.noreply.github.com> Date: Mon, 6 Jan 2025 16:19:27 +0100 Subject: [PATCH 27/32] refactor: fix configuration set logic + improve interaction reply messages --- bun.lockb | Bin 372676 -> 372676 bytes src/db.ts | 2 +- .../configuration/configuration-message.ts | 30 ++++++++---------- src/types/env.ts | 2 +- src/utils/index.ts | 1 - 5 files changed, 16 insertions(+), 19 deletions(-) diff --git a/bun.lockb b/bun.lockb index 1427b969aca7aa877888879f4272da2d14adb130..e70ef2c1ab0575181b4f9549a4aab491488bf146 100755 GIT binary patch delta 1589 zcmXZcdu)?c7{~GV?X#A3u;GN(Y-6%HuG{FYAt6CY_(M09kZAlz-BgH{Ah-;bChZBkKOORlY&NY9aj8VzW*Wa8x-F>b>)`q^k8({oHi zJG3?GIDV(&9`qQm*>N+dN1$zq&1O>nJC-=kQq4U74td2Fy=1ekcHC3+e?bXaZB|FU z1A6{X$@n<+*lDxv1bQD&L-*BdYP0)3L;V2SjLn{1@-fqd5b#5b3;w^udq#b z-Gm!x^78gbfrJwLnQ`S2 zhjG;W-X$gTbZV}KPDh=7%b`AoJ9;hh$De+toJ)*&f664qXq9_Gs?KW za?C*mBADnmhr8v)=qa}UjA2~FF}D4PZ`Fby*? z3k4{~3=D9L4XEVURbc4_#@1T3NylVCD2r|5F#*}gL8EsoCIxQr6UMiq-2~%O;FZVa z54Tj;%dhCbE_7ly)?qz#u8*Ql%o9n;_g+p)rLQo-43D7~N$f)p`VcpXeX_0iGCOr6 z2A$t~@HyUr%LEm(vQMUFrzi)I#-RD2Pqv27Ga`k4bYTNBT@&e-g@tMA{Wyqs(aLU} z=)f-Xalb5%uH%;1ViPvvBW%Hk`pYA%1`o{%;$dVV7k*5_c(|B=045`hY=n>lAI4!K z1P>t-83D(84`|hrQf>O(>P!x+dL_tA6Xlk0;T0v_}G=69synN7tm)cqqtxdYd zU`!ONEeQvTViR=P1z`a%)2h9wL`}R$jT&RLNsHDd)?3mv3iSWtoa}GsJX@YG+_GLe?DV`O1ouV_L7W`3F1>P>mhLH_rvF2*6U>nWEDDD z8;j|@L_B4luI=zOgl!V?U(E}ep*qo?sFwVZh2+`_h;)7mrf5K$O5k%-tyrhzU;4=? z(Oub&!V73y>}8{=U-l)&Eb+22^sAujF7>jp)HTqyj7;1>tt|0{uhY-QPH*09>RxEu zZrol~Ko_?X;Jzq+Y^e!;K! z4JU9Cr_gDCZ;^vk8{jNE+$~2u}AmIN8x*n4dH;pkd6$zflLg?2xMU--b6M=VKl~IEXJXmb$ZZ? zLi(pU#u=Q&WwMKC!$vG7OJdtUV1}Ck6v;@zBnBp<0Qy``v9%F-+kG67p^o#&rbZ-cbJ7icHg;nV>aiDkf4__OFbnTvHj3O2o21Z}8S!&95k(A5 zXvP7w*w_K7O8Syz9Bxf { const message = await this.getReplyMessage(); - const isReplyAuthor = message.interaction?.user.id === interaction.user.id; + const isReplyAuthor = message.interactionMetadata?.user.id === interaction.user.id; const isReplyMessage = interaction.message.id === message.id; if (!Object.keys(manifestOptionMap).includes(interaction.values[0])) return false; @@ -116,6 +117,15 @@ export class ConfigurationMessage< return this.#reply instanceof Message ? this.#reply : await this.#reply.fetch(); } + private getPostHookMessageContent(type: string, value: unknown) { + switch (type) { + case "channel": + return value ? `Channel set to ${channelMention(value as string)}` : "Channel value removed"; + default: + return "Value updated successfully"; + } + } + private async handleInteractionCollect( interaction: MessageComponentInteraction<"cached" | "raw">, manifestOption: ConfigurationOption
, @@ -135,23 +145,14 @@ export class ConfigurationMessage< // TEMP: use .all() and select the first row manually, .get() does not work .at(0) as { value: unknown }; - let newValue: unknown; - const hook = async ({ interaction, type, value }: UpdateValueHookContext) => { db.update(manifestOption.table) - .set({ [manifestOption.column as keyof object]: newValue ? newValue : null }) + .set({ [manifestOption.column as keyof object]: value ? value : null }) .where(whereClause) .run(); - const messageContent = "Value updated successfully"; - - if (type === "channel") { - await interaction.deferReply(); - return; - } - await interaction.reply({ - content: messageContent, + content: this.getPostHookMessageContent(type, value), ephemeral: true, }); }; @@ -173,9 +174,6 @@ export class ConfigurationMessage< !(error instanceof DiscordjsTypeError && error.code === DiscordjsErrorCodes.ModalSubmitInteractionFieldNotFound) ) throw error; - - // User did not provide a value, meaning the database value should be set to NULL - newValue = null; } } @@ -225,7 +223,7 @@ export class ConfigurationMessage< } return { - content: "Select which configuration option you want to view/edit:", + content: "What option would you like to edit?", components: [new ActionRowBuilder().addComponents(selectMenu)], ephemeral: true, }; diff --git a/src/types/env.ts b/src/types/env.ts index 8bb81df..3e29901 100644 --- a/src/types/env.ts +++ b/src/types/env.ts @@ -17,7 +17,7 @@ declare module "bun" { CSE_KEY: string; CSE_CSX: string; SCRAPE_CACHE?: number; - ORM_DEBUG?: boolean; + ORM_DEBUG?: string; } } export default env; diff --git a/src/utils/index.ts b/src/utils/index.ts index 58a8b0c..bc458b5 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -2,4 +2,3 @@ export * from "./drizzle.ts"; export * from "./common.ts"; export * from "./discordjs.ts"; export * from "./error.ts"; -export * from "./format.ts"; From 6ca060030d8235c759adbf74dda9a8ecd575557d Mon Sep 17 00:00:00 2001 From: Gus Libens <27970303+goestav@users.noreply.github.com> Date: Mon, 6 Jan 2025 16:32:00 +0100 Subject: [PATCH 28/32] build: add common-tags dependency --- bun.lockb | Bin 372676 -> 373404 bytes package.json | 2 ++ 2 files changed, 2 insertions(+) diff --git a/bun.lockb b/bun.lockb index e70ef2c1ab0575181b4f9549a4aab491488bf146..d44537f74fb4514eb09b6f6c74fa2b3ed574ffd1 100755 GIT binary patch delta 57751 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 ThANf#(`fjs`V~2Cb=dWPK0_c3 delta 57024 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 og4E8II{IguW!2p9EXyx7)!egqtyErFmUMcpMFX Date: Mon, 6 Jan 2025 18:19:44 +0100 Subject: [PATCH 29/32] refactor: handle role configuration options --- .../configuration/prompt-user-input.ts | 76 +++++++++++++++++-- 1 file changed, 68 insertions(+), 8 deletions(-) diff --git a/src/structures/configuration/prompt-user-input.ts b/src/structures/configuration/prompt-user-input.ts index 4e70dcb..b44d4ec 100644 --- a/src/structures/configuration/prompt-user-input.ts +++ b/src/structures/configuration/prompt-user-input.ts @@ -17,11 +17,14 @@ import { ChannelSelectMenuInteraction, ChannelType, subtext, + RoleSelectMenuBuilder, + RoleSelectMenuInteraction, } from "discord.js"; import type { ConfigurationOption } from "./configuration-manifest.ts"; import { sql, type Table as DrizzleTable } from "drizzle-orm"; import { Time } from "../../utils.ts"; import { getCustomId } from "./utils.ts"; +import { stripIndents } from "common-tags"; /** The context to provide for the update value hook. */ export interface UpdateValueHookContext { @@ -43,13 +46,23 @@ export const promptNewConfigurationOptionValue = async ( value: unknown, updateValueHook: UpdateValueHook, ) => { - if (manifestOption.type === "text") { - const modalInputCustomId = getCustomId(manifestOption, "modal-input"); - await promptTextValue(interaction, manifestOption, modalInputCustomId, value as string | null, updateValueHook); - } else if (manifestOption.type === "channel") { - await promptChannelValue(interaction, manifestOption, value as string | null, updateValueHook); - } else if (manifestOption.type === "boolean") { - await promptBooleanValue(interaction, manifestOption, value as boolean | null, updateValueHook); + switch (manifestOption.type) { + case "text": { + const modalInputCustomId = getCustomId(manifestOption, "modal-input"); + await promptTextValue(interaction, manifestOption, modalInputCustomId, value as string | null, updateValueHook); + break; + } + case "channel": + await promptChannelValue(interaction, manifestOption, value as string | null, updateValueHook); + break; + case "boolean": + await promptBooleanValue(interaction, manifestOption, value as boolean | null, updateValueHook); + break; + case "role": + await promptRoleValue(interaction, manifestOption, value as string | null, updateValueHook); + break; + case "select": + throw new Error("Not yet implemented"); } }; @@ -155,7 +168,7 @@ const promptBooleanValue = async ( if (manifestOption.emoji) button.setEmoji(manifestOption.emoji); const messageOptions: InteractionReplyOptions = { - content: ` + content: stripIndents` ${bold(manifestOption.name)} ${formattedDescription} `, @@ -179,3 +192,50 @@ const promptBooleanValue = async ( await updateValueHook({ interaction, value: sql`NOT ${manifestOption.column}`, type: manifestOption.type }); }); }; + +/** Get the user input for a role option. */ +const promptRoleValue = async ( + interaction: MessageComponentInteraction, + manifestOption: ConfigurationOption & { type: "role" }, + value: string | null, + updateValueHook: UpdateValueHook, +) => { + const selectMenuCustomId = getCustomId(manifestOption); + const formattedDescription = manifestOption.description + .split("\n") + .map((text) => subtext(text)) + .join("\n"); + const selectMenu = new RoleSelectMenuBuilder() + .setCustomId(selectMenuCustomId) + .setPlaceholder(manifestOption.placeholder ?? "Select a channel") + .setMinValues(0) + .setMaxValues(1); + + if (value) selectMenu.setDefaultRoles(value); + + const messageOptions = { + content: stripIndents` + ${bold(manifestOption.name)} + ${formattedDescription} + `, + components: [new ActionRowBuilder().addComponents(selectMenu)], + ephemeral: true, + fetchReply: true, + } as const satisfies InteractionReplyOptions; + + const followUpInteraction = await interaction.reply(messageOptions); + + const collector = followUpInteraction.createMessageComponentCollector({ + componentType: ComponentType.RoleSelect, + filter: (interaction) => { + if (!interaction.inGuild()) return false; + + return interaction.customId === selectMenuCustomId; + }, + time: Time.Minute * 2, + }); + + collector.on("collect", async (interaction: RoleSelectMenuInteraction<"cached" | "raw">) => { + await updateValueHook({ interaction, value: interaction.values.at(0), type: manifestOption.type }); + }); +}; From 47993950620c3b75b5b1e1f7d77607e32ea12ab5 Mon Sep 17 00:00:00 2001 From: Gus Libens <27970303+goestav@users.noreply.github.com> Date: Mon, 6 Jan 2025 18:23:39 +0100 Subject: [PATCH 30/32] refactor: improve interaction response message for value reset --- src/structures/configuration/configuration-message.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structures/configuration/configuration-message.ts b/src/structures/configuration/configuration-message.ts index 76cdf66..d9a5884 100644 --- a/src/structures/configuration/configuration-message.ts +++ b/src/structures/configuration/configuration-message.ts @@ -122,7 +122,7 @@ export class ConfigurationMessage< case "channel": return value ? `Channel set to ${channelMention(value as string)}` : "Channel value removed"; default: - return "Value updated successfully"; + return value ? "Value updated successfully" : "Successfully reset value"; } } From 7f4480c81e18f0943bf4fb4f8fd86f553593a063 Mon Sep 17 00:00:00 2001 From: Gus Libens <27970303+goestav@users.noreply.github.com> Date: Mon, 6 Jan 2025 18:34:38 +0100 Subject: [PATCH 31/32] refactor: gracefully handle collector timeout --- src/structures/configuration/configuration-message.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/structures/configuration/configuration-message.ts b/src/structures/configuration/configuration-message.ts index d9a5884..41f5c0d 100644 --- a/src/structures/configuration/configuration-message.ts +++ b/src/structures/configuration/configuration-message.ts @@ -108,6 +108,12 @@ export class ConfigurationMessage< await this.handleInteractionCollect(interaction, manifestOptionMap[interaction.values[0]]); }); + + collector.on("end", (_, reason) => { + if (reason === "time") return; + + console.error(`Collector stopped for reason: ${reason}`); + }); } /** Get the {@link Message} for the configuration message reply. */ From 2b182bef0627d16638e1134056087bb3770f2636 Mon Sep 17 00:00:00 2001 From: Gus Libens <27970303+goestav@users.noreply.github.com> Date: Sat, 11 Jan 2025 12:27:45 +0100 Subject: [PATCH 32/32] refactor: favor bun's built-in test runner over vitest --- bun.lockb | Bin 373404 -> 339116 bytes package.json | 3 +-- src/utils/placeholder.test.ts | 26 ++++++++++++++++---------- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/bun.lockb b/bun.lockb index d44537f74fb4514eb09b6f6c74fa2b3ed574ffd1..9bf89fe86f088a5e1e786aec36f7d96b87316a0a 100755 GIT binary patch delta 49615 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⪙8bQg^f9=1#L&4i#7vZJ3{g^f~#eN!3vq03* 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 delta 69654 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 diff --git a/package.json b/package.json index a77c42e..2109ee9 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,7 @@ "@types/common-tags": "^1.8.4", "better-sqlite3": "^11.6.0", "bun-types": "^1.0.20", - "drizzle-kit": "^0.20.9", - "vitest": "^2.0.5" + "drizzle-kit": "^0.20.9" }, "author": "Jacob Jackson", "license": "GPL-3.0-or-later", diff --git a/src/utils/placeholder.test.ts b/src/utils/placeholder.test.ts index 95e9fc3..ee4b93b 100644 --- a/src/utils/placeholder.test.ts +++ b/src/utils/placeholder.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, it, expect } from "bun:test"; import { findPlaceholders, replacePlaceholders } from "./placeholder.ts"; describe("Utils - Placeholder", () => { @@ -9,11 +9,14 @@ describe("Utils - Placeholder", () => { ["Hello, [yourName]! I'm [myName].", [["yourName", 7, 16], ["myName", 23, 30]]], // biome-ignore format: more readable when inline ["Hello, [[yourName]! I'm [myName]].",[["yourName", 8, 17],["myName", 24, 31]]], - ])('Should find the placeholders for the string "%s"', (text, expected) => { - const actual = findPlaceholders(text); + ] as [string, [string, number, number][]][])( + 'Should find the placeholders for the string "%s"', + (text, expected) => { + const actual = findPlaceholders(text); - expect(actual).toEqual(expected); - }); + expect(actual).toEqual(expected); + }, + ); it("Should not find any placeholders when none are present", () => { const actual = findPlaceholders("Hello you!"); @@ -31,11 +34,14 @@ describe("Utils - Placeholder", () => { ["Hello, [[name]]!", []], ["Hello, [yourName]! I'm [[myName]].", [["yourName", 7, 16]]], ["Hello, [your:name]! I'm [[myName]].", []], - ])('Should ignore invalid placeholders for the string "%s"', (text, expected) => { - const actual = findPlaceholders(text); - - expect(actual).toEqual(expected); - }); + ] as [string, [string, number, number][]][])( + 'Should ignore invalid placeholders for the string "%s"', + (text, expected) => { + const actual = findPlaceholders(text); + + expect(actual).toEqual(expected); + }, + ); }); describe(`${replacePlaceholders.name}()`, () => {