diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cfcb281..10c0338 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,19 +1,18 @@ on: - push: - branches: [ "main" ] + push: + branches: ["main"] jobs: - build: - runs-on: self-hosted + build: + runs-on: self-hosted - steps: - - uses: actions/checkout@v4.2.0 - - run: bun install - - run: | - touch .env - echo "${{ secrets.ENV }}" >> .env - - name: Stop all proccess - run: pm2 delete all || true - - name: Start a new process - run: pm2 start "bun src/index.ts" - + steps: + - uses: actions/checkout@v4.2.0 + - run: bun install + - run: | + touch .env + echo "${{ secrets.ENV }}" >> .env + - name: Stop all proccess + run: pm2 delete all || true + - name: Start a new process + run: pm2 start "bun src/index.ts" diff --git a/.prettierrc b/.prettierrc index 50d5a18..30856b4 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,4 +1,4 @@ { - "printWidth": 100, + "printWidth": 80, "tabWidth": 4 } diff --git a/bun.lockb b/bun.lockb index d542f55..33906d4 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 45fcf2f..a8d240c 100644 --- a/package.json +++ b/package.json @@ -2,11 +2,13 @@ "name": "zentbot", "type": "module", "scripts": { - "start": "bun run src/index.ts" + "start": "bun run src/index.ts", + "format": "prettier . -w" }, "devDependencies": { "@types/bun": "latest", - "@types/humanize-duration": "^3.27.4" + "@types/humanize-duration": "^3.27.4", + "prettier": "^3.3.3" }, "peerDependencies": { "typescript": "^5.0.0" diff --git a/src/commands/Command.ts b/src/commands/Command.ts index 07861e9..9e272e2 100644 --- a/src/commands/Command.ts +++ b/src/commands/Command.ts @@ -5,14 +5,16 @@ import type { Client, RESTPostAPIApplicationCommandsJSONBody as CommandData, UserContextMenuCommandInteraction, - AutocompleteInteraction + AutocompleteInteraction, + PermissionResolvable, } from "discord.js"; import type { Subcommand } from "../types/subcommand"; namespace Command { export type ChatInput = ChatInputCommandInteraction<"cached">; export type UserContextMenu = UserContextMenuCommandInteraction<"cached">; - export type MessageContentMenu = MessageContextMenuCommandInteraction<"cached">; + export type MessageContentMenu = + MessageContextMenuCommandInteraction<"cached">; export type Autocomplete = AutocompleteInteraction<"cached">; } @@ -22,6 +24,7 @@ abstract class Command { public subcommands: { [x: string]: Subcommand[] }; public client!: Client; public cooldown = 5000; + public permissions?: PermissionResolvable; public constructor(name: string) { this.name = name; @@ -31,9 +34,15 @@ abstract class Command { public init?(): Awaitable; public executeChatInput?(interaction: Command.ChatInput): Awaitable; - public executeUserContextMenu?(interaction: Command.UserContextMenu): Awaitable; - public executeMessageContextMenu?(interaction: Command.MessageContentMenu): Awaitable; - public executeAutocomplete?(interaction: Command.Autocomplete): Awaitable; + public executeUserContextMenu?( + interaction: Command.UserContextMenu, + ): Awaitable; + public executeMessageContextMenu?( + interaction: Command.MessageContentMenu, + ): Awaitable; + public executeAutocomplete?( + interaction: Command.Autocomplete, + ): Awaitable; } export default Command; diff --git a/src/commands/modules/Goji.ts b/src/commands/modules/Goji.ts index 2c32948..497d9ed 100644 --- a/src/commands/modules/Goji.ts +++ b/src/commands/modules/Goji.ts @@ -1,6 +1,14 @@ -import { EmbedBuilder, SlashCommandBuilder } from "discord.js"; +import { + ApplicationCommandType, + ContextMenuCommandBuilder, + EmbedBuilder, + SlashCommandBuilder, + type ContextMenuCommandType, +} from "discord.js"; + import Command from "../Command"; import Goji from "../../models/Goji"; +import GojiMessage from "../../models/GojiMessage"; const SUPPORTED_EXTENSIONS = ["jpeg", "png"]; @@ -21,8 +29,8 @@ export default class extends Command { .setName("name") .setDescription("Tên Goji") .setRequired(true) - .setMaxLength(60) - ) + .setMaxLength(60), + ), ) .addSubcommand((subcommand) => subcommand @@ -34,53 +42,96 @@ export default class extends Command { .setDescription("Tên Goji để xoá") .setRequired(true) .setMaxLength(60) - .setAutocomplete(true) - ) + .setAutocomplete(true), + ), + ) + .addSubcommand((subcommand) => + subcommand + .setName("list") + .setDescription("Liệt kê danh sách Goji mà cậu có"), ) .addSubcommandGroup((group) => group .setName("update") - .setDescription("Update thông tin Goji") + .setDescription("Cập nhật thông tin cho Goji") .addSubcommand((subcommand) => subcommand - .setName("prefix") - .setDescription("Update prefix của Goji") + .setName("name") + .setDescription("Đổi tên cho Goji") .addStringOption((option) => option - .setName("name") - .setDescription("Tên Goji để update") + .setName("goji") + .setDescription( + "Goji mà cậu muốn đổi tên", + ) .setRequired(true) .setMaxLength(60) - .setAutocomplete(true) + .setAutocomplete(true), ) .addStringOption((option) => option - .setName("prefix") - .setDescription("Prefix dùng để khởi tạo tin nhắn") + .setName("name") + .setDescription("Tên mới cho Goji") + .setRequired(true) + .setMaxLength(60), + ), + ) + .addSubcommand((subcommand) => + subcommand + .setName("prefix") + .setDescription("Cập nhật prefix của Goji") + .addStringOption((option) => + option + .setName("goji") + .setDescription( + "Goji mà cậu muốn cập nhật", + ) .setRequired(true) + .setMaxLength(60) + .setAutocomplete(true), ) + .addStringOption((option) => + option + .setName("prefix") + .setDescription( + "Prefix mới cho Goji dùng để khởi tạo tin nhắn", + ) + .setRequired(true), + ), ) .addSubcommand((subcommand) => subcommand .setName("avatar") - .setDescription("Update avatar của Goji") + .setDescription( + "Cập nhật ảnh đại diện của Goji", + ) .addStringOption((option) => option - .setName("name") - .setDescription("Tên Goji để update") + .setName("goji") + .setDescription( + "Goji mà cậu muốn cập nhật", + ) .setRequired(true) .setMaxLength(60) - .setAutocomplete(true) + .setAutocomplete(true), ) .addAttachmentOption((option) => option .setName("avatar") - .setDescription("Avatar của Goji") - .setRequired(true) - ) - ) + .setDescription( + "Ảnh đại diện mới cho Goji", + ) + .setRequired(true), + ), + ), ) - .toJSON() + .toJSON(), + new ContextMenuCommandBuilder() + .setName("Xoá tin nhắn của Goji") + .setType( + ApplicationCommandType.Message as ContextMenuCommandType, + ) + .toJSON(), ); this.subcommands[this.name] = [ @@ -92,9 +143,17 @@ export default class extends Command { name: "delete", target: "delete", }, + { + name: "list", + target: "list", + }, { name: "update", subcommands: [ + { + name: "name", + target: "updateName", + }, { name: "prefix", target: "updatePrefix", @@ -108,24 +167,110 @@ export default class extends Command { ]; } - protected async _create(interaction: Command.ChatInput) { + public override async executeAutocomplete( + interaction: Command.Autocomplete, + ) { + const focused = interaction.options.getFocused(true); + + if (focused.name === "goji") { + const gojis = await Goji.find({ + authorId: interaction.user.id, + guildId: interaction.guildId, + }); + + const results = gojis + .filter( + (g) => + g.name + .toUpperCase() + .search(focused.value.toUpperCase()) !== -1, + ) + .slice(0, 25); // Giới hạn API chỉ cho list 25 item một lần :/ + + await interaction.respond( + results.map((r) => ({ name: r.name, value: r.name })), + ); + } + } + + public override async executeMessageContextMenu( + interaction: Command.MessageContentMenu, + ) { const { - options, + targetMessage: message, + channelId, + user, guildId, - user: { id: authorId }, + client, } = interaction; + const { config } = client; + + const gojiMessage = await GojiMessage.findOne({ + guildId, + channelId, + messageId: message.id, + }); + + if (!gojiMessage) { + await interaction.reply({ + embeds: [ + new EmbedBuilder() + .setDescription( + "❌ Tin nhắn này không có trong dữ liệu của tớ", + ) + .setColor(config.colors.error), + ], + ephemeral: true, + }); + + return; + } + + if (gojiMessage.authorId !== user.id) { + await interaction.reply({ + embeds: [ + new EmbedBuilder() + .setDescription( + "❌ Goji này không phải của cậu!", + ) + .setColor(config.colors.error), + ], + ephemeral: true, + }); + + return; + } + + await interaction.deferReply({ ephemeral: true }); + + await message.delete(); + await gojiMessage.deleteOne(); + + await interaction.deleteReply(); + } + + protected async _create(interaction: Command.ChatInput) { + const { options, guildId, user, client } = interaction; const name = options.getString("name", true); const existed = await Goji.findOne({ name, guildId, - authorId, + authorId: user.id, }); if (existed) { await interaction.reply({ - content: "Cậu đã có một Goji khác dùng tên này!", + embeds: [ + new EmbedBuilder() + .setTitle(name) + .setDescription( + "❌ Cậu đã có một Goji khác dùng tên này!", + ) + .setThumbnail(existed.avatarURL || null) + .setColor(client.config.colors.error), + ], }); return; @@ -134,32 +279,41 @@ export default class extends Command { await new Goji({ name, guildId, - authorId, + authorId: user.id, }).save(); await interaction.reply({ - content: "Đã tạo một Goji mới!", + embeds: [ + new EmbedBuilder() + .setTitle(name) + .setDescription("✅ Đã tạo một Goji mới!") + .setColor(client.config.colors.default), + ], }); } protected async _delete(interaction: Command.ChatInput) { - const { - options, - guildId, - user: { id: authorId }, - } = interaction; + const { options, guildId, user, client } = interaction; + const { config } = client; - const name = options.getString("name", true); + const name = options.getString("goji", true); const goji = await Goji.findOne({ name, guildId, - authorId, + authorId: user.id, }); if (!goji) { await interaction.reply({ - content: "Không tìm thấy Goji nào với tên này :/", + embeds: [ + new EmbedBuilder() + .setTitle(name) + .setDescription( + "❌ Không tìm thấy Goji nào với tên này!", + ) + .setColor(config.colors.error), + ], }); return; @@ -168,106 +322,179 @@ export default class extends Command { await goji.deleteOne(); await interaction.reply({ - content: "Đã xoá một Goji!", + embeds: [ + new EmbedBuilder() + .setTitle(name) + .setDescription("✅ Đã xoá Goji này!") + .setThumbnail(goji.avatarURL || null) + .setColor(config.colors.default), + ], }); } - protected async _updatePrefix(interaction: Command.ChatInput) { - const { - options, + protected async _list(interaction: Command.ChatInput) { + const { guildId, user, client } = interaction; + const { config } = client; + + const gojis = await Goji.find({ guildId, - user: { id: authorId }, - } = interaction; + authorId: user.id, + }); - const name = options.getString("name", true); - const prefix = options.getString("prefix", true); + const info = gojis + .map( + (g) => + `- **${g.name}** - prefix: ${g.prefix ? `\`${g.prefix}\`` : "None"}`, + ) + .join("\n"); + + const embed = new EmbedBuilder() + .setDescription(gojis.length ? info : "Cậu không có Goji nào cả") + .setColor(config.colors.default); + + await interaction.reply({ + embeds: [embed], + }); + } + + protected async _updateName(interaction: Command.ChatInput) { + const { options, guildId, user, client } = interaction; + const { config } = client; + + const name = options.getString("goji", true); + const newName = options.getString("name", true); const goji = await Goji.findOne({ name, guildId, - authorId, + authorId: user.id, }); if (!goji) { await interaction.reply({ - content: "Không tìm thấy Goji nào với tên này :/", + embeds: [ + new EmbedBuilder() + .setTitle(name) + .setDescription( + "❌ Không tìm thấy Goji nào với tên này!", + ) + .setColor(config.colors.error), + ], }); return; } - await goji.updateOne({ - prefix, - }); + await goji.updateOne({ name: newName }); await interaction.reply({ - content: `Đã update prefix của Goji **${goji.name}** thành \`${prefix}\``, + embeds: [ + new EmbedBuilder() + .setTitle(newName) + .setDescription(`✅ Đã đổi tên Goji!`) + .setThumbnail(goji.avatarURL || null) + .setColor(config.colors.default), + ], }); } - protected async _updateAvatar(interaction: Command.ChatInput) { - const { - options, + protected async _updatePrefix(interaction: Command.ChatInput) { + const { options, guildId, user, client } = interaction; + const { config } = client; + + const name = options.getString("goji", true); + const prefix = options.getString("prefix", true); + + const goji = await Goji.findOne({ + name, guildId, - user: { id: authorId }, - client - } = interaction; + authorId: user.id, + }); + + if (!goji) { + await interaction.reply({ + embeds: [ + new EmbedBuilder() + .setTitle(name) + .setDescription( + "❌ Không tìm thấy Goji nào với tên này!", + ) + .setColor(config.colors.error), + ], + }); + + return; + } + + await goji.updateOne({ prefix }); + + await interaction.reply({ + embeds: [ + new EmbedBuilder() + .setTitle(name) + .setDescription(`✅ Đã update prefix thành \`${prefix}\``) + .setThumbnail(goji.avatarURL || null) + .setColor(config.colors.default), + ], + }); + } + protected async _updateAvatar(interaction: Command.ChatInput) { + const { options, guildId, user, client } = interaction; const { config } = client; - const name = options.getString("name", true); + const name = options.getString("goji", true); const avatar = options.getAttachment("avatar", true); const goji = await Goji.findOne({ name, guildId, - authorId, + authorId: user.id, }); if (!goji) { await interaction.reply({ - content: "Không tìm thấy Goji nào với tên này :/", + embeds: [ + new EmbedBuilder() + .setTitle(name) + .setDescription( + "❌ Không tìm thấy Goji nào với tên này!", + ) + .setColor(config.colors.error), + ], }); return; } - if (!SUPPORTED_EXTENSIONS.some((x) => avatar.contentType?.startsWith(`image/${x}`))) { + if ( + !SUPPORTED_EXTENSIONS.some((x) => + avatar.contentType?.startsWith(`image/${x}`), + ) + ) { await interaction.reply({ - content: "Định dạng không hợp lệ. Hãy thử ảnh có đuôi `.jpeg` hoặc `.png`", + embeds: [ + new EmbedBuilder() + .setDescription( + `❌ Định dạng không hỗ trợ! (${SUPPORTED_EXTENSIONS.join(", ")})`, + ) + .setColor(config.colors.error), + ], }); return; } - await goji.updateOne({ - avatarURL: avatar.url, - }); + await goji.updateOne({ avatarURL: avatar.url }); await interaction.reply({ embeds: [ new EmbedBuilder() - .setDescription(`Đã update ảnh thành công cho Goji **${goji.name}**`) - .setImage(avatar.url) + .setTitle(name) + .setDescription(`✅ Đã update ảnh đại diện thành công!`) + .setThumbnail(avatar.url) .setColor(config.colors.default), ], }); } - - public override async executeAutocomplete(interaction: Command.Autocomplete) { - const focused = interaction.options.getFocused(true); - - if (focused.name === "name") { - const gojis = await Goji.find({ - authorId: interaction.user.id, - guildId: interaction.guildId, - }); - - const results = gojis.filter( - (g) => g.name.toUpperCase().search(focused.value.toUpperCase()) !== -1 - ); - - await interaction.respond(results.map((r) => ({ name: r.name, value: r.name }))); - } - } } diff --git a/src/commands/modules/Sticky.ts b/src/commands/modules/Sticky.ts index 3bc1644..de1190c 100644 --- a/src/commands/modules/Sticky.ts +++ b/src/commands/modules/Sticky.ts @@ -1,4 +1,9 @@ -import { PermissionFlagsBits, Routes, SlashCommandBuilder } from "discord.js"; +import { + PermissionFlagsBits, + Routes, + SlashCommandBuilder, +} from "discord.js"; + import Command from "../Command"; import Sticky from "../../models/Sticky"; @@ -20,18 +25,20 @@ export default class extends Command { .setName("content") .setDescription("Nội dung của Sticky Message") .setRequired(true) - .setMaxLength(1024) - ) + .setMaxLength(1024), + ), ) .addSubcommand((subcommand) => subcommand .setName("disable") .setDescription("Tắt Sticky Message cho kênh hiện tại") .addBooleanOption((option) => - option.setName("clear").setDescription("Xoá sticky message") - ) + option + .setName("clear") + .setDescription("Xoá sticky message"), + ), ) - .toJSON() + .toJSON(), ); this.subcommands[this.name] = [ @@ -42,7 +49,7 @@ export default class extends Command { { name: "disable", target: "disable", - } + }, ]; } diff --git a/src/commands/modules/TempVoice.ts b/src/commands/modules/TempVoice.ts index 318d32d..a31693a 100644 --- a/src/commands/modules/TempVoice.ts +++ b/src/commands/modules/TempVoice.ts @@ -1,4 +1,8 @@ -import { ChannelType, SlashCommandBuilder } from "discord.js"; +import { + ChannelType, + PermissionFlagsBits, + SlashCommandBuilder, +} from "discord.js"; import Command from "../Command"; // import TempVoice from "../../models/TempVoice"; import TempVoiceCreator from "../../models/TempVoiceCreator"; @@ -20,33 +24,27 @@ export default class extends Command { .setName("channel") .setDescription("Kênh dùng để tạo") .setRequired(false) - .addChannelTypes(ChannelType.GuildVoice) - ) + .addChannelTypes(ChannelType.GuildVoice), + ), ) - .toJSON() + .toJSON(), ); this.subcommands[this.name] = [ { name: "setup", target: "setup", + permissions: [PermissionFlagsBits.ManageChannels], }, ]; } public async _setup(interaction: Command.ChatInput) { - const { options, guild, member } = interaction; + const { options, guild } = interaction; - if (!member.permissions.has("Administrator")) { - await interaction.reply({ - content: "Cậu không có quyền để dùng lệnh này :P", - ephemeral: true, - }); - - return; - } - - let channel = options.getChannel("channel", false, [ChannelType.GuildVoice]); + let channel = options.getChannel("channel", false, [ + ChannelType.GuildVoice, + ]); if (channel) { const existed = await TempVoiceCreator.findOne({ diff --git a/src/commands/utils/Avatar.ts b/src/commands/utils/Avatar.ts index 40ff346..17b6c92 100644 --- a/src/commands/utils/Avatar.ts +++ b/src/commands/utils/Avatar.ts @@ -10,20 +10,31 @@ export default class extends Command { .setName(this.name) .setDescription("Xem avatar thành viên trong server") .addUserOption((option) => - option.setName("target").setDescription("Chọn người mà cậu muốn xem") + option + .setName("target") + .setDescription("Chọn người mà cậu muốn xem"), ) - .toJSON() + .toJSON(), ); } public override async executeChatInput(interaction: Command.ChatInput) { - const target = interaction.options.getUser("target") || interaction.user; + const target = await this.client.users.fetch( + (interaction.options.getUser("target") || interaction.user).id, + { + force: true, + }, + ); await interaction.reply({ embeds: [ new EmbedBuilder() .setImage(target.displayAvatarURL({ size: 4096 })) - .setDescription(`${target.tag} - ${target}`), + .setDescription(`${target.tag} - ${target}`) + .setColor( + target.hexAccentColor || + this.client.config.colors.default, + ), ], }); } diff --git a/src/commands/utils/Ping.ts b/src/commands/utils/Ping.ts index 1a65dc5..2ffa944 100644 --- a/src/commands/utils/Ping.ts +++ b/src/commands/utils/Ping.ts @@ -1,4 +1,4 @@ -import { SlashCommandBuilder } from "discord.js"; +import { EmbedBuilder, SlashCommandBuilder } from "discord.js"; import Command from "../Command"; export default class extends Command { @@ -9,13 +9,17 @@ export default class extends Command { new SlashCommandBuilder() .setName(this.name) .setDescription("Xem tốc độ phản hồi của bot") - .toJSON() + .toJSON(), ); } public override async executeChatInput(interaction: Command.ChatInput) { await interaction.reply({ - content: `🏓 Pong! ${this.client.ws.ping}ms!`, + embeds: [ + new EmbedBuilder() + .setDescription(`🏓 Pong! ${this.client.ws.ping}ms!`) + .setColor(this.client.config.colors.default), + ], ephemeral: true, }); } diff --git a/src/config.ts b/src/config.ts index c79c766..9149751 100644 --- a/src/config.ts +++ b/src/config.ts @@ -4,5 +4,6 @@ export default { redisURI: process.env.REDIS_URI, colors: { default: 0xfeff9f, + error: 0xf95454, }, }; diff --git a/src/events/misc/InteractionCreate.ts b/src/events/misc/InteractionCreate.ts index fffe074..178a635 100644 --- a/src/events/misc/InteractionCreate.ts +++ b/src/events/misc/InteractionCreate.ts @@ -13,7 +13,7 @@ export default class extends Listener { const { client } = this; const { commands } = client; - + if (interaction.isCommand()) { const { commandName, commandType } = interaction; await commands.execute(commandName, commandType, interaction); diff --git a/src/events/modules/Goji.ts b/src/events/modules/Goji.ts index 7e498ed..65a2fd9 100644 --- a/src/events/modules/Goji.ts +++ b/src/events/modules/Goji.ts @@ -1,6 +1,7 @@ import type { Message, TextChannel } from "discord.js"; import Listener from "../Listener"; import Goji from "../../models/Goji"; +import GojiMessage from "../../models/GojiMessage"; export default class extends Listener { public constructor() { @@ -15,7 +16,9 @@ export default class extends Listener { } const gojis = await Goji.find({ authorId: author.id, guildId }); - const goji = gojis.find((g) => g.prefix && message.content.startsWith(g.prefix)); + const goji = gojis.find( + (g) => g.prefix && message.content.startsWith(g.prefix), + ); if (!goji || !goji.prefix) { return; @@ -24,7 +27,7 @@ export default class extends Listener { await message.delete().catch(() => void 0); let webhook = (await (channel as TextChannel).fetchWebhooks()).find( - (w) => w.owner?.id === client.user.id && w.name === "GojiHook" + (w) => w.owner?.id === client.user.id && w.name === "GojiHook", ); if (!webhook) { @@ -37,7 +40,7 @@ export default class extends Listener { let content = ""; if (message.reference?.messageId) { - const submark = `-# ╭>`; + const submark = `-# ╭┈`; const repliedMessage = await channel.messages .fetch(message.reference.messageId) @@ -66,10 +69,17 @@ export default class extends Listener { content += message.content.slice(goji.prefix.length).trim(); - await webhook.send({ + const gMessage = await webhook.send({ username: goji.name, avatarURL: goji.avatarURL || void 0, content, }); + + await new GojiMessage({ + guildId, + messageId: gMessage.id, + channelId: channel.id, + authorId: author.id, + }).save(); } } diff --git a/src/events/modules/TempVoice/Ready.ts b/src/events/modules/TempVoice/Ready.ts index 05d3cb0..44993bf 100644 --- a/src/events/modules/TempVoice/Ready.ts +++ b/src/events/modules/TempVoice/Ready.ts @@ -20,7 +20,9 @@ export default class extends Listener { const tempVoices = await TempVoice.find(); for (const tempVoice of tempVoices) { - const channel = await client.channels.fetch(tempVoice.channelId).catch(() => void 0); + const channel = await client.channels + .fetch(tempVoice.channelId) + .catch(() => void 0); if (!channel) { await tempVoice.deleteOne(); @@ -34,7 +36,9 @@ export default class extends Listener { const tempVoices = await TempVoice.find(); for (const tempVoice of tempVoices) { - const channel = await client.channels.fetch(tempVoice.channelId).catch(() => void 0); + const channel = await client.channels + .fetch(tempVoice.channelId) + .catch(() => void 0); if (!channel || !channel.isVoiceBased()) { await tempVoice.deleteOne(); diff --git a/src/events/modules/TempVoice/VoiceStateUpdate.ts b/src/events/modules/TempVoice/VoiceStateUpdate.ts index 6ced8e9..52557ce 100644 --- a/src/events/modules/TempVoice/VoiceStateUpdate.ts +++ b/src/events/modules/TempVoice/VoiceStateUpdate.ts @@ -41,18 +41,17 @@ export default class extends Listener { await member.voice.setChannel(null).catch(() => null); - const msg = await user.send({ - content: `Cậu phải chờ nữa mới có thể tạo kênh mới!`, - }).catch(() => null); - - if (!msg) { - return; + const msg = await user + .send({ + content: `Cậu phải chờ nữa mới có thể tạo kênh mới!`, + }) + .catch(() => null); + + if (msg) { + await sleep(expire * 1000 - Date.now()); + await msg.delete().catch(() => null); } - await sleep(expire * 1000 - Date.now()); - - await msg.delete().catch(() => null); - return; } @@ -86,7 +85,7 @@ export default class extends Listener { const tempVoice = await TempVoice.findOne({ channelId: channel.id, - guildId: guild.id, + guildId: guild.id, }); if (!tempVoice) { diff --git a/src/index.ts b/src/index.ts index 3debe0a..ed4dcd5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,7 +22,9 @@ client.commands = new CommandManager(client); client.redis = new Redis(config.redisURI); client.redis.on("ready", () => console.log("Đã kết nối tới Redis")); -mongoose.connection.on("connected", () => console.log("Đã kết nối tới MongoDB")); +mongoose.connection.on("connected", () => + console.log("Đã kết nối tới MongoDB"), +); await Promise.all([ loadEvents(client), diff --git a/src/models/Goji.ts b/src/models/Goji.ts index 26379b8..a75fcb4 100644 --- a/src/models/Goji.ts +++ b/src/models/Goji.ts @@ -8,5 +8,5 @@ export default model( authorId: { type: String, required: true }, guildId: { type: String, required: true }, prefix: { type: String, required: false }, - }) + }), ); diff --git a/src/models/GojiMessage.ts b/src/models/GojiMessage.ts new file mode 100644 index 0000000..eb18083 --- /dev/null +++ b/src/models/GojiMessage.ts @@ -0,0 +1,11 @@ +import { model, Schema } from "mongoose"; + +export default model( + "goji-message", + new Schema({ + guildId: { type: String, required: true }, + messageId: { type: String, required: true }, + channelId: { type: String, required: true }, + authorId: { type: String, required: true }, + }), +); diff --git a/src/models/Sticky.ts b/src/models/Sticky.ts index 88fe348..de05310 100644 --- a/src/models/Sticky.ts +++ b/src/models/Sticky.ts @@ -7,5 +7,5 @@ export default model( channelId: { type: String, required: true }, content: { type: String, required: true }, oldMessageId: { type: String, required: true }, - }) + }), ); diff --git a/src/models/TempVoice.ts b/src/models/TempVoice.ts index d67176e..1fa094c 100644 --- a/src/models/TempVoice.ts +++ b/src/models/TempVoice.ts @@ -6,5 +6,5 @@ export default model( channelId: { type: String, required: true }, guildId: { type: String, required: true }, ownerId: { type: String, required: true }, - }) + }), ); diff --git a/src/models/TempVoiceCreator.ts b/src/models/TempVoiceCreator.ts index f8e8fd4..c77b0f2 100644 --- a/src/models/TempVoiceCreator.ts +++ b/src/models/TempVoiceCreator.ts @@ -5,5 +5,5 @@ export default model( new Schema({ guildId: { type: String, required: true }, channelId: { type: String, required: true }, - }) + }), ); diff --git a/src/types/subcommand.ts b/src/types/subcommand.ts index 2bb01dc..85007a5 100644 --- a/src/types/subcommand.ts +++ b/src/types/subcommand.ts @@ -1,6 +1,9 @@ +import type { PermissionResolvable } from "discord.js"; + export interface SubcommandData { name: string; target: string; + permissions?: PermissionResolvable; } export interface SubcommandGroupData { diff --git a/src/utils/loader.ts b/src/utils/loader.ts index 9e5e8c6..2ddc5f9 100644 --- a/src/utils/loader.ts +++ b/src/utils/loader.ts @@ -8,15 +8,25 @@ const glob = new Glob("*/**/*.ts"); export async function loadEvents(client: Client): Promise { for await (const path of glob.scan("src/events")) { - const listener: Listener = new (await import(`../events/${path}`)).default(); + const listener: Listener = new ( + await import(`../events/${path}`) + ).default(); + listener.client = client; - client[listener.once ? "once" : "on"](listener.name, listener.execute!.bind(listener)); + + client[listener.once ? "once" : "on"]( + listener.name, + listener.execute!.bind(listener), + ); } } export async function loadCommands(client: Client): Promise { for await (const path of glob.scan("src/commands")) { - const command: Command = new (await import(`../commands/${path}`)).default(); - client.commands.add(command) + const command: Command = new ( + await import(`../commands/${path}`) + ).default(); + + client.commands.add(command); } } diff --git a/src/utils/managers/CommandManager.ts b/src/utils/managers/CommandManager.ts index dac9073..ef79f36 100644 --- a/src/utils/managers/CommandManager.ts +++ b/src/utils/managers/CommandManager.ts @@ -2,7 +2,7 @@ import type { Client, CommandInteraction } from "discord.js"; import type Command from "../../commands/Command"; import type { SubcommandData } from "../../types/subcommand"; -import { Collection, ApplicationCommandType } from "discord.js"; +import { Collection, ApplicationCommandType, EmbedBuilder } from "discord.js"; import { sleep } from "bun"; interface SubcommandCollectionValue { @@ -15,7 +15,6 @@ export default class CommandManager { private commands: Collection; - // Value là tên lệnh chính (.name) private chatInputs: Collection; private userContextMenus: Collection; private messageContextMenus: Collection; @@ -51,10 +50,13 @@ export default class CommandManager { for (const data of command.subcommands[k]) { if ("subcommands" in data) { for (const subcommand of data.subcommands) { - this.subcommands.set(`${k}:${data.name}:${subcommand.name}`, { - commandName: k, - data: subcommand, - }); + this.subcommands.set( + `${k}:${data.name}:${subcommand.name}`, + { + commandName: k, + data: subcommand, + }, + ); } } else { this.subcommands.set(`${k}:${data.name}`, { @@ -83,10 +85,11 @@ export default class CommandManager { public async execute( name: string, type: ApplicationCommandType, - interaction: CommandInteraction<"cached"> + interaction: CommandInteraction<"cached">, ) { + const { member, user } = interaction; const { client } = this; - const { redis } = client; + const { redis, config } = client; const command = this.get(name, type); @@ -94,13 +97,19 @@ export default class CommandManager { return; } - const cooldownKey = `${command.name}:${interaction.user.id}`; + const cooldownKey = `${command.name}:${user.id}`; if ((await redis.get(cooldownKey)) !== null) { const expire = await redis.expiretime(cooldownKey); await interaction.reply({ - content: `Cậu phải chờ nữa mới có thể dùng tiếp lệnh này!`, + embeds: [ + new EmbedBuilder() + .setDescription( + `⏰ Cậu phải chờ nữa mới có thể dùng tiếp lệnh này!`, + ) + .setColor(config.colors.error), + ], ephemeral: true, }); @@ -113,6 +122,24 @@ export default class CommandManager { await redis.set(cooldownKey, "", "EX", command.cooldown / 1000); + if ( + command.permissions && + !member.permissions.has(command.permissions, true) + ) { + await interaction.reply({ + embeds: [ + new EmbedBuilder() + .setDescription( + "❌ Cậu không có quyền để dùng lệnh này :P", + ) + .setColor(config.colors.error), + ], + ephemeral: true, + }); + + return; + } + if (interaction.isChatInputCommand()) { await command.executeChatInput?.(interaction); await this.handleSubcommand(command, interaction); @@ -127,8 +154,11 @@ export default class CommandManager { } } - public async handleSubcommand(command: Command, interaction: Command.ChatInput) { - const { commandName, options } = interaction; + public async handleSubcommand( + command: Command, + interaction: Command.ChatInput, + ) { + const { commandName, options, member, client } = interaction; const _subcommand = options.getSubcommand(false); const _subcommandGroup = options.getSubcommandGroup(false); @@ -140,7 +170,9 @@ export default class CommandManager { let subcommand: SubcommandCollectionValue | undefined; if (_subcommandGroup) { - subcommand = this.subcommands.get(`${commandName}:${_subcommandGroup}:${_subcommand}`); + subcommand = this.subcommands.get( + `${commandName}:${_subcommandGroup}:${_subcommand}`, + ); } else { subcommand = this.subcommands.get(`${commandName}:${_subcommand}`); } @@ -149,6 +181,24 @@ export default class CommandManager { return; } + if ( + subcommand.data.permissions && + !member.permissions.has(subcommand.data.permissions, true) + ) { + await interaction.reply({ + embeds: [ + new EmbedBuilder() + .setDescription( + "❌ Cậu không có quyền để dùng lệnh này :P", + ) + .setColor(client.config.colors.error), + ], + ephemeral: true, + }); + + return; + } + await (command as any)[`_${subcommand.data.target}`]?.(interaction); } diff --git a/tsconfig.json b/tsconfig.json index 8827b1e..04a0a9e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,29 +1,29 @@ { - "compilerOptions": { - // Enable latest features - "lib": ["ESNext", "DOM"], - "target": "ESNext", - "module": "ESNext", - "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, - // Bundler mode - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, - // Best practices - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false, + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false, - "noImplicitOverride": true, - } + "noImplicitOverride": true + } }