From 57b23b38a715728e806234be0120aa26bf32e524 Mon Sep 17 00:00:00 2001 From: Vijay <74645268+vijaysingh2219@users.noreply.github.com> Date: Sun, 15 Sep 2024 17:19:13 +0530 Subject: [PATCH] feat: add guilds command with pagination --- .github/workflows/deploy.yml | 2 + src/commands/developer/guilds.ts | 141 ++++++++++++++++++++++ src/components/buttons/guilds-first.ts | 12 ++ src/components/buttons/guilds-last.ts | 12 ++ src/components/buttons/guilds-next.ts | 12 ++ src/components/buttons/guilds-previous.ts | 12 ++ src/events/client/interactionCreate.ts | 10 ++ src/types/command.ts | 7 +- 8 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 src/commands/developer/guilds.ts create mode 100644 src/components/buttons/guilds-first.ts create mode 100644 src/components/buttons/guilds-last.ts create mode 100644 src/components/buttons/guilds-next.ts create mode 100644 src/components/buttons/guilds-previous.ts diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3613b1b..442a9f0 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -47,6 +47,7 @@ jobs: build-args: | DISCORD_BOT_TOKEN=${{ secrets.DISCORD_BOT_TOKEN }} CLIENT_ID=${{ secrets.CLIENT_ID }} + DEVELOPER_IDS=${{ secrets.DEVELOPER_IDS }} NODE_ENV=production - name: Pull image @@ -65,5 +66,6 @@ jobs: sudo docker run -d --name discord-app -p 3005:3000 \ -e DISCORD_BOT_TOKEN=${{ secrets.DISCORD_BOT_TOKEN }} \ -e CLIENT_ID=${{ secrets.CLIENT_ID }} \ + -e DEVELOPER_IDS=${{ secrets.DEVELOPER_IDS }} \ -e NODE_ENV=production \ ${{ secrets.DOCKERHUB_USERNAME }}/cosmo-discord-bot:latest diff --git a/src/commands/developer/guilds.ts b/src/commands/developer/guilds.ts new file mode 100644 index 0000000..0ca6ce1 --- /dev/null +++ b/src/commands/developer/guilds.ts @@ -0,0 +1,141 @@ +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, Collection, ComponentType, EmbedBuilder, Guild, SlashCommandBuilder } from 'discord.js'; +import { Command } from '../../types/command'; +import Client from "../../extensions/custom-client"; +import config from '../../lib/config'; + +const ITEMS_PER_PAGE = 10; + +const command: Command = { + data: new SlashCommandBuilder() + .setName('guilds') + .setDescription('Get information about the guilds the bot is in.'), + category: 'Developer', + developerOnly: true, + async execute(interaction, client) { + // Defer the reply to fetch the message + await interaction.deferReply({ ephemeral: true }); + + // Get all the guilds the bot is in + const guilds = client.guilds.cache; + + // Calculate the total number of pages + const totalPages = Math.max(1, Math.ceil(guilds.size / ITEMS_PER_PAGE)); + + let currentPage = 1; + + // Function to update the message with the current page + const updateMessage = async (page: number) => { + // Create an embed with the guild information for the current page + const embed = createEmbed(client, guilds, page); + + // Create the buttons for pagination + const buttons = createButtons(page, totalPages); + const row = new ActionRowBuilder().addComponents(buttons); + + // Edit the reply with the updated information + return await interaction.editReply({ embeds: [embed], components: [row] }); + }; + + // Send the initial message + const message = await updateMessage(currentPage); + + // Filter for the message component collector to only collect button interactions from the user + const filter = (i: any) => i.customId.startsWith('guilds') && i.user.id === interaction.user.id; + + // Create a message component collector to listen for button interactions + const collector = message.createMessageComponentCollector({ + filter, + componentType: ComponentType.Button, + time: 60000, // Collector time in milliseconds (60 seconds) + }); + + collector.on('collect', async (i) => { + await i.deferUpdate(); + + switch (i.customId) { + case 'guilds-first': + currentPage = 1; + break; + case 'guilds-previous': + currentPage = Math.max(currentPage - 1, 1); + break; + case 'guilds-next': + currentPage = Math.min(currentPage + 1, totalPages); + break; + case 'guilds-last': + currentPage = totalPages; + break; + } + + // Update the message with the new page + await updateMessage(currentPage); + }); + + collector.on('end', async () => { + // Disable the buttons after the collector ends + const buttons = createButtons(currentPage, totalPages, true); + const row = new ActionRowBuilder().addComponents(buttons); + return await interaction.editReply({ components: [row] }); + }); + }, +}; + +// Function to create an embed with the guild information for the current page +const createEmbed = (client: Client, guilds: Collection, page: number) => { + const totalGuilds = guilds.size; + const totalUsers = guilds.reduce((acc, guild) => acc + guild.memberCount, 0); + const totalPages = Math.ceil(totalGuilds / ITEMS_PER_PAGE); + + const start = (page - 1) * ITEMS_PER_PAGE; + const end = start + ITEMS_PER_PAGE; + + // Get the guilds sorted by member count + const currentGuilds = totalGuilds === 0 + ? "No guilds available." + : [...guilds.values()] + .sort((a, b) => b.memberCount - a.memberCount) + .slice(start, end) + .map((guild: any, index: number) => { + return `**${start + index + 1}.** ${guild.iconURL() ? `[${guild.name}](${guild.iconURL()})` : guild.name} - ${guild.memberCount.toLocaleString()} members`; + }) + .join("\n"); + + // Create the embed with the guild information + return new EmbedBuilder() + .setTitle('Guilds') + .setColor(config.colors.embed) + .setDescription(`Total Guilds: ${totalGuilds}\nTotal Users: ${totalUsers}\n\n${currentGuilds}`) + .setFooter({ text: `Page ${page} of ${totalPages}`, iconURL: client.user?.displayAvatarURL() }); +} + +// Function to create the buttons for pagination +const createButtons = (page: number, totalPages: number, disable: boolean = false) => { + return [ + new ButtonBuilder() + .setCustomId('guilds-first') + .setEmoji('⏮️') + .setLabel('First') + .setStyle(ButtonStyle.Primary) + .setDisabled(page === 1 || disable), + new ButtonBuilder() + .setCustomId('guilds-previous') + .setEmoji('⬅️') + .setLabel('Previous') + .setStyle(ButtonStyle.Primary) + .setDisabled(page === 1 || disable), + new ButtonBuilder() + .setCustomId('guilds-next') + .setEmoji('➡️') + .setLabel('Next') + .setStyle(ButtonStyle.Primary) + .setDisabled(page === totalPages || disable), + new ButtonBuilder() + .setCustomId('guilds-last') + .setEmoji('⏭️') + .setLabel('Last') + .setStyle(ButtonStyle.Primary) + .setDisabled(page === totalPages || disable), + ]; +} + +export default command; diff --git a/src/components/buttons/guilds-first.ts b/src/components/buttons/guilds-first.ts new file mode 100644 index 0000000..03b9900 --- /dev/null +++ b/src/components/buttons/guilds-first.ts @@ -0,0 +1,12 @@ +import { Button } from '../../types/button'; + +const button: Button = { + data: { + name: 'guilds-first', + }, + async execute(interaction) { + return; + }, +}; + +export default button; diff --git a/src/components/buttons/guilds-last.ts b/src/components/buttons/guilds-last.ts new file mode 100644 index 0000000..9f60a58 --- /dev/null +++ b/src/components/buttons/guilds-last.ts @@ -0,0 +1,12 @@ +import { Button } from '../../types/button'; + +const button: Button = { + data: { + name: 'guilds-last', + }, + async execute(interaction) { + return; + }, +}; + +export default button; diff --git a/src/components/buttons/guilds-next.ts b/src/components/buttons/guilds-next.ts new file mode 100644 index 0000000..e35bc6b --- /dev/null +++ b/src/components/buttons/guilds-next.ts @@ -0,0 +1,12 @@ +import { Button } from '../../types/button'; + +const button: Button = { + data: { + name: 'guilds-next', + }, + async execute(interaction) { + return; + }, +}; + +export default button; diff --git a/src/components/buttons/guilds-previous.ts b/src/components/buttons/guilds-previous.ts new file mode 100644 index 0000000..e32f405 --- /dev/null +++ b/src/components/buttons/guilds-previous.ts @@ -0,0 +1,12 @@ +import { Button } from '../../types/button'; + +const button: Button = { + data: { + name: 'guilds-previous', + }, + async execute(interaction) { + return; + }, +}; + +export default button; diff --git a/src/events/client/interactionCreate.ts b/src/events/client/interactionCreate.ts index fd662dd..227babf 100644 --- a/src/events/client/interactionCreate.ts +++ b/src/events/client/interactionCreate.ts @@ -15,6 +15,16 @@ const interactionCreateEvent: Event = { if (!command) return; try { + const DEVELOPER_IDS = process.env.DEVELOPER_IDS?.split(','); + + // Check if the command is developer-only and the user is not authorized + if (command.developerOnly && !DEVELOPER_IDS?.includes(interaction.user.id)) { + return await interaction.reply({ + content: 'You are not authorized to use this command.', + ephemeral: true, + }); + } + await command.execute(interaction, client); } catch (error) { console.error(error); diff --git a/src/types/command.ts b/src/types/command.ts index f64722b..b8977e5 100644 --- a/src/types/command.ts +++ b/src/types/command.ts @@ -12,7 +12,7 @@ import Client from '../extensions/custom-client'; /** * Represents the categories a command can belong to. */ -export type Category = 'Fun' | 'Images' | 'Information' | 'Utility'; +export type Category = 'Developer' | 'Fun' | 'Images' | 'Information' | 'Utility'; /** * Interface for defining a command in the Discord bot. @@ -29,6 +29,11 @@ export interface Command { */ category: Category; + /** + * Whether the command is a developer-only command. + * Developer commands are only accessible by the bot owner. + */ + developerOnly?: boolean; /** * The function to be executed when the command is invoked. * It handles the command's logic and interacts with the Discord API.