From 6381a3d7c2a8ec97b9dbeeaa964a7bc0c650b2f7 Mon Sep 17 00:00:00 2001 From: CarelessInternet <59174259+CarelessInternet@users.noreply.github.com> Date: Mon, 2 Sep 2024 21:42:38 +0200 Subject: [PATCH] feat(thread-ticketing): add toggle author actions (#394) --- apps/bot/package.json | 2 +- .../staff/configuration-ticket-threads.ts | 214 +++++-- .../src/commands/thread-ticketing/ticket.ts | 38 +- apps/bot/src/events/guild/MessageCreate.ts | 2 +- apps/bot/src/events/guild/ThreadCreate.ts | 2 +- apps/bot/src/i18n/en-GB/threads.ts | 8 +- apps/bot/src/i18n/en-GB/userForums.ts | 2 +- apps/bot/src/i18n/i18n-types.ts | 36 +- apps/bot/src/i18n/sv-SE/threads.ts | 6 +- apps/bot/src/i18n/sv-SE/userForums.ts | 2 +- .../src/utils/thread-ticketing/closeTicket.ts | 44 +- .../utils/thread-ticketing/createTicket.ts | 10 +- .../utils/thread-ticketing/deleteTicket.ts | 43 +- apps/bot/src/utils/thread-ticketing/index.ts | 1 + .../src/utils/thread-ticketing/lockTicket.ts | 45 +- .../thread-ticketing/renameTitleModal.ts | 2 +- .../utils/thread-ticketing/ticketActions.ts | 46 ++ apps/bot/src/utils/user-forums/closeTicket.ts | 2 +- .../bot/src/utils/user-forums/deleteTicket.ts | 2 +- apps/bot/src/utils/user-forums/lockTicket.ts | 2 +- apps/bot/src/utils/user-forums/renameTitle.ts | 2 +- .../src/utils/user-forums/renameTitleModal.ts | 2 +- apps/website/package.json | 6 +- apps/website/src/app/layout.tsx | 1 + .../migrations/0004_outgoing_titania.sql | 1 + .../migrations/meta/0004_snapshot.json | 523 ++++++++++++++++++ .../database/migrations/meta/_journal.json | 7 + packages/database/src/index.ts | 1 + packages/database/src/schema.ts | 5 +- .../ThreadTicketActionsPermissionBitField.ts | 44 ++ .../database/src/{utils.ts => utils/index.ts} | 11 +- packages/djs-framework/package.json | 2 +- packages/env/package.json | 2 +- packages/eslint-config/package.json | 2 +- pnpm-lock.yaml | 192 +++---- 35 files changed, 1080 insertions(+), 230 deletions(-) create mode 100644 apps/bot/src/utils/thread-ticketing/ticketActions.ts create mode 100644 packages/database/migrations/0004_outgoing_titania.sql create mode 100644 packages/database/migrations/meta/0004_snapshot.json create mode 100644 packages/database/src/utils/ThreadTicketActionsPermissionBitField.ts rename packages/database/src/{utils.ts => utils/index.ts} (88%) diff --git a/apps/bot/package.json b/apps/bot/package.json index 375de35..52cc597 100644 --- a/apps/bot/package.json +++ b/apps/bot/package.json @@ -25,6 +25,6 @@ "devDependencies": { "@eslint/eslintrc": "^3.1.0", "@ticketer/eslint-config": "workspace:*", - "@types/node": "^22.5.1" + "@types/node": "^22.5.2" } } diff --git a/apps/bot/src/commands/staff/configuration-ticket-threads.ts b/apps/bot/src/commands/staff/configuration-ticket-threads.ts index 21bec03..c7b1dd3 100644 --- a/apps/bot/src/commands/staff/configuration-ticket-threads.ts +++ b/apps/bot/src/commands/staff/configuration-ticket-threads.ts @@ -25,16 +25,7 @@ import { Modal, } from '@ticketer/djs-framework'; import { - ThreadTicketing, - extractEmoji, - goToPage, - messageWithPagination, - ticketThreadsOpeningMessageDescription, - ticketThreadsOpeningMessageTitle, - withPagination, - zodErrorToString, -} from '@/utils'; -import { + ThreadTicketActionsPermissionBitField, and, asc, count, @@ -49,6 +40,16 @@ import { ticketThreadsConfigurationsInsertSchema, ticketsThreads, } from '@ticketer/database'; +import { + ThreadTicketing, + extractEmoji, + goToPage, + messageWithPagination, + ticketThreadsOpeningMessageDescription, + ticketThreadsOpeningMessageTitle, + withPagination, + zodErrorToString, +} from '@/utils'; const MAXIMUM_CATEGORY_AMOUNT = 10; const CATEGORY_PAGE_SIZE = 2; @@ -132,6 +133,12 @@ function categoryViewEmbed( }), inline: true, }, + { + name: 'Allowed Author Actions', + value: ThreadTicketing.actionsBitfieldToNames(category.allowedAuthorActions) + .map((name) => inlineCode(name)) + .join(', '), + }, { name: '\u200B', value: '\u200B', @@ -146,6 +153,11 @@ function categoryViewEmbed( value: category.silentPings ? 'Enabled' : 'Disabled', inline: true, }, + { + name: 'Skip Modal', + value: category.skipModal ? 'Enabled' : 'Disabled', + inline: true, + }, { name: 'Thread Notifications', value: category.threadNotifications ? 'Enabled' : 'Disabled', @@ -156,11 +168,6 @@ function categoryViewEmbed( value: category.titleAndDescriptionRequired ? 'Required' : 'Optional', inline: true, }, - { - name: 'Skip Modal', - value: category.skipModal ? 'Enabled' : 'Disabled', - inline: true, - }, ), ); } @@ -460,6 +467,11 @@ export default class extends Command.Interaction { .setLabel('Emoji, Title, & Description') .setDescription('Change the emoji, title, and description used for this category.') .setValue('emoji_title_description'), + new StringSelectMenuOptionBuilder() + .setEmoji('🛡️') + .setLabel('Ticket Managers') + .setDescription('Choose the managers who are responsible for this category.') + .setValue('managers'), new StringSelectMenuOptionBuilder() .setEmoji('#️⃣') .setLabel('Channel') @@ -470,16 +482,16 @@ export default class extends Command.Interaction { .setLabel('Logs Channel') .setDescription('Change the channel where logs get sent during ticket activity for the category.') .setValue('logs_channel'), - new StringSelectMenuOptionBuilder() - .setEmoji('🛡️') - .setLabel('Ticket Managers') - .setDescription('Choose the managers who are responsible for this category.') - .setValue('managers'), new StringSelectMenuOptionBuilder() .setEmoji('📔') .setLabel('Message Title & Description') .setDescription("Change the opening message's title and description.") .setValue('message_title_description'), + new StringSelectMenuOptionBuilder() + .setEmoji('🚦') + .setLabel('Allowed Author Actions') + .setDescription('Change what actions the ticket author can use.') + .setValue('allowed_author_actions'), new StringSelectMenuOptionBuilder() .setEmoji('🛃') .setLabel('Private Thread') @@ -490,6 +502,11 @@ export default class extends Command.Interaction { .setLabel('Silent Pings') .setDescription('Toggle whether managers get pinged (with noise) on ticket creation.') .setValue('silent_pings'), + new StringSelectMenuOptionBuilder() + .setEmoji('⏩') + .setLabel('Skip Modal') + .setDescription('Toggle whether modals are skipped.') + .setValue('skip_modals'), new StringSelectMenuOptionBuilder() .setEmoji('📣') .setLabel('Thread Notification') @@ -500,11 +517,6 @@ export default class extends Command.Interaction { .setLabel('Title & Description') .setDescription('Toggle whether ticket authors must write a title and description.') .setValue('ticket_title_description'), - new StringSelectMenuOptionBuilder() - .setEmoji('⏩') - .setLabel('Skip Modal') - .setDescription('Toggle whether modals are skipped.') - .setValue('skip_modals'), ); const row = new ActionRowBuilder().setComponents(categoriesMenu); @@ -616,6 +628,7 @@ export class ComponentInteraction extends Component.Interaction { super.dynamicCustomId('ticket_threads_category_configuration_channel'), super.dynamicCustomId('ticket_threads_category_configuration_logs_channel'), super.dynamicCustomId('ticket_threads_category_configuration_managers'), + super.dynamicCustomId('ticket_threads_category_configuration_allowed_author_actions'), super.dynamicCustomId('ticket_threads_category_delete_confirm'), super.dynamicCustomId('ticket_threads_category_delete_cancel'), super.dynamicCustomId('ticket_threads_category_view_previous'), @@ -637,6 +650,9 @@ export class ComponentInteraction extends Component.Interaction { case super.dynamicCustomId('ticket_threads_category_configuration_managers'): { return interaction.isRoleSelectMenu() && this.categoryManagers({ interaction }); } + case super.dynamicCustomId('ticket_threads_category_configuration_allowed_author_actions'): { + return interaction.isStringSelectMenu() && this.allowedAuthorActions({ interaction }); + } case super.dynamicCustomId('ticket_threads_category_delete_confirm'): case super.dynamicCustomId('ticket_threads_category_delete_cancel'): { return interaction.isButton() && this.confirmDeleteCategory({ interaction }); @@ -665,6 +681,18 @@ export class ComponentInteraction extends Component.Interaction { case 'emoji_title_description': { return this.categoryFieldsModalValues({ interaction }); } + case 'managers': { + const { dynamicValue } = super.extractCustomId(interaction.customId); + const managersMenu = new RoleSelectMenuBuilder() + .setCustomId(super.customId('ticket_threads_category_configuration_managers', dynamicValue)) + .setMinValues(0) + .setMaxValues(10) + .setPlaceholder('Choose the ticket managers of this category.'); + + const row = new ActionRowBuilder().setComponents(managersMenu); + + return interaction.reply({ components: [row] }); + } case 'channel': case 'logs_channel': { const { dynamicValue } = super.extractCustomId(interaction.customId); @@ -678,25 +706,53 @@ export class ComponentInteraction extends Component.Interaction { ) .setChannelTypes(ChannelType.GuildText); - const channelRow = new ActionRowBuilder().setComponents(channelMenu); + const row = new ActionRowBuilder().setComponents(channelMenu); - return interaction.reply({ components: [channelRow] }); + return interaction.reply({ components: [row] }); } - case 'managers': { + case 'message_title_description': { + return this.categoryMessageTitleDescriptionValues({ interaction }); + } + case 'allowed_author_actions': { const { dynamicValue } = super.extractCustomId(interaction.customId); - const managersMenu = new RoleSelectMenuBuilder() - .setCustomId(super.customId('ticket_threads_category_configuration_managers', dynamicValue)) - .setMinValues(0) - .setMaxValues(10) - .setPlaceholder('Choose the ticket managers of this category.'); - const row = new ActionRowBuilder().setComponents(managersMenu); + const selectMenu = new StringSelectMenuBuilder() + .setCustomId(super.customId('ticket_threads_category_configuration_allowed_author_actions', dynamicValue)) + .setMinValues(1) + .setMaxValues(1) + .setPlaceholder('Edit one of the following ticket author actions:') + .setOptions( + new StringSelectMenuOptionBuilder() + .setEmoji('📝') + .setLabel('Rename Title') + .setDescription('Toggle whether ticket authors can rename titles.') + .setValue('rename_title'), + new StringSelectMenuOptionBuilder() + .setEmoji('🔒') + .setLabel('Lock') + .setDescription('Toggle whether ticket authors can lock tickets.') + .setValue('lock'), + new StringSelectMenuOptionBuilder() + .setEmoji('🗃') + .setLabel('Close') + .setDescription('Toggle whether ticket authors can close tickets.') + .setValue('close'), + new StringSelectMenuOptionBuilder() + .setEmoji('🔐') + .setLabel('Lock & Close') + .setDescription('Toggle whether ticket authors can lock and close tickets.') + .setValue('lock_and_close'), + new StringSelectMenuOptionBuilder() + .setEmoji('🗑') + .setLabel('Delete') + .setDescription('Toggle whether ticket authors can delete tickets.') + .setValue('delete'), + ); + + const row = new ActionRowBuilder().setComponents(selectMenu); return interaction.reply({ components: [row] }); } - case 'message_title_description': { - return this.categoryMessageTitleDescriptionValues({ interaction }); - } case 'private': case 'notification': { return this.categoryPrivateAndNotification({ interaction }); @@ -812,16 +868,82 @@ export class ComponentInteraction extends Component.Interaction { .where(and(eq(ticketThreadsCategories.id, categoryId), eq(ticketThreadsCategories.guildId, interaction.guildId))); const roles = managers.map((id) => roleMention(id)).join(', '); - const embed = super - .userEmbed(interaction.user) - .setTitle('Updated the Thread Ticket Category') - .setDescription( - `${interaction.user.toString()} updated the managers of the category to: ${ - managers.length > 0 ? roles : 'none' - }.`, - ); - return interaction.editReply({ components: [], embeds: [embed] }); + return interaction.editReply({ + components: [], + embeds: [ + super + .userEmbed(interaction.user) + .setTitle('Updated the Thread Ticket Category') + .setDescription( + `${interaction.user.toString()} updated the managers of the category to: ${ + managers.length > 0 ? roles : 'none' + }.`, + ), + ], + }); + } + + @DeferUpdate + private async allowedAuthorActions({ interaction }: Component.Context<'string'>) { + const { dynamicValue } = super.extractCustomId(interaction.customId, true); + const { + data: categoryId, + error, + success, + } = ticketThreadsCategoriesSelectSchema.shape.id.safeParse(Number(dynamicValue)); + + if (!success) { + return interaction.editReply({ + components: [], + embeds: [super.userEmbedError(interaction.user).setDescription(zodErrorToString(error))], + }); + } + + const value = interaction.values.at(0); + + if (!value) { + return interaction.reply({ + embeds: [super.userEmbedError(interaction.user).setDescription('The selected value could not be found.')], + ephemeral: true, + }); + } + + const [row] = await database + .select() + .from(ticketThreadsCategories) + .where(and(eq(ticketThreadsCategories.id, categoryId), eq(ticketThreadsCategories.guildId, interaction.guildId))); + + if (!row) { + return interaction.reply({ + embeds: [ + super.userEmbedError(interaction.user).setDescription('No category with the given ID could be found.'), + ], + }); + } + + const authorPermissions = new ThreadTicketActionsPermissionBitField(row.allowedAuthorActions); + let enabled = false; + + for (const [name, flag] of ThreadTicketing.actionsAsKeyAndFlagsMap) { + if (value === name) { + enabled = authorPermissions.toggle(flag); + break; + } + } + + await authorPermissions.updateAuthorPermissions(row.id, row.guildId); + + return interaction.editReply({ + embeds: [ + super + .userEmbed(interaction.user) + .setTitle('Updated the Thread Ticket Category') + .setDescription( + `${interaction.user.toString()} has toggled the ${inlineCode(ThreadTicketing.ActionsAsName[value as ThreadTicketing.KeyOfActions])} ticket author action to ${enabled ? 'enabled' : 'disabled'}.`, + ), + ], + }); } @DeferUpdate diff --git a/apps/bot/src/commands/thread-ticketing/ticket.ts b/apps/bot/src/commands/thread-ticketing/ticket.ts index e1217a4..9cace4a 100644 --- a/apps/bot/src/commands/thread-ticketing/ticket.ts +++ b/apps/bot/src/commands/thread-ticketing/ticket.ts @@ -1,7 +1,7 @@ import { ChannelType, Colors, PermissionFlagsBits, type Snowflake } from 'discord.js'; import { Command, Component, DeferReply, Modal } from '@ticketer/djs-framework'; -import { ThreadTicketing, zodErrorToString } from '@/utils'; import { + ThreadTicketActionsPermissionBitField, and, database, eq, @@ -9,6 +9,7 @@ import { ticketThreadsCategoriesSelectSchema, ticketsThreads, } from '@ticketer/database'; +import { ThreadTicketing, zodErrorToString } from '@/utils'; import { getTranslations, translate } from '@/i18n'; import { z } from 'zod'; @@ -271,7 +272,7 @@ export class ModalInteraction extends Modal.Interaction { @DeferReply({ ephemeral: true }) private async renameTitle({ interaction }: Modal.Context) { const { channel, fields, guild, guildLocale, locale, member, user } = interaction; - const translations = translate(locale).tickets.threads.categories.buttons; + const translations = translate(locale).tickets.threads.categories.actions; if (channel?.type !== ChannelType.PrivateThread && channel?.type !== ChannelType.PublicThread) { return interaction.editReply({ @@ -295,6 +296,7 @@ export class ModalInteraction extends Modal.Interaction { const [row] = await database .select({ + allowedAuthorActions: ticketThreadsCategories.allowedAuthorActions, authorId: ticketsThreads.authorId, logsChannelId: ticketThreadsCategories.logsChannelId, managers: ticketThreadsCategories.managers, @@ -303,14 +305,28 @@ export class ModalInteraction extends Modal.Interaction { .where(eq(ticketsThreads.threadId, channel.id)) .innerJoin(ticketThreadsCategories, eq(ticketsThreads.categoryId, ticketThreadsCategories.id)); - if (row?.authorId !== user.id && !row?.managers.some((id) => member.roles.resolve(id))) { - return interaction.editReply({ - embeds: [ - super - .userEmbedError(user, translations._errorIfNotTicketAuthorOrManager.title()) - .setDescription(translations._errorIfNotTicketAuthorOrManager.description()), - ], - }); + if (!row?.managers.some((id) => member.roles.resolve(id))) { + if (row?.authorId !== user.id) { + return interaction.editReply({ + embeds: [ + super + .userEmbedError(user, translations._errorIfNotTicketAuthorOrManager.title()) + .setDescription(translations._errorIfNotTicketAuthorOrManager.description()), + ], + }); + } + + const authorPermissions = new ThreadTicketActionsPermissionBitField(row.allowedAuthorActions); + + if (!authorPermissions.has(ThreadTicketActionsPermissionBitField.Flags.RenameTitle)) { + return interaction.editReply({ + embeds: [ + this.userEmbedError(user, translations._errorIfNoAuthorPermissions.title()).setDescription( + translations._errorIfNoAuthorPermissions.description(), + ), + ], + }); + } } const oldTitle = channel.name; @@ -328,7 +344,7 @@ export class ModalInteraction extends Modal.Interaction { const successTranslations = translations.renameTitle.modal.success; const guildSuccessTranslations = - translate(guildLocale).tickets.threads.categories.buttons.renameTitle.modal.success; + translate(guildLocale).tickets.threads.categories.actions.renameTitle.modal.success; const embed = super .userEmbed(user) .setColor(Colors.DarkGreen) diff --git a/apps/bot/src/events/guild/MessageCreate.ts b/apps/bot/src/events/guild/MessageCreate.ts index d730bd5..68190c2 100644 --- a/apps/bot/src/events/guild/MessageCreate.ts +++ b/apps/bot/src/events/guild/MessageCreate.ts @@ -47,7 +47,7 @@ export default class extends Event.Handler { user: message.author, }); - const translations = translate(message.guild.preferredLocale).tickets.automaticThreads.buttons; + const translations = translate(message.guild.preferredLocale).tickets.automaticThreads.actions; const buttonsRow = ticketButtons({ close: { customId: super.customId('ticket_automatic_threads_thread_close'), diff --git a/apps/bot/src/events/guild/ThreadCreate.ts b/apps/bot/src/events/guild/ThreadCreate.ts index d0df5f6..d83b1af 100644 --- a/apps/bot/src/events/guild/ThreadCreate.ts +++ b/apps/bot/src/events/guild/ThreadCreate.ts @@ -39,7 +39,7 @@ export default class extends Event.Handler { user, }); - const translations = translate(thread.guild.preferredLocale).tickets.userForums.buttons; + const translations = translate(thread.guild.preferredLocale).tickets.userForums.actions; const buttonsRow = ticketButtons({ close: { customId: super.customId('ticket_user_forums_thread_close'), diff --git a/apps/bot/src/i18n/en-GB/threads.ts b/apps/bot/src/i18n/en-GB/threads.ts index 940e4e0..e399bcc 100644 --- a/apps/bot/src/i18n/en-GB/threads.ts +++ b/apps/bot/src/i18n/en-GB/threads.ts @@ -101,14 +101,18 @@ export default { }, }, }, - buttons: { + actions: { _errorIfNotTicketChannel: { title: ERROR_TITLE, description: 'The channel is not a valid ticket channel.', }, _errorIfNotTicketAuthorOrManager: { title: ERROR_TITLE, - description: 'You need to be the ticket author or manager to execute this button/command.', + description: 'You need to be the ticket author or manager to execute this action.', + }, + _errorIfNoAuthorPermissions: { + title: ERROR_TITLE, + description: 'You do not have permission to use this action.', }, renameTitle: { builder: { diff --git a/apps/bot/src/i18n/en-GB/userForums.ts b/apps/bot/src/i18n/en-GB/userForums.ts index f606ee2..c6ba2dc 100644 --- a/apps/bot/src/i18n/en-GB/userForums.ts +++ b/apps/bot/src/i18n/en-GB/userForums.ts @@ -1,7 +1,7 @@ import ERROR_TITLE from './errorTitle'; export default { - buttons: { + actions: { _errorIfNotThreadChannel: { title: ERROR_TITLE, description: 'The channel is not a valid thread channel.', diff --git a/apps/bot/src/i18n/i18n-types.ts b/apps/bot/src/i18n/i18n-types.ts index a76b9d9..85444fe 100644 --- a/apps/bot/src/i18n/i18n-types.ts +++ b/apps/bot/src/i18n/i18n-types.ts @@ -501,7 +501,7 @@ type RootTranslation = { } } automaticThreads: { - buttons: { + actions: { _errorIfNotThreadChannel: { /** * A​n​ ​E​r​r​o​r​ ​O​c​c​u​r​e​d @@ -931,7 +931,7 @@ type RootTranslation = { } } } - buttons: { + actions: { _errorIfNotTicketChannel: { /** * A​n​ ​E​r​r​o​r​ ​O​c​c​u​r​e​d @@ -948,7 +948,17 @@ type RootTranslation = { */ title: string /** - * Y​o​u​ ​n​e​e​d​ ​t​o​ ​b​e​ ​t​h​e​ ​t​i​c​k​e​t​ ​a​u​t​h​o​r​ ​o​r​ ​m​a​n​a​g​e​r​ ​t​o​ ​e​x​e​c​u​t​e​ ​t​h​i​s​ ​b​u​t​t​o​n​/​c​o​m​m​a​n​d​. + * Y​o​u​ ​n​e​e​d​ ​t​o​ ​b​e​ ​t​h​e​ ​t​i​c​k​e​t​ ​a​u​t​h​o​r​ ​o​r​ ​m​a​n​a​g​e​r​ ​t​o​ ​e​x​e​c​u​t​e​ ​t​h​i​s​ ​a​c​t​i​o​n​. + */ + description: string + } + _errorIfNoAuthorPermissions: { + /** + * A​n​ ​E​r​r​o​r​ ​O​c​c​u​r​e​d + */ + title: string + /** + * Y​o​u​ ​d​o​ ​n​o​t​ ​h​a​v​e​ ​p​e​r​m​i​s​s​i​o​n​ ​t​o​ ​u​s​e​ ​t​h​i​s​ ​a​c​t​i​o​n​. */ description: string } @@ -1222,7 +1232,7 @@ type RootTranslation = { } } userForums: { - buttons: { + actions: { _errorIfNotThreadChannel: { /** * A​n​ ​E​r​r​o​r​ ​O​c​c​u​r​e​d @@ -1907,7 +1917,7 @@ export type TranslationFunctions = { } } automaticThreads: { - buttons: { + actions: { _errorIfNotThreadChannel: { /** * An Error Occured @@ -2320,7 +2330,7 @@ export type TranslationFunctions = { } } } - buttons: { + actions: { _errorIfNotTicketChannel: { /** * An Error Occured @@ -2337,7 +2347,17 @@ export type TranslationFunctions = { */ title: () => LocalizedString /** - * You need to be the ticket author or manager to execute this button/command. + * You need to be the ticket author or manager to execute this action. + */ + description: () => LocalizedString + } + _errorIfNoAuthorPermissions: { + /** + * An Error Occured + */ + title: () => LocalizedString + /** + * You do not have permission to use this action. */ description: () => LocalizedString } @@ -2597,7 +2617,7 @@ export type TranslationFunctions = { } } userForums: { - buttons: { + actions: { _errorIfNotThreadChannel: { /** * An Error Occured diff --git a/apps/bot/src/i18n/sv-SE/threads.ts b/apps/bot/src/i18n/sv-SE/threads.ts index 8ece87d..38fd195 100644 --- a/apps/bot/src/i18n/sv-SE/threads.ts +++ b/apps/bot/src/i18n/sv-SE/threads.ts @@ -102,7 +102,7 @@ export default { }, }, }, - buttons: { + actions: { _errorIfNotTicketChannel: { title: ERROR_TITLE, description: 'Kanalen är inte en giltigt stödbiljettkanal.', @@ -112,6 +112,10 @@ export default { description: 'Du måste vara stödbiljettägaren eller stödbiljettansvarigt för att köra den/det här knappen/kommandot.', }, + _errorIfNoAuthorPermissions: { + title: ERROR_TITLE, + description: 'Du har inte tillstånd för att använda denna funktion.', + }, renameTitle: { builder: { label: 'Ändra Titeln', diff --git a/apps/bot/src/i18n/sv-SE/userForums.ts b/apps/bot/src/i18n/sv-SE/userForums.ts index 8b0deae..c9783fc 100644 --- a/apps/bot/src/i18n/sv-SE/userForums.ts +++ b/apps/bot/src/i18n/sv-SE/userForums.ts @@ -3,7 +3,7 @@ import ERROR_TITLE from './errorTitle'; import type { Translation } from '../i18n-types'; export default { - buttons: { + actions: { _errorIfNotThreadChannel: { title: ERROR_TITLE, description: 'Kanalen är inte en giltigt trådkanal.', diff --git a/apps/bot/src/utils/thread-ticketing/closeTicket.ts b/apps/bot/src/utils/thread-ticketing/closeTicket.ts index 8da5075..61fff97 100644 --- a/apps/bot/src/utils/thread-ticketing/closeTicket.ts +++ b/apps/bot/src/utils/thread-ticketing/closeTicket.ts @@ -1,6 +1,12 @@ import type { BaseInteraction, Command, Component } from '@ticketer/djs-framework'; import { ChannelType, Colors, PermissionFlagsBits } from 'discord.js'; -import { database, eq, ticketThreadsCategories, ticketsThreads } from '@ticketer/database'; +import { + ThreadTicketActionsPermissionBitField, + database, + eq, + ticketThreadsCategories, + ticketsThreads, +} from '@ticketer/database'; import { translate } from '@/i18n'; export async function closeTicket( @@ -8,8 +14,8 @@ export async function closeTicket( { interaction }: Command.Context | Component.Context, ) { const { channel, guild, guildLocale, locale, member, user } = interaction; - const translations = translate(locale).tickets.threads.categories.buttons; - const guildSuccessTranslations = translate(guildLocale).tickets.threads.categories.buttons.close.execute.success; + const translations = translate(locale).tickets.threads.categories.actions; + const guildSuccessTranslations = translate(guildLocale).tickets.threads.categories.actions.close.execute.success; if (channel?.type !== ChannelType.PrivateThread && channel?.type !== ChannelType.PublicThread) { return interaction.editReply({ @@ -22,7 +28,6 @@ export async function closeTicket( } if (channel.archived) return; - if (!channel.editable) { return interaction.editReply({ embeds: [ @@ -35,6 +40,7 @@ export async function closeTicket( const [row] = await database .select({ + allowedAuthorActions: ticketThreadsCategories.allowedAuthorActions, authorId: ticketsThreads.authorId, logsChannelId: ticketThreadsCategories.logsChannelId, managers: ticketThreadsCategories.managers, @@ -43,14 +49,28 @@ export async function closeTicket( .where(eq(ticketsThreads.threadId, channel.id)) .innerJoin(ticketThreadsCategories, eq(ticketsThreads.categoryId, ticketThreadsCategories.id)); - if (row?.authorId !== user.id && !row?.managers.some((id) => member.roles.resolve(id))) { - return interaction.editReply({ - embeds: [ - this.userEmbedError(user, translations._errorIfNotTicketAuthorOrManager.title()).setDescription( - translations._errorIfNotTicketAuthorOrManager.description(), - ), - ], - }); + if (!row?.managers.some((id) => member.roles.resolve(id))) { + if (row?.authorId !== user.id) { + return interaction.editReply({ + embeds: [ + this.userEmbedError(user, translations._errorIfNotTicketAuthorOrManager.title()).setDescription( + translations._errorIfNotTicketAuthorOrManager.description(), + ), + ], + }); + } + + const authorPermissions = new ThreadTicketActionsPermissionBitField(row.allowedAuthorActions); + + if (!authorPermissions.has(ThreadTicketActionsPermissionBitField.Flags.Close)) { + return interaction.editReply({ + embeds: [ + this.userEmbedError(user, translations._errorIfNoAuthorPermissions.title()).setDescription( + translations._errorIfNoAuthorPermissions.description(), + ), + ], + }); + } } const embed = this.userEmbed(user) diff --git a/apps/bot/src/utils/thread-ticketing/createTicket.ts b/apps/bot/src/utils/thread-ticketing/createTicket.ts index 0458ee0..e4afc6c 100644 --- a/apps/bot/src/utils/thread-ticketing/createTicket.ts +++ b/apps/bot/src/utils/thread-ticketing/createTicket.ts @@ -255,23 +255,23 @@ export async function createTicket( const buttonsRow = ticketButtons({ close: { customId: this.customId('ticket_threads_category_create_close'), - label: guildTranslations.buttons.close.builder.label(), + label: guildTranslations.actions.close.builder.label(), }, delete: { customId: this.customId('ticket_threads_category_create_delete'), - label: guildTranslations.buttons.delete.builder.label(), + label: guildTranslations.actions.delete.builder.label(), }, lock: { customId: this.customId('ticket_threads_category_create_lock'), - label: guildTranslations.buttons.lock.builder.label(), + label: guildTranslations.actions.lock.builder.label(), }, lockAndClose: { customId: this.customId('ticket_threads_category_create_lock_and_close'), - label: guildTranslations.buttons.lockAndClose.builder.label(), + label: guildTranslations.actions.lockAndClose.builder.label(), }, renameTitle: { customId: this.customId('ticket_threads_category_create_rename_title'), - label: guildTranslations.buttons.renameTitle.builder.label(), + label: guildTranslations.actions.renameTitle.builder.label(), }, }); diff --git a/apps/bot/src/utils/thread-ticketing/deleteTicket.ts b/apps/bot/src/utils/thread-ticketing/deleteTicket.ts index 15f0885..c1780fa 100644 --- a/apps/bot/src/utils/thread-ticketing/deleteTicket.ts +++ b/apps/bot/src/utils/thread-ticketing/deleteTicket.ts @@ -1,6 +1,12 @@ import type { BaseInteraction, Command, Component } from '@ticketer/djs-framework'; import { ChannelType, Colors, PermissionFlagsBits, inlineCode } from 'discord.js'; -import { database, eq, ticketThreadsCategories, ticketsThreads } from '@ticketer/database'; +import { + ThreadTicketActionsPermissionBitField, + database, + eq, + ticketThreadsCategories, + ticketsThreads, +} from '@ticketer/database'; import { translate } from '@/i18n'; export async function deleteTicket( @@ -8,9 +14,9 @@ export async function deleteTicket( { interaction }: Command.Context | Component.Context, ) { const { channel, guild, guildLocale, locale, member, user } = interaction; - const translations = translate(locale).tickets.threads.categories.buttons; + const translations = translate(locale).tickets.threads.categories.actions; const guildSuccessTranslations = - translate(guildLocale).tickets.threads.categories.buttons.delete.execute.success.logs; + translate(guildLocale).tickets.threads.categories.actions.delete.execute.success.logs; if (channel?.type !== ChannelType.PrivateThread && channel?.type !== ChannelType.PublicThread) { return interaction.editReply({ @@ -34,6 +40,7 @@ export async function deleteTicket( const [row] = await database .select({ + allowedAuthorActions: ticketThreadsCategories.allowedAuthorActions, authorId: ticketsThreads.authorId, logsChannelId: ticketThreadsCategories.logsChannelId, managers: ticketThreadsCategories.managers, @@ -42,14 +49,28 @@ export async function deleteTicket( .where(eq(ticketsThreads.threadId, channel.id)) .innerJoin(ticketThreadsCategories, eq(ticketsThreads.categoryId, ticketThreadsCategories.id)); - if (row?.authorId !== user.id && !row?.managers.some((id) => member.roles.resolve(id))) { - return interaction.editReply({ - embeds: [ - this.userEmbedError(user, translations._errorIfNotTicketAuthorOrManager.title()).setDescription( - translations._errorIfNotTicketAuthorOrManager.description(), - ), - ], - }); + if (!row?.managers.some((id) => member.roles.resolve(id))) { + if (row?.authorId !== user.id) { + return interaction.editReply({ + embeds: [ + this.userEmbedError(user, translations._errorIfNotTicketAuthorOrManager.title()).setDescription( + translations._errorIfNotTicketAuthorOrManager.description(), + ), + ], + }); + } + + const authorPermissions = new ThreadTicketActionsPermissionBitField(row.allowedAuthorActions); + + if (!authorPermissions.has(ThreadTicketActionsPermissionBitField.Flags.Delete)) { + return interaction.editReply({ + embeds: [ + this.userEmbedError(user, translations._errorIfNoAuthorPermissions.title()).setDescription( + translations._errorIfNoAuthorPermissions.description(), + ), + ], + }); + } } const embed = this.userEmbed(user) diff --git a/apps/bot/src/utils/thread-ticketing/index.ts b/apps/bot/src/utils/thread-ticketing/index.ts index bcd6a4e..5fdc42e 100644 --- a/apps/bot/src/utils/thread-ticketing/index.ts +++ b/apps/bot/src/utils/thread-ticketing/index.ts @@ -4,6 +4,7 @@ export * from './createTicket'; export * from './deleteTicket'; export * from './lockTicket'; export * from './renameTitleModal'; +export * from './ticketActions'; export * from './ticketModal'; export * from './ticketState'; export * from './titleAndEmoji'; diff --git a/apps/bot/src/utils/thread-ticketing/lockTicket.ts b/apps/bot/src/utils/thread-ticketing/lockTicket.ts index 8c0b38d..423bc30 100644 --- a/apps/bot/src/utils/thread-ticketing/lockTicket.ts +++ b/apps/bot/src/utils/thread-ticketing/lockTicket.ts @@ -1,6 +1,12 @@ import type { BaseInteraction, Command, Component } from '@ticketer/djs-framework'; import { ChannelType, Colors, PermissionFlagsBits } from 'discord.js'; -import { database, eq, ticketThreadsCategories, ticketsThreads } from '@ticketer/database'; +import { + ThreadTicketActionsPermissionBitField, + database, + eq, + ticketThreadsCategories, + ticketsThreads, +} from '@ticketer/database'; import { translate } from '@/i18n'; export async function lockTicket( @@ -9,9 +15,9 @@ export async function lockTicket( lockAndClose = false, ) { const { channel, guild, guildLocale, locale, member, user } = interaction; - const translations = translate(locale).tickets.threads.categories.buttons; + const translations = translate(locale).tickets.threads.categories.actions; const guildSuccessTranslations = - translate(guildLocale).tickets.threads.categories.buttons[lockAndClose ? 'lockAndClose' : 'lock'].execute.success; + translate(guildLocale).tickets.threads.categories.actions[lockAndClose ? 'lockAndClose' : 'lock'].execute.success; if (channel?.type !== ChannelType.PrivateThread && channel?.type !== ChannelType.PublicThread) { return interaction.editReply({ @@ -23,9 +29,7 @@ export async function lockTicket( }); } - // This is here just in case somebody bypassed the disabled buttons in locked threads. if (channel.locked) return; - if (lockAndClose ? !channel.manageable || !channel.editable : !channel.manageable) { return interaction.editReply({ embeds: [ @@ -45,6 +49,7 @@ export async function lockTicket( const [row] = await database .select({ + allowedAuthorActions: ticketThreadsCategories.allowedAuthorActions, authorId: ticketsThreads.authorId, logsChannelId: ticketThreadsCategories.logsChannelId, managers: ticketThreadsCategories.managers, @@ -53,14 +58,28 @@ export async function lockTicket( .where(eq(ticketsThreads.threadId, channel.id)) .innerJoin(ticketThreadsCategories, eq(ticketsThreads.categoryId, ticketThreadsCategories.id)); - if (row?.authorId !== user.id && !row?.managers.some((id) => member.roles.resolve(id))) { - return interaction.editReply({ - embeds: [ - this.userEmbedError(user, translations._errorIfNotTicketAuthorOrManager.title()).setDescription( - translations._errorIfNotTicketAuthorOrManager.description(), - ), - ], - }); + if (!row?.managers.some((id) => member.roles.resolve(id))) { + if (row?.authorId !== user.id) { + return interaction.editReply({ + embeds: [ + this.userEmbedError(user, translations._errorIfNotTicketAuthorOrManager.title()).setDescription( + translations._errorIfNotTicketAuthorOrManager.description(), + ), + ], + }); + } + + const authorPermissions = new ThreadTicketActionsPermissionBitField(row.allowedAuthorActions); + + if (!authorPermissions.has(ThreadTicketActionsPermissionBitField.Flags[lockAndClose ? 'LockAndClose' : 'Lock'])) { + return interaction.editReply({ + embeds: [ + this.userEmbedError(user, translations._errorIfNoAuthorPermissions.title()).setDescription( + translations._errorIfNoAuthorPermissions.description(), + ), + ], + }); + } } const embed = this.userEmbed(user) diff --git a/apps/bot/src/utils/thread-ticketing/renameTitleModal.ts b/apps/bot/src/utils/thread-ticketing/renameTitleModal.ts index 893bbb8..f357c99 100644 --- a/apps/bot/src/utils/thread-ticketing/renameTitleModal.ts +++ b/apps/bot/src/utils/thread-ticketing/renameTitleModal.ts @@ -6,7 +6,7 @@ export function renameTitleModal( this: BaseInteraction.Interaction, { interaction }: Command.Context | Component.Context, ) { - const translations = translate(interaction.locale).tickets.threads.categories.buttons.renameTitle.component.modal; + const translations = translate(interaction.locale).tickets.threads.categories.actions.renameTitle.component.modal; const input = new TextInputBuilder() .setCustomId(this.customId('title')) diff --git a/apps/bot/src/utils/thread-ticketing/ticketActions.ts b/apps/bot/src/utils/thread-ticketing/ticketActions.ts new file mode 100644 index 0000000..78d3704 --- /dev/null +++ b/apps/bot/src/utils/thread-ticketing/ticketActions.ts @@ -0,0 +1,46 @@ +import { ThreadTicketActionsPermissionBitField } from '@ticketer/database'; + +export const ActionsAsName = { + rename_title: 'Rename Title', + lock: 'Lock', + close: 'Close', + lock_and_close: 'Lock And Close', + delete: 'Delete', +} as const; + +type Actions = typeof ActionsAsName; +export type KeyOfActions = keyof Actions; + +export const ActionsAsKey = (value: Actions[KeyOfActions]) => + Object.keys(ActionsAsName).find((key) => ActionsAsName[key as KeyOfActions] === value) as KeyOfActions; + +// We are going to be explicit instead of looping through the objects to avoid possible unwanted behaviour. +export const actionsAsKeyAndFlagsMap = new Map([ + [ActionsAsKey(ActionsAsName.rename_title), ThreadTicketActionsPermissionBitField.Flags.RenameTitle], + [ActionsAsKey(ActionsAsName.lock), ThreadTicketActionsPermissionBitField.Flags.Lock], + [ActionsAsKey(ActionsAsName.close), ThreadTicketActionsPermissionBitField.Flags.Close], + [ActionsAsKey(ActionsAsName.lock_and_close), ThreadTicketActionsPermissionBitField.Flags.LockAndClose], + [ActionsAsKey(ActionsAsName.delete), ThreadTicketActionsPermissionBitField.Flags.Delete], +]); + +const actionsAndFlagsMap = new Map([ + [ActionsAsName.rename_title, ThreadTicketActionsPermissionBitField.Flags.RenameTitle], + [ActionsAsName.lock, ThreadTicketActionsPermissionBitField.Flags.Lock], + [ActionsAsName.close, ThreadTicketActionsPermissionBitField.Flags.Close], + [ActionsAsName.lock_and_close, ThreadTicketActionsPermissionBitField.Flags.LockAndClose], + [ActionsAsName.delete, ThreadTicketActionsPermissionBitField.Flags.Delete], +]); + +export function actionsBitfieldToNames(bitfield: number | null) { + bitfield ??= ThreadTicketActionsPermissionBitField.Default; + const permissions = new ThreadTicketActionsPermissionBitField(bitfield); + const names: Actions[KeyOfActions][] = []; + + for (const [name, flag] of actionsAndFlagsMap) { + if (permissions.has(flag)) { + names.push(name); + } + } + + return names; +} diff --git a/apps/bot/src/utils/user-forums/closeTicket.ts b/apps/bot/src/utils/user-forums/closeTicket.ts index 382cef6..901bc62 100644 --- a/apps/bot/src/utils/user-forums/closeTicket.ts +++ b/apps/bot/src/utils/user-forums/closeTicket.ts @@ -9,7 +9,7 @@ export async function closeTicket( isAutomaticThreads = false, ) { const { channel, locale, member, user } = interaction; - const translations = translate(locale).tickets[isAutomaticThreads ? 'automaticThreads' : 'userForums'].buttons; + const translations = translate(locale).tickets[isAutomaticThreads ? 'automaticThreads' : 'userForums'].actions; const table = isAutomaticThreads ? automaticThreadsConfigurations : userForumsConfigurations; if ( diff --git a/apps/bot/src/utils/user-forums/deleteTicket.ts b/apps/bot/src/utils/user-forums/deleteTicket.ts index bc3f8d2..b5de936 100644 --- a/apps/bot/src/utils/user-forums/deleteTicket.ts +++ b/apps/bot/src/utils/user-forums/deleteTicket.ts @@ -9,7 +9,7 @@ export async function deleteTicket( isAutomaticThreads = false, ) { const { channel, locale, member, user } = interaction; - const translations = translate(locale).tickets[isAutomaticThreads ? 'automaticThreads' : 'userForums'].buttons; + const translations = translate(locale).tickets[isAutomaticThreads ? 'automaticThreads' : 'userForums'].actions; const table = isAutomaticThreads ? automaticThreadsConfigurations : userForumsConfigurations; if ( diff --git a/apps/bot/src/utils/user-forums/lockTicket.ts b/apps/bot/src/utils/user-forums/lockTicket.ts index a188895..211c0b9 100644 --- a/apps/bot/src/utils/user-forums/lockTicket.ts +++ b/apps/bot/src/utils/user-forums/lockTicket.ts @@ -10,7 +10,7 @@ export async function lockTicket( isAutomaticThreads = false, ) { const { channel, locale, member, user } = interaction; - const translations = translate(locale).tickets[isAutomaticThreads ? 'automaticThreads' : 'userForums'].buttons; + const translations = translate(locale).tickets[isAutomaticThreads ? 'automaticThreads' : 'userForums'].actions; const table = isAutomaticThreads ? automaticThreadsConfigurations : userForumsConfigurations; if ( diff --git a/apps/bot/src/utils/user-forums/renameTitle.ts b/apps/bot/src/utils/user-forums/renameTitle.ts index 9c73663..2477849 100644 --- a/apps/bot/src/utils/user-forums/renameTitle.ts +++ b/apps/bot/src/utils/user-forums/renameTitle.ts @@ -9,7 +9,7 @@ export async function renameTitle( isAutomaticThreads = false, ) { const { channel, fields, locale, member, user } = interaction; - const translations = translate(locale).tickets[isAutomaticThreads ? 'automaticThreads' : 'userForums'].buttons; + const translations = translate(locale).tickets[isAutomaticThreads ? 'automaticThreads' : 'userForums'].actions; const table = isAutomaticThreads ? automaticThreadsConfigurations : userForumsConfigurations; if ( diff --git a/apps/bot/src/utils/user-forums/renameTitleModal.ts b/apps/bot/src/utils/user-forums/renameTitleModal.ts index c0c0934..072d0e9 100644 --- a/apps/bot/src/utils/user-forums/renameTitleModal.ts +++ b/apps/bot/src/utils/user-forums/renameTitleModal.ts @@ -8,7 +8,7 @@ export function renameTitleModal( isAutomaticThreads = false, ) { const translations = translate(interaction.locale).tickets[isAutomaticThreads ? 'automaticThreads' : 'userForums'] - .buttons.renameTitle.component.modal; + .actions.renameTitle.component.modal; const input = new TextInputBuilder() .setCustomId(this.customId('title')) diff --git a/apps/website/package.json b/apps/website/package.json index cbf797c..030a099 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -20,7 +20,7 @@ "@vercel/speed-insights": "^1.0.12", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", - "lucide-react": "^0.437.0", + "lucide-react": "^0.438.0", "next": "14.2.7", "next-themes": "^0.3.0", "react": "^18.3.1", @@ -33,11 +33,11 @@ "@eslint/eslintrc": "^3.1.0", "@next/eslint-plugin-next": "^14.2.7", "@ticketer/eslint-config": "workspace:*", - "@types/node": "^22.5.1", + "@types/node": "^22.5.2", "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", "autoprefixer": "^10.4.20", - "postcss": "^8.4.41", + "postcss": "^8.4.44", "tailwindcss": "^3.4.10", "typescript": "^5.5.4" } diff --git a/apps/website/src/app/layout.tsx b/apps/website/src/app/layout.tsx index 0cfba00..fc2187c 100644 --- a/apps/website/src/app/layout.tsx +++ b/apps/website/src/app/layout.tsx @@ -28,6 +28,7 @@ export const metadata: Metadata = { alt: 'Ticketer logo', }, ], + url: baseURL, }, metadataBase: baseURL, }; diff --git a/packages/database/migrations/0004_outgoing_titania.sql b/packages/database/migrations/0004_outgoing_titania.sql new file mode 100644 index 0000000..f9dea08 --- /dev/null +++ b/packages/database/migrations/0004_outgoing_titania.sql @@ -0,0 +1 @@ +ALTER TABLE `ticketThreadsCategories` ADD `allowedAuthorActions` int unsigned; \ No newline at end of file diff --git a/packages/database/migrations/meta/0004_snapshot.json b/packages/database/migrations/meta/0004_snapshot.json new file mode 100644 index 0000000..b93d008 --- /dev/null +++ b/packages/database/migrations/meta/0004_snapshot.json @@ -0,0 +1,523 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "7e16ba19-2d2d-4aa4-9923-a426a5a30f58", + "prevId": "45f93445-768b-4c3b-8793-2c8ea47c7040", + "tables": { + "automaticThreadsConfigurations": { + "name": "automaticThreadsConfigurations", + "columns": { + "channelId": { + "name": "channelId", + "type": "bigint unsigned", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "guildId": { + "name": "guildId", + "type": "bigint unsigned", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "managers": { + "name": "managers", + "type": "json", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('[]')" + }, + "openingMessageTitle": { + "name": "openingMessageTitle", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "openingMessageDescription": { + "name": "openingMessageDescription", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "guildId_index": { + "name": "guildId_index", + "columns": [ + "guildId" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "automaticThreadsConfigurations_channelId": { + "name": "automaticThreadsConfigurations_channelId", + "columns": [ + "channelId" + ] + } + }, + "uniqueConstraints": {} + }, + "ticketThreadsCategories": { + "name": "ticketThreadsCategories", + "columns": { + "id": { + "name": "id", + "type": "int unsigned", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "guildId": { + "name": "guildId", + "type": "bigint unsigned", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "allowedAuthorActions": { + "name": "allowedAuthorActions", + "type": "int unsigned", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "categoryEmoji": { + "name": "categoryEmoji", + "type": "varchar(8)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "categoryTitle": { + "name": "categoryTitle", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "categoryDescription": { + "name": "categoryDescription", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channelId": { + "name": "channelId", + "type": "bigint unsigned", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "logsChannelId": { + "name": "logsChannelId", + "type": "bigint unsigned", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "managers": { + "name": "managers", + "type": "json", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('[]')" + }, + "openingMessageTitle": { + "name": "openingMessageTitle", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "openingMessageDescription": { + "name": "openingMessageDescription", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "privateThreads": { + "name": "privateThreads", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "silentPings": { + "name": "silentPings", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "skipModal": { + "name": "skipModal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "threadNotifications": { + "name": "threadNotifications", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "titleAndDescriptionRequired": { + "name": "titleAndDescriptionRequired", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "guildId_index": { + "name": "guildId_index", + "columns": [ + "guildId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "ticket_threads_categories_fk": { + "name": "ticket_threads_categories_fk", + "tableFrom": "ticketThreadsCategories", + "tableTo": "ticketThreadsConfigurations", + "columnsFrom": [ + "guildId" + ], + "columnsTo": [ + "guildId" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ticketThreadsCategories_id": { + "name": "ticketThreadsCategories_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {} + }, + "ticketThreadsConfigurations": { + "name": "ticketThreadsConfigurations", + "columns": { + "guildId": { + "name": "guildId", + "type": "bigint unsigned", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "activeTickets": { + "name": "activeTickets", + "type": "tinyint unsigned", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "ticketThreadsConfigurations_guildId": { + "name": "ticketThreadsConfigurations_guildId", + "columns": [ + "guildId" + ] + } + }, + "uniqueConstraints": {} + }, + "ticketsThreads": { + "name": "ticketsThreads", + "columns": { + "threadId": { + "name": "threadId", + "type": "bigint unsigned", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "authorId": { + "name": "authorId", + "type": "bigint unsigned", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "categoryId": { + "name": "categoryId", + "type": "int unsigned", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "guildId": { + "name": "guildId", + "type": "bigint unsigned", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "enum('active','archived','locked','lockedAndArchived')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + } + }, + "indexes": { + "authorId_index": { + "name": "authorId_index", + "columns": [ + "authorId" + ], + "isUnique": false + }, + "categoryId_index": { + "name": "categoryId_index", + "columns": [ + "categoryId" + ], + "isUnique": false + }, + "guildId_index": { + "name": "guildId_index", + "columns": [ + "guildId" + ], + "isUnique": false + }, + "state_index": { + "name": "state_index", + "columns": [ + "state" + ], + "isUnique": false + } + }, + "foreignKeys": { + "tickets_threads_fk": { + "name": "tickets_threads_fk", + "tableFrom": "ticketsThreads", + "tableTo": "ticketThreadsCategories", + "columnsFrom": [ + "guildId", + "categoryId" + ], + "columnsTo": [ + "guildId", + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ticketsThreads_threadId": { + "name": "ticketsThreads_threadId", + "columns": [ + "threadId" + ] + } + }, + "uniqueConstraints": {} + }, + "userForumsConfigurations": { + "name": "userForumsConfigurations", + "columns": { + "channelId": { + "name": "channelId", + "type": "bigint unsigned", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "guildId": { + "name": "guildId", + "type": "bigint unsigned", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "managers": { + "name": "managers", + "type": "json", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('[]')" + }, + "openingMessageTitle": { + "name": "openingMessageTitle", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "openingMessageDescription": { + "name": "openingMessageDescription", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "guildId_index": { + "name": "guildId_index", + "columns": [ + "guildId" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "userForumsConfigurations_channelId": { + "name": "userForumsConfigurations_channelId", + "columns": [ + "channelId" + ] + } + }, + "uniqueConstraints": {} + }, + "welcomeAndFarewell": { + "name": "welcomeAndFarewell", + "columns": { + "guildId": { + "name": "guildId", + "type": "bigint unsigned", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "welcomeChannelId": { + "name": "welcomeChannelId", + "type": "bigint unsigned", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "welcomeMessageTitle": { + "name": "welcomeMessageTitle", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "welcomeMessageDescription": { + "name": "welcomeMessageDescription", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "welcomeNewMemberRoles": { + "name": "welcomeNewMemberRoles", + "type": "json", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('[]')" + }, + "welcomeEnabled": { + "name": "welcomeEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "farewellChannelId": { + "name": "farewellChannelId", + "type": "bigint unsigned", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "farewellMessageTitle": { + "name": "farewellMessageTitle", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "farewellMessageDescription": { + "name": "farewellMessageDescription", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "farewellEnabled": { + "name": "farewellEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "welcomeAndFarewell_guildId": { + "name": "welcomeAndFarewell_guildId", + "columns": [ + "guildId" + ] + } + }, + "uniqueConstraints": { + "welcomeAndFarewell_welcomeChannelId_unique": { + "name": "welcomeAndFarewell_welcomeChannelId_unique", + "columns": [ + "welcomeChannelId" + ] + }, + "welcomeAndFarewell_farewellChannelId_unique": { + "name": "welcomeAndFarewell_farewellChannelId_unique", + "columns": [ + "farewellChannelId" + ] + } + } + } + }, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/packages/database/migrations/meta/_journal.json b/packages/database/migrations/meta/_journal.json index 245b93b..d074de4 100644 --- a/packages/database/migrations/meta/_journal.json +++ b/packages/database/migrations/meta/_journal.json @@ -29,6 +29,13 @@ "when": 1725118814651, "tag": "0003_white_jocasta", "breakpoints": true + }, + { + "idx": 4, + "version": "5", + "when": 1725287274068, + "tag": "0004_outgoing_titania", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/database/src/index.ts b/packages/database/src/index.ts index 835786e..5bea475 100644 --- a/packages/database/src/index.ts +++ b/packages/database/src/index.ts @@ -22,3 +22,4 @@ export function migrate() { export * from 'drizzle-orm'; export { type MySqlSelect, unionAll } from 'drizzle-orm/mysql-core'; export * from './schema'; +export { ThreadTicketActionsPermissionBitField } from './utils/ThreadTicketActionsPermissionBitField'; diff --git a/packages/database/src/schema.ts b/packages/database/src/schema.ts index 80e1135..f647fb3 100644 --- a/packages/database/src/schema.ts +++ b/packages/database/src/schema.ts @@ -37,7 +37,8 @@ export const ticketThreadsCategories = mysqlTable( { id: int('id', { unsigned: true }).autoincrement().primaryKey(), guildId: snowflake('guildId').notNull(), - // This is not a char because one emoji can compose of several like 👨‍👩‍👦‍👦. + allowedAuthorActions: int('allowedAuthorActions', { unsigned: true }), + // This is not a char because one emoji can compose of several emojis (e.g. 👨‍👩‍👦‍👦). categoryEmoji: varchar('categoryEmoji', { length: 8 }), categoryTitle: varchar('categoryTitle', { length: 100 }).notNull(), categoryDescription: varchar('categoryDescription', { length: 100 }).notNull(), @@ -46,9 +47,9 @@ export const ticketThreadsCategories = mysqlTable( ...baseTicketConfiguration, privateThreads: boolean('privateThreads').notNull().default(true), silentPings: boolean('silentPings').notNull().default(true), + skipModal: boolean('skipModal').notNull().default(false), threadNotifications: boolean('threadNotifications').notNull().default(false), titleAndDescriptionRequired: boolean('titleAndDescriptionRequired').notNull().default(true), - skipModal: boolean('skipModal').notNull().default(false), }, (table) => ({ guildIdIndex: index('guildId_index').on(table.guildId), diff --git a/packages/database/src/utils/ThreadTicketActionsPermissionBitField.ts b/packages/database/src/utils/ThreadTicketActionsPermissionBitField.ts new file mode 100644 index 0000000..233a0cc --- /dev/null +++ b/packages/database/src/utils/ThreadTicketActionsPermissionBitField.ts @@ -0,0 +1,44 @@ +import { and, eq } from 'drizzle-orm'; +import type { DiscordSnowflake } from '.'; +import { database } from '..'; +import { ticketThreadsCategories } from '../schema'; + +export class ThreadTicketActionsPermissionBitField { + public static Flags = { + // key: 1 << n (n=0 at initial) + RenameTitle: 1, + Lock: 2, + Close: 4, + LockAndClose: 8, + Delete: 16, + } as const; + + public static All = Object.values(this.Flags).reduce((permissionBit, accumulator) => permissionBit | accumulator, 0); + + public static Default = this.All; + + private bitfield: number; + + public constructor(bitfield?: typeof ThreadTicketActionsPermissionBitField.Default | null) { + this.bitfield = bitfield ?? ThreadTicketActionsPermissionBitField.Default; + } + + public has(bit: number) { + return (this.bitfield & bit) === bit; + } + + public toggle(bit: number) { + this.bitfield ^= bit; + return this.has(bit); + } + + public updateAuthorPermissions( + categoryId: typeof ticketThreadsCategories.$inferSelect.id, + guildId: DiscordSnowflake, + ) { + return database + .update(ticketThreadsCategories) + .set({ allowedAuthorActions: this.bitfield }) + .where(and(eq(ticketThreadsCategories.id, categoryId), eq(ticketThreadsCategories.guildId, guildId))); + } +} diff --git a/packages/database/src/utils.ts b/packages/database/src/utils/index.ts similarity index 88% rename from packages/database/src/utils.ts rename to packages/database/src/utils/index.ts index 86c5dae..c14b49d 100644 --- a/packages/database/src/utils.ts +++ b/packages/database/src/utils/index.ts @@ -4,15 +4,14 @@ import { z } from 'zod'; // TODO: Change mode to 'string' when available: https://github.com/drizzle-team/drizzle-orm/issues/813 // const snowflake = (name: string) => bigint(name, { mode: 'bigint', unsigned: true }); -export const snowflake = customType<{ data: string }>({ +export type DiscordSnowflake = string; + +export const snowflake = customType<{ data: DiscordSnowflake }>({ dataType() { return 'bigint unsigned'; }, - // eslint-disable-next-line unicorn/prefer-native-coercion-functions - fromDriver(value: unknown) { - return String(value); - }, - toDriver(value: string) { + fromDriver: String, + toDriver(value: DiscordSnowflake) { return value; }, }); diff --git a/packages/djs-framework/package.json b/packages/djs-framework/package.json index dc197ef..65cc98e 100644 --- a/packages/djs-framework/package.json +++ b/packages/djs-framework/package.json @@ -17,7 +17,7 @@ }, "devDependencies": { "@ticketer/eslint-config": "workspace:*", - "@types/node": "^22.5.1", + "@types/node": "^22.5.2", "tsx": "^4.19.0" } } \ No newline at end of file diff --git a/packages/env/package.json b/packages/env/package.json index 12e75fc..fcc9903 100644 --- a/packages/env/package.json +++ b/packages/env/package.json @@ -16,6 +16,6 @@ }, "devDependencies": { "@ticketer/eslint-config": "workspace:*", - "@types/node": "^22.5.1" + "@types/node": "^22.5.2" } } \ No newline at end of file diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index 4c3cc9f..7183fc6 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -15,6 +15,6 @@ "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-react": "^7.35.0", "eslint-plugin-unicorn": "^55.0.0", - "typescript-eslint": "^8.3.0" + "typescript-eslint": "^8.4.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 045ac73..e95ea91 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -55,8 +55,8 @@ importers: specifier: workspace:* version: link:../../packages/eslint-config '@types/node': - specifier: ^22.5.1 - version: 22.5.1 + specifier: ^22.5.2 + version: 22.5.2 apps/website: dependencies: @@ -91,8 +91,8 @@ importers: specifier: ^2.1.1 version: 2.1.1 lucide-react: - specifier: ^0.437.0 - version: 0.437.0(react@18.3.1) + specifier: ^0.438.0 + version: 0.438.0(react@18.3.1) next: specifier: 14.2.7 version: 14.2.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -125,8 +125,8 @@ importers: specifier: workspace:* version: link:../../packages/eslint-config '@types/node': - specifier: ^22.5.1 - version: 22.5.1 + specifier: ^22.5.2 + version: 22.5.2 '@types/react': specifier: ^18.3.5 version: 18.3.5 @@ -135,10 +135,10 @@ importers: version: 18.3.0 autoprefixer: specifier: ^10.4.20 - version: 10.4.20(postcss@8.4.41) + version: 10.4.20(postcss@8.4.44) postcss: - specifier: ^8.4.41 - version: 8.4.41 + specifier: ^8.4.44 + version: 8.4.44 tailwindcss: specifier: ^3.4.10 version: 3.4.10 @@ -184,8 +184,8 @@ importers: specifier: workspace:* version: link:../eslint-config '@types/node': - specifier: ^22.5.1 - version: 22.5.1 + specifier: ^22.5.2 + version: 22.5.2 tsx: specifier: ^4.19.0 version: 4.19.0 @@ -203,8 +203,8 @@ importers: specifier: workspace:* version: link:../eslint-config '@types/node': - specifier: ^22.5.1 - version: 22.5.1 + specifier: ^22.5.2 + version: 22.5.2 packages/eslint-config: devDependencies: @@ -242,8 +242,8 @@ importers: specifier: ^55.0.0 version: 55.0.0(eslint@9.9.1(jiti@1.21.6)) typescript-eslint: - specifier: ^8.3.0 - version: 8.3.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4) + specifier: ^8.4.0 + version: 8.4.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4) packages: @@ -1249,8 +1249,8 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/node@22.5.1': - resolution: {integrity: sha512-KkHsxej0j9IW1KKOOAA/XBA0z08UFSrRQHErzEfA3Vgq57eXIMYboIlHJuYIfd+lwCQjtKqUu3UnmKbtUc9yRw==} + '@types/node@22.5.2': + resolution: {integrity: sha512-acJsPTEqYqulZS/Yp/S3GgeE6GZ0qYODUR8aVr/DkhHQ8l9nd4j5x1/ZJy9/gHrRlFMqkO6i0I3E27Alu4jjPg==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -1267,8 +1267,8 @@ packages: '@types/ws@8.5.12': resolution: {integrity: sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==} - '@typescript-eslint/eslint-plugin@8.3.0': - resolution: {integrity: sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==} + '@typescript-eslint/eslint-plugin@8.4.0': + resolution: {integrity: sha512-rg8LGdv7ri3oAlenMACk9e+AR4wUV0yrrG+XKsGKOK0EVgeEDqurkXMPILG2836fW4ibokTB5v4b6Z9+GYQDEw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 @@ -1288,8 +1288,8 @@ packages: typescript: optional: true - '@typescript-eslint/parser@8.3.0': - resolution: {integrity: sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ==} + '@typescript-eslint/parser@8.4.0': + resolution: {integrity: sha512-NHgWmKSgJk5K9N16GIhQ4jSobBoJwrmURaLErad0qlLjrpP5bECYg+wxVTGlGZmJbU03jj/dfnb6V9bw+5icsA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -1302,12 +1302,12 @@ packages: resolution: {integrity: sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg==} engines: {node: ^16.0.0 || >=18.0.0} - '@typescript-eslint/scope-manager@8.3.0': - resolution: {integrity: sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg==} + '@typescript-eslint/scope-manager@8.4.0': + resolution: {integrity: sha512-n2jFxLeY0JmKfUqy3P70rs6vdoPjHK8P/w+zJcV3fk0b0BwRXC/zxRTEnAsgYT7MwdQDt/ZEbtdzdVC+hcpF0A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/type-utils@8.3.0': - resolution: {integrity: sha512-wrV6qh//nLbfXZQoj32EXKmwHf4b7L+xXLrP3FZ0GOUU72gSvLjeWUl5J5Ue5IwRxIV1TfF73j/eaBapxx99Lg==} + '@typescript-eslint/type-utils@8.4.0': + resolution: {integrity: sha512-pu2PAmNrl9KX6TtirVOrbLPLwDmASpZhK/XU7WvoKoCUkdtq9zF7qQ7gna0GBZFN0hci0vHaSusiL2WpsQk37A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '*' @@ -1319,8 +1319,8 @@ packages: resolution: {integrity: sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA==} engines: {node: ^16.0.0 || >=18.0.0} - '@typescript-eslint/types@8.3.0': - resolution: {integrity: sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==} + '@typescript-eslint/types@8.4.0': + resolution: {integrity: sha512-T1RB3KQdskh9t3v/qv7niK6P8yvn7ja1mS7QK7XfRVL6wtZ8/mFs/FHf4fKvTA0rKnqnYxl/uHFNbnEt0phgbw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript-eslint/typescript-estree@7.2.0': @@ -1332,8 +1332,8 @@ packages: typescript: optional: true - '@typescript-eslint/typescript-estree@8.3.0': - resolution: {integrity: sha512-Mq7FTHl0R36EmWlCJWojIC1qn/ZWo2YiWYc1XVtasJ7FIgjo0MVv9rZWXEE7IK2CGrtwe1dVOxWwqXUdNgfRCA==} + '@typescript-eslint/typescript-estree@8.4.0': + resolution: {integrity: sha512-kJ2OIP4dQw5gdI4uXsaxUZHRwWAGpREJ9Zq6D5L0BweyOrWsL6Sz0YcAZGWhvKnH7fm1J5YFE1JrQL0c9dd53A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '*' @@ -1341,8 +1341,8 @@ packages: typescript: optional: true - '@typescript-eslint/utils@8.3.0': - resolution: {integrity: sha512-F77WwqxIi/qGkIGOGXNBLV7nykwfjLsdauRB/DOFPdv6LTF3BHHkBpq81/b5iMPSF055oO2BiivDJV4ChvNtXA==} + '@typescript-eslint/utils@8.4.0': + resolution: {integrity: sha512-swULW8n1IKLjRAgciCkTCafyTHHfwVQFt8DovmaF69sKbOxTSFMmIZaSHjqO9i/RV0wIblaawhzvtva8Nmm7lQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -1351,8 +1351,8 @@ packages: resolution: {integrity: sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==} engines: {node: ^16.0.0 || >=18.0.0} - '@typescript-eslint/visitor-keys@8.3.0': - resolution: {integrity: sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA==} + '@typescript-eslint/visitor-keys@8.4.0': + resolution: {integrity: sha512-zTQD6WLNTre1hj5wp09nBIDiOc2U5r/qmzo7wxPn4ZgAjHql09EofqhF9WF+fZHzL5aCyaIpPcT2hyxl73kr9A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@vercel/analytics@1.3.1': @@ -2477,8 +2477,8 @@ packages: resolution: {integrity: sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==} engines: {node: '>=16.14'} - lucide-react@0.437.0: - resolution: {integrity: sha512-RXQq6tnm1FlXDUtOwLaoXET2TOEGpQULrQlPOjGHgIVsPhicHNat9sWF33OAe2UCLMFiWF1oL+FtAg43BqVY4Q==} + lucide-react@0.438.0: + resolution: {integrity: sha512-uq6yCB+IzVfgIPMK8ibkecXSWTTSOMs9UjUgZigfrDCVqgdwkpIgYg1fSYnf0XXF2AoSyCJZhoZXQwzoai7VGw==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc @@ -2736,8 +2736,8 @@ packages: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} - postcss@8.4.41: - resolution: {integrity: sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==} + postcss@8.4.44: + resolution: {integrity: sha512-Aweb9unOEpQ3ezu4Q00DPvvM2ZTUitJdNKeP/+uQgr1IBIqu574IaZoURId7BKtWMREwzKa9OgzPzezWGPWFQw==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: @@ -3217,8 +3217,8 @@ packages: peerDependencies: typescript: '>=3.5.1' - typescript-eslint@8.3.0: - resolution: {integrity: sha512-EvWjwWLwwKDIJuBjk2I6UkV8KEQcwZ0VM10nR1rIunRDIP67QJTZAHBXTX0HW/oI1H10YESF8yWie8fRQxjvFA==} + typescript-eslint@8.4.0: + resolution: {integrity: sha512-67qoc3zQZe3CAkO0ua17+7aCLI0dU+sSQd1eKPGq06QE4rfQjstVXR6woHO5qQvGUa550NfGckT4tzh3b3c8Pw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '*' @@ -4120,7 +4120,7 @@ snapshots: '@types/json5@0.0.29': {} - '@types/node@22.5.1': + '@types/node@22.5.2': dependencies: undici-types: 6.19.8 @@ -4139,16 +4139,16 @@ snapshots: '@types/ws@8.5.12': dependencies: - '@types/node': 22.5.1 + '@types/node': 22.5.2 - '@typescript-eslint/eslint-plugin@8.3.0(@typescript-eslint/parser@8.3.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4))(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4)': + '@typescript-eslint/eslint-plugin@8.4.0(@typescript-eslint/parser@8.4.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4))(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4)': dependencies: '@eslint-community/regexpp': 4.11.0 - '@typescript-eslint/parser': 8.3.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4) - '@typescript-eslint/scope-manager': 8.3.0 - '@typescript-eslint/type-utils': 8.3.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4) - '@typescript-eslint/utils': 8.3.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4) - '@typescript-eslint/visitor-keys': 8.3.0 + '@typescript-eslint/parser': 8.4.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4) + '@typescript-eslint/scope-manager': 8.4.0 + '@typescript-eslint/type-utils': 8.4.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4) + '@typescript-eslint/utils': 8.4.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 8.4.0 eslint: 9.9.1(jiti@1.21.6) graphemer: 1.4.0 ignore: 5.3.2 @@ -4172,12 +4172,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.3.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4)': + '@typescript-eslint/parser@8.4.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4)': dependencies: - '@typescript-eslint/scope-manager': 8.3.0 - '@typescript-eslint/types': 8.3.0 - '@typescript-eslint/typescript-estree': 8.3.0(typescript@5.5.4) - '@typescript-eslint/visitor-keys': 8.3.0 + '@typescript-eslint/scope-manager': 8.4.0 + '@typescript-eslint/types': 8.4.0 + '@typescript-eslint/typescript-estree': 8.4.0(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 8.4.0 debug: 4.3.6 eslint: 9.9.1(jiti@1.21.6) optionalDependencies: @@ -4190,15 +4190,15 @@ snapshots: '@typescript-eslint/types': 7.2.0 '@typescript-eslint/visitor-keys': 7.2.0 - '@typescript-eslint/scope-manager@8.3.0': + '@typescript-eslint/scope-manager@8.4.0': dependencies: - '@typescript-eslint/types': 8.3.0 - '@typescript-eslint/visitor-keys': 8.3.0 + '@typescript-eslint/types': 8.4.0 + '@typescript-eslint/visitor-keys': 8.4.0 - '@typescript-eslint/type-utils@8.3.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4)': + '@typescript-eslint/type-utils@8.4.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4)': dependencies: - '@typescript-eslint/typescript-estree': 8.3.0(typescript@5.5.4) - '@typescript-eslint/utils': 8.3.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4) + '@typescript-eslint/typescript-estree': 8.4.0(typescript@5.5.4) + '@typescript-eslint/utils': 8.4.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4) debug: 4.3.6 ts-api-utils: 1.3.0(typescript@5.5.4) optionalDependencies: @@ -4209,7 +4209,7 @@ snapshots: '@typescript-eslint/types@7.2.0': {} - '@typescript-eslint/types@8.3.0': {} + '@typescript-eslint/types@8.4.0': {} '@typescript-eslint/typescript-estree@7.2.0(typescript@5.5.4)': dependencies: @@ -4226,10 +4226,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.3.0(typescript@5.5.4)': + '@typescript-eslint/typescript-estree@8.4.0(typescript@5.5.4)': dependencies: - '@typescript-eslint/types': 8.3.0 - '@typescript-eslint/visitor-keys': 8.3.0 + '@typescript-eslint/types': 8.4.0 + '@typescript-eslint/visitor-keys': 8.4.0 debug: 4.3.6 fast-glob: 3.3.2 is-glob: 4.0.3 @@ -4241,12 +4241,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.3.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4)': + '@typescript-eslint/utils@8.4.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4)': dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@9.9.1(jiti@1.21.6)) - '@typescript-eslint/scope-manager': 8.3.0 - '@typescript-eslint/types': 8.3.0 - '@typescript-eslint/typescript-estree': 8.3.0(typescript@5.5.4) + '@typescript-eslint/scope-manager': 8.4.0 + '@typescript-eslint/types': 8.4.0 + '@typescript-eslint/typescript-estree': 8.4.0(typescript@5.5.4) eslint: 9.9.1(jiti@1.21.6) transitivePeerDependencies: - supports-color @@ -4257,9 +4257,9 @@ snapshots: '@typescript-eslint/types': 7.2.0 eslint-visitor-keys: 3.4.3 - '@typescript-eslint/visitor-keys@8.3.0': + '@typescript-eslint/visitor-keys@8.4.0': dependencies: - '@typescript-eslint/types': 8.3.0 + '@typescript-eslint/types': 8.4.0 eslint-visitor-keys: 3.4.3 '@vercel/analytics@1.3.1(next@14.2.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': @@ -4391,14 +4391,14 @@ snapshots: ast-types-flow@0.0.8: {} - autoprefixer@10.4.20(postcss@8.4.41): + autoprefixer@10.4.20(postcss@8.4.44): dependencies: browserslist: 4.23.3 caniuse-lite: 1.0.30001655 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.0.1 - postcss: 8.4.41 + postcss: 8.4.44 postcss-value-parser: 4.2.0 available-typed-arrays@1.0.7: @@ -4871,7 +4871,7 @@ snapshots: eslint: 9.9.1(jiti@1.21.6) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.2.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@9.9.1(jiti@1.21.6)) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@8.3.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4))(eslint@9.9.1(jiti@1.21.6)) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@8.4.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4))(eslint@9.9.1(jiti@1.21.6)) eslint-plugin-jsx-a11y: 6.9.0(eslint@9.9.1(jiti@1.21.6)) eslint-plugin-react: 7.35.0(eslint@9.9.1(jiti@1.21.6)) eslint-plugin-react-hooks: 4.6.2(eslint@9.9.1(jiti@1.21.6)) @@ -4911,7 +4911,7 @@ snapshots: is-bun-module: 1.1.0 is-glob: 4.0.3 optionalDependencies: - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@8.3.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4))(eslint@9.9.1(jiti@1.21.6)) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@8.4.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4))(eslint@9.9.1(jiti@1.21.6)) transitivePeerDependencies: - '@typescript-eslint/parser' - eslint-import-resolver-node @@ -4929,11 +4929,11 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.8.2(@typescript-eslint/parser@8.3.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint@9.9.1(jiti@1.21.6)): + eslint-module-utils@2.8.2(@typescript-eslint/parser@8.4.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint@9.9.1(jiti@1.21.6)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.3.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4) + '@typescript-eslint/parser': 8.4.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4) eslint: 9.9.1(jiti@1.21.6) eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: @@ -4943,7 +4943,7 @@ snapshots: dependencies: eslint: 9.9.1(jiti@1.21.6) - eslint-plugin-import@2.29.1(@typescript-eslint/parser@8.3.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4))(eslint@9.9.1(jiti@1.21.6)): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@8.4.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4))(eslint@9.9.1(jiti@1.21.6)): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 @@ -4953,7 +4953,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.9.1(jiti@1.21.6) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.2(@typescript-eslint/parser@8.3.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint@9.9.1(jiti@1.21.6)) + eslint-module-utils: 2.8.2(@typescript-eslint/parser@8.4.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint@9.9.1(jiti@1.21.6)) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -4964,7 +4964,7 @@ snapshots: semver: 6.3.1 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.3.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4) + '@typescript-eslint/parser': 8.4.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -5530,7 +5530,7 @@ snapshots: lru-cache@8.0.5: {} - lucide-react@0.437.0(react@18.3.1): + lucide-react@0.438.0(react@18.3.1): dependencies: react: 18.3.1 @@ -5744,28 +5744,28 @@ snapshots: possible-typed-array-names@1.0.0: {} - postcss-import@15.1.0(postcss@8.4.41): + postcss-import@15.1.0(postcss@8.4.44): dependencies: - postcss: 8.4.41 + postcss: 8.4.44 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.8 - postcss-js@4.0.1(postcss@8.4.41): + postcss-js@4.0.1(postcss@8.4.44): dependencies: camelcase-css: 2.0.1 - postcss: 8.4.41 + postcss: 8.4.44 - postcss-load-config@4.0.2(postcss@8.4.41): + postcss-load-config@4.0.2(postcss@8.4.44): dependencies: lilconfig: 3.1.2 yaml: 2.5.0 optionalDependencies: - postcss: 8.4.41 + postcss: 8.4.44 - postcss-nested@6.2.0(postcss@8.4.41): + postcss-nested@6.2.0(postcss@8.4.44): dependencies: - postcss: 8.4.41 + postcss: 8.4.44 postcss-selector-parser: 6.1.2 postcss-selector-parser@6.1.2: @@ -5781,7 +5781,7 @@ snapshots: picocolors: 1.0.1 source-map-js: 1.2.0 - postcss@8.4.41: + postcss@8.4.44: dependencies: nanoid: 3.3.7 picocolors: 1.0.1 @@ -6132,11 +6132,11 @@ snapshots: normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.0.1 - postcss: 8.4.41 - postcss-import: 15.1.0(postcss@8.4.41) - postcss-js: 4.0.1(postcss@8.4.41) - postcss-load-config: 4.0.2(postcss@8.4.41) - postcss-nested: 6.2.0(postcss@8.4.41) + postcss: 8.4.44 + postcss-import: 15.1.0(postcss@8.4.44) + postcss-js: 4.0.1(postcss@8.4.44) + postcss-load-config: 4.0.2(postcss@8.4.44) + postcss-nested: 6.2.0(postcss@8.4.44) postcss-selector-parser: 6.1.2 resolve: 1.22.8 sucrase: 3.35.0 @@ -6256,11 +6256,11 @@ snapshots: dependencies: typescript: 5.5.4 - typescript-eslint@8.3.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4): + typescript-eslint@8.4.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4): dependencies: - '@typescript-eslint/eslint-plugin': 8.3.0(@typescript-eslint/parser@8.3.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4))(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4) - '@typescript-eslint/parser': 8.3.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4) - '@typescript-eslint/utils': 8.3.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4) + '@typescript-eslint/eslint-plugin': 8.4.0(@typescript-eslint/parser@8.4.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4))(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4) + '@typescript-eslint/parser': 8.4.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4) + '@typescript-eslint/utils': 8.4.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.5.4) optionalDependencies: typescript: 5.5.4 transitivePeerDependencies: