diff --git a/.changeset/many-zoos-admire.md b/.changeset/many-zoos-admire.md new file mode 100644 index 00000000..38fa56a1 --- /dev/null +++ b/.changeset/many-zoos-admire.md @@ -0,0 +1,5 @@ +--- +"dynamica-v2": minor +--- + +Pinned Channels diff --git a/drizzle/0002_cool_titanium_man.sql b/drizzle/0002_cool_titanium_man.sql new file mode 100644 index 00000000..240c4021 --- /dev/null +++ b/drizzle/0002_cool_titanium_man.sql @@ -0,0 +1 @@ +ALTER TABLE "Secondary" ADD COLUMN "pinned" boolean DEFAULT false NOT NULL; \ No newline at end of file diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json new file mode 100644 index 00000000..ea632326 --- /dev/null +++ b/drizzle/meta/0002_snapshot.json @@ -0,0 +1,430 @@ +{ + "id": "593fe56a-6d8b-481d-ba77-d2a86d448f37", + "prevId": "b4cf4bf1-39d1-4a6b-ab67-6b8f4decc3dd", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.Alias": { + "name": "Alias", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "activity": { + "name": "activity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "alias": { + "name": "alias", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "guildId": { + "name": "guildId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "Alias_guildId_activity_key": { + "name": "Alias_guildId_activity_key", + "columns": [ + { + "expression": "guildId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "activity", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Alias_guildId_Guild_id_fk": { + "name": "Alias_guildId_Guild_id_fk", + "tableFrom": "Alias", + "tableTo": "Guild", + "columnsFrom": [ + "guildId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "Alias_guild_fkey": { + "name": "Alias_guild_fkey", + "tableFrom": "Alias", + "tableTo": "Guild", + "columnsFrom": [ + "guildId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.Guild": { + "name": "Guild", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "allowJoinRequests": { + "name": "allowJoinRequests", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.Migrated": { + "name": "Migrated", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.Primary": { + "name": "Primary", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "creator": { + "name": "creator", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template": { + "name": "template", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generalName": { + "name": "generalName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "guildId": { + "name": "guildId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "Primary_guildId_id_key": { + "name": "Primary_guildId_id_key", + "columns": [ + { + "expression": "guildId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Primary_guildId_Guild_id_fk": { + "name": "Primary_guildId_Guild_id_fk", + "tableFrom": "Primary", + "tableTo": "Guild", + "columnsFrom": [ + "guildId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "Primary_guild_fkey": { + "name": "Primary_guild_fkey", + "tableFrom": "Primary", + "tableTo": "Guild", + "columnsFrom": [ + "guildId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.Secondary": { + "name": "Secondary", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "creator": { + "name": "creator", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emoji": { + "name": "emoji", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "guildId": { + "name": "guildId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "primaryId": { + "name": "primaryId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "lastName": { + "name": "lastName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "Secondary_guildId_id_key": { + "name": "Secondary_guildId_id_key", + "columns": [ + { + "expression": "guildId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Secondary_guildId_Guild_id_fk": { + "name": "Secondary_guildId_Guild_id_fk", + "tableFrom": "Secondary", + "tableTo": "Guild", + "columnsFrom": [ + "guildId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "Secondary_primaryId_Primary_id_fk": { + "name": "Secondary_primaryId_Primary_id_fk", + "tableFrom": "Secondary", + "tableTo": "Primary", + "columnsFrom": [ + "primaryId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "Secondary_guild_fkey": { + "name": "Secondary_guild_fkey", + "tableFrom": "Secondary", + "tableTo": "Guild", + "columnsFrom": [ + "guildId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Secondary_primary_fkey": { + "name": "Secondary_primary_fkey", + "tableFrom": "Secondary", + "tableTo": "Primary", + "columnsFrom": [ + "primaryId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 7c42a0e7..ef661158 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1724905859884, "tag": "0001_grey_unicorn", "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1724917866926, + "tag": "0002_cool_titanium_man", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/features/drizzle/schema.ts b/src/features/drizzle/schema.ts index 900ffda3..d0f181da 100644 --- a/src/features/drizzle/schema.ts +++ b/src/features/drizzle/schema.ts @@ -80,6 +80,7 @@ export const secondaryTable = pgTable( .notNull() .defaultNow(), lastName: text("lastName").notNull(), + pinned: boolean("pinned").notNull().default(false), }, (secondaryTable) => ({ Secondary_guild_fkey: foreignKey({ diff --git a/src/features/secondary/dto/PinDto.ts b/src/features/secondary/dto/PinDto.ts new file mode 100644 index 00000000..e5f56131 --- /dev/null +++ b/src/features/secondary/dto/PinDto.ts @@ -0,0 +1,11 @@ +import { StringOption } from "necord"; + +export class PinDto { + @StringOption({ + name: "secondary", + description: "The secondary channel to pin", + required: true, + autocomplete: true, + }) + secondary: string; +} diff --git a/src/features/secondary/dto/UnpinDto.ts b/src/features/secondary/dto/UnpinDto.ts new file mode 100644 index 00000000..907d6bcb --- /dev/null +++ b/src/features/secondary/dto/UnpinDto.ts @@ -0,0 +1,11 @@ +import { StringOption } from "necord"; + +export class UnpinDto { + @StringOption({ + name: "secondary", + description: "The secondary channel to unpin", + required: true, + autocomplete: true, + }) + secondary: string; +} diff --git a/src/features/secondary/secondary.buttons.ts b/src/features/secondary/secondary.buttons.ts index d0dcf3e0..132247ed 100644 --- a/src/features/secondary/secondary.buttons.ts +++ b/src/features/secondary/secondary.buttons.ts @@ -36,7 +36,7 @@ export class SecondaryButtons { ); return interaction.update({ - components: [messageComponents], + components: messageComponents, }); } catch (error) { const errorEmbed = createErrorEmbed(error.message); @@ -68,7 +68,7 @@ export class SecondaryButtons { id, ); return interaction.update({ - components: [messageComponents], + components: messageComponents, }); } catch (error) { const errorEmbed = createErrorEmbed(error.message); @@ -282,4 +282,76 @@ export class SecondaryButtons { }); } } + + @Button("secondary/buttons/pin/:channelId") + public async onPin( + @Context() [interaction]: ButtonContext, + @ComponentParam("channelId") channelId: string, + ) { + const guildId = interaction.guildId; + if (!guildId) { + return interaction.reply({ + content: "This command can only be used in a guild", + ephemeral: true, + }); + } + try { + await this.secondaryService.pin(guildId, channelId, interaction.user.id); + + const messageComponents = + await this.secondaryService.createSecondarySettingsComponents( + guildId, + channelId, + ); + + return interaction.update({ + components: messageComponents, + }); + } catch (error) { + const errorEmbed = createErrorEmbed(error.message); + + return interaction.reply({ + embeds: [errorEmbed], + ephemeral: true, + }); + } + } + + @Button("secondary/buttons/unpin/:channelId") + public async onUnpin( + @Context() [interaction]: ButtonContext, + @ComponentParam("channelId") channelId: string, + ) { + const guildId = interaction.guildId; + if (!guildId) { + return interaction.reply({ + content: "This command can only be used in a guild", + ephemeral: true, + }); + } + try { + await this.secondaryService.unpin( + guildId, + channelId, + interaction.user.id, + ); + + const messageComponents = + await this.secondaryService.createSecondarySettingsComponents( + guildId, + channelId, + ); + + return interaction.update({ + components: messageComponents, + }); + } catch (error) { + const errorEmbed = createErrorEmbed(error.message); + + return interaction.reply({ + embeds: [errorEmbed], + ephemeral: true, + }); + } + } } diff --git a/src/features/secondary/secondary.commands.ts b/src/features/secondary/secondary.commands.ts index 32856237..f7de3fd5 100644 --- a/src/features/secondary/secondary.commands.ts +++ b/src/features/secondary/secondary.commands.ts @@ -24,6 +24,8 @@ import type { TransferDto } from "./dto/TransferDto"; import type { UnlockDto } from "./dto/UnlockDto"; import { SecondaryAutocompleteInterceptor } from "./interceptors/secondary.interceptor"; import { SecondaryService } from "./secondary.service"; +import { PinDto } from "./dto/PinDto"; +import { UnpinDto } from "./dto/UnpinDto"; @UseInterceptors(SecondaryAutocompleteInterceptor) @Injectable() @@ -337,4 +339,80 @@ export class SecondaryCommands { }); } } + + @UseInterceptors(SecondaryAutocompleteInterceptor) + @SlashCommand({ + name: "pin", + description: "Pin the channel so it doesn't get deleted", + dmPermission: false, + }) + async onPin( + @Context() [interaction]: SlashCommandContext, + @Options() { secondary }: PinDto, + ) { + const guildId = interaction.guildId; + if (!guildId) { + return interaction.reply({ + content: "This command can only be used in a guild", + ephemeral: true, + }); + } + + try { + const newChannel = await this.secondaryService.pin( + guildId, + secondary, + interaction.user.id, + ); + return interaction.reply({ + ephemeral: true, + content: `Channel Pinned: ${channelMention(newChannel.id)}`, + }); + } catch (error) { + const errorEmbed = createErrorEmbed(error.message); + + return interaction.reply({ + embeds: [errorEmbed], + ephemeral: true, + }); + } + } + + @UseInterceptors(SecondaryAutocompleteInterceptor) + @SlashCommand({ + name: "unpin", + description: "Unpin the channel", + dmPermission: false, + }) + async onUnpin( + @Context() [interaction]: SlashCommandContext, + @Options() { secondary }: UnpinDto, + ) { + const guildId = interaction.guildId; + if (!guildId) { + return interaction.reply({ + content: "This command can only be used in a guild", + ephemeral: true, + }); + } + + try { + const newChannel = await this.secondaryService.unpin( + guildId, + secondary, + interaction.user.id, + ); + return interaction.reply({ + ephemeral: true, + content: `Channel Unpinned: ${channelMention(newChannel.id)}`, + }); + } catch (error) { + const errorEmbed = createErrorEmbed(error.message); + + return interaction.reply({ + embeds: [errorEmbed], + ephemeral: true, + }); + } + } } diff --git a/src/features/secondary/secondary.modals.ts b/src/features/secondary/secondary.modals.ts index d9f52bc1..d5560cdd 100644 --- a/src/features/secondary/secondary.modals.ts +++ b/src/features/secondary/secondary.modals.ts @@ -32,16 +32,6 @@ export class SecondaryModals { interaction.user.id, ); - // const updatedComponents = - // await this.secondaryService.createSecondarySettingsComponents( - // interaction.guildId, - // id, - // ); - - // await interaction({ - // components: [updatedComponents], - // }); - return interaction.reply({ ephemeral: true, content: `Channel name updated to ${updatedSecondary.name}`, diff --git a/src/features/secondary/secondary.service.ts b/src/features/secondary/secondary.service.ts index 564bcb0a..c3e8f424 100644 --- a/src/features/secondary/secondary.service.ts +++ b/src/features/secondary/secondary.service.ts @@ -150,7 +150,7 @@ export class SecondaryService { try { await newDiscordChannel.send({ content: "Edit the channel settings here", - components: [channelSettingsComponents], + components: channelSettingsComponents, }); } catch (error) { if (error instanceof DiscordAPIError) { @@ -275,7 +275,11 @@ export class SecondaryService { return; } - if (discordChannel.members.size === 0 && discordChannel.manageable) { + if ( + discordChannel.members.size === 0 && + discordChannel.manageable && + !secondaryChannel.pinned + ) { await discordChannel.delete(); } else { const memberIds = discordChannel.members.map((member) => member.id); @@ -287,7 +291,7 @@ export class SecondaryService { await this.db .update(secondaryTable) .set({ - creator: memberIds[0], + creator: memberIds[0] ?? null, }) .where( and( @@ -961,6 +965,50 @@ export class SecondaryService { return discordChannel; } + public async pin(guildId: string, channelId: string, userId: string) { + const { discordChannel } = await this.checkChannelControl( + guildId, + channelId, + userId, + ); + + await this.db + .update(secondaryTable) + .set({ + pinned: true, + }) + .where( + and( + eq(secondaryTable.id, channelId), + eq(secondaryTable.guildId, guildId), + ), + ); + + return discordChannel; + } + + public async unpin(guildId: string, channelId: string, userId: string) { + const { discordChannel } = await this.checkChannelControl( + guildId, + channelId, + userId, + ); + + await this.db + .update(secondaryTable) + .set({ + pinned: false, + }) + .where( + and( + eq(secondaryTable.id, channelId), + eq(secondaryTable.guildId, guildId), + ), + ); + + return discordChannel; + } + /** * Create a modal to edit a secondary channel * @param guildId the guild id @@ -1036,7 +1084,7 @@ export class SecondaryService { async createSecondarySettingsComponents( guildId: string, channelId: string, - ): Promise> { + ): Promise>> { const databaseChannel = await this.db.query.secondaryTable.findFirst({ where: and( eq(secondaryTable.id, channelId), @@ -1090,15 +1138,52 @@ export class SecondaryService { !databaseChannel.guild.allowJoinRequests || !databaseChannel.locked, ); - const isLocked = databaseChannel.locked; + const pin = new ButtonBuilder() + .setCustomId(`secondary/buttons/pin/${channelId}`) + .setEmoji("📌") + .setLabel("Pin") + .setStyle(ButtonStyle.Primary); + + const unpin = new ButtonBuilder() + .setCustomId(`secondary/buttons/unpin/${channelId}`) + .setEmoji("📌") + .setLabel("Unpin") + .setStyle(ButtonStyle.Primary); - return new ActionRowBuilder().addComponents( + const buttons: Array = [ transferButton, - isLocked ? unlockButton : lockButton, settingsButton, allyourbaseButton, - requestJoin, - ); + ]; + + if (databaseChannel.guild.allowJoinRequests) { + buttons.push(requestJoin); + } + + if (!databaseChannel.locked) { + buttons.push(lockButton); + } else { + buttons.push(unlockButton); + } + + if (!databaseChannel.pinned) { + buttons.push(pin); + } else { + buttons.push(unpin); + } + + // chunk by 5 + const actionRows: Array> = []; + + for (let i = 0; i < buttons.length; i += 5) { + actionRows.push( + new ActionRowBuilder().addComponents( + ...buttons.slice(i, i + 5), + ), + ); + } + + return actionRows; } /**