diff --git a/backend/src/plugins/RoleButtons/RoleButtonsPlugin.ts b/backend/src/plugins/RoleButtons/RoleButtonsPlugin.ts index 60f5a221..aa76140a 100644 --- a/backend/src/plugins/RoleButtons/RoleButtonsPlugin.ts +++ b/backend/src/plugins/RoleButtons/RoleButtonsPlugin.ts @@ -8,6 +8,8 @@ import { RoleManagerPlugin } from "../RoleManager/RoleManagerPlugin"; import { StrictValidationError } from "../../validatorUtils"; import { onButtonInteraction } from "./events/buttonInteraction"; import { pluginInfo } from "./info"; +import { createButtonComponents } from "./functions/createButtonComponents"; +import { TooManyComponentsError } from "./functions/TooManyComponentsError"; export const RoleButtonsPlugin = zeppelinGuildPlugin()({ name: "role_buttons", @@ -26,10 +28,6 @@ export const RoleButtonsPlugin = zeppelinGuildPlugin()({ if (buttonsConfig) { buttonsConfig.name = name; - // 5 action rows * 5 buttons - if (buttonsConfig.options?.length > 25) { - throw new StrictValidationError(["A single message can have at most 25 role buttons"]); - } if (buttonsConfig.message) { if ("message_id" in buttonsConfig.message) { @@ -39,6 +37,17 @@ export const RoleButtonsPlugin = zeppelinGuildPlugin()({ seenMessages.add(buttonsConfig.message.message_id); } } + + if (buttonsConfig.options) { + try { + createButtonComponents(buttonsConfig); + } catch (err) { + if (err instanceof TooManyComponentsError) { + throw new StrictValidationError(["Too many options; can only have max 5 buttons per row on max 5 rows."]); + } + throw new StrictValidationError(["Error validating options"]); + } + } } } diff --git a/backend/src/plugins/RoleButtons/functions/TooManyComponentsError.ts b/backend/src/plugins/RoleButtons/functions/TooManyComponentsError.ts new file mode 100644 index 00000000..b79be8bb --- /dev/null +++ b/backend/src/plugins/RoleButtons/functions/TooManyComponentsError.ts @@ -0,0 +1 @@ +export class TooManyComponentsError extends Error {} diff --git a/backend/src/plugins/RoleButtons/functions/applyRoleButtons.ts b/backend/src/plugins/RoleButtons/functions/applyRoleButtons.ts index 18c8beb5..b171bc2b 100644 --- a/backend/src/plugins/RoleButtons/functions/applyRoleButtons.ts +++ b/backend/src/plugins/RoleButtons/functions/applyRoleButtons.ts @@ -4,8 +4,8 @@ import { isSnowflake, snowflakeRegex } from "../../../utils"; import { LogsPlugin } from "../../Logs/LogsPlugin"; import { Message, MessageButton, MessageEditOptions, MessageOptions, Snowflake } from "discord.js"; import { RoleButtonsItem } from "../../../data/entities/RoleButtonsItem"; -import { splitButtonsIntoRows } from "./splitButtonsIntoRows"; import { buildCustomId } from "../../../utils/buildCustomId"; +import { createButtonComponents } from "./createButtonComponents"; const channelMessageRegex = new RegExp(`^(${snowflakeRegex.source})-(${snowflakeRegex.source})$`); @@ -104,24 +104,8 @@ export async function applyRoleButtons( } // Apply role buttons - const buttons = configItem.options.map((opt, index) => { - const button = new MessageButton() - .setLabel(opt.label ?? "") - .setStyle(opt.style ?? "PRIMARY") - .setCustomId(buildCustomId("roleButtons", { name: configItem.name, index })); - - if (opt.emoji) { - const emo = pluginData.client.emojis.resolve(opt.emoji as Snowflake) ?? opt.emoji; - button.setEmoji(emo); - } - - return button; - }); - const rows = splitButtonsIntoRows(buttons); - - await message.edit({ - components: rows, - }); + const components = createButtonComponents(configItem); + await message.edit({ components }); return { channel_id: message.channelId, diff --git a/backend/src/plugins/RoleButtons/functions/createButtonComponents.ts b/backend/src/plugins/RoleButtons/functions/createButtonComponents.ts new file mode 100644 index 00000000..05736484 --- /dev/null +++ b/backend/src/plugins/RoleButtons/functions/createButtonComponents.ts @@ -0,0 +1,39 @@ +import { MessageActionRow, MessageButton, Snowflake } from "discord.js"; +import { chunkArray } from "../../../utils"; +import { RoleButtonsPluginType, TRoleButtonOption, TRoleButtonsConfigItem } from "../types"; +import { buildCustomId } from "../../../utils/buildCustomId"; +import { GuildPluginData } from "knub"; +import { TooManyComponentsError } from "./TooManyComponentsError"; + +export function createButtonComponents(configItem: TRoleButtonsConfigItem): MessageActionRow[] { + const rows: MessageActionRow[] = []; + + let currentRow = new MessageActionRow(); + for (const [index, option] of configItem.options.entries()) { + if (currentRow.components.length === 5 || (currentRow.components.length > 0 && option.start_new_row)) { + rows.push(currentRow); + currentRow = new MessageActionRow(); + } + + const button = new MessageButton() + .setLabel(option.label ?? "") + .setStyle(option.style ?? "PRIMARY") + .setCustomId(buildCustomId("roleButtons", { name: configItem.name, index })); + + if (option.emoji) { + button.setEmoji(option.emoji); + } + + currentRow.components.push(button); + } + + if (currentRow.components.length > 0) { + rows.push(currentRow); + } + + if (rows.length > 5) { + throw new TooManyComponentsError(); + } + + return rows; +} diff --git a/backend/src/plugins/RoleButtons/functions/splitButtonsIntoRows.ts b/backend/src/plugins/RoleButtons/functions/splitButtonsIntoRows.ts deleted file mode 100644 index 118f20ce..00000000 --- a/backend/src/plugins/RoleButtons/functions/splitButtonsIntoRows.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { MessageActionRow, MessageButton } from "discord.js"; -import { chunkArray } from "../../../utils"; - -export function splitButtonsIntoRows(buttons: MessageButton[]): MessageActionRow[] { - // Max 5 buttons per row - const buttonChunks = chunkArray(buttons, 5); - return buttonChunks.map((chunk) => { - const row = new MessageActionRow(); - row.setComponents(chunk); - return row; - }); -} diff --git a/backend/src/plugins/RoleButtons/types.ts b/backend/src/plugins/RoleButtons/types.ts index 0dbbcd07..2b23b692 100644 --- a/backend/src/plugins/RoleButtons/types.ts +++ b/backend/src/plugins/RoleButtons/types.ts @@ -16,6 +16,7 @@ const RoleButtonOption = t.type({ label: tNullable(t.string), emoji: tNullable(t.string), style: tNullable(t.keyof(ButtonStyles)), // https://discord.js.org/#/docs/discord.js/v13/typedef/MessageButtonStyle + start_new_row: tNullable(t.boolean), }); export type TRoleButtonOption = t.TypeOf;