Skip to content

Commit

Permalink
fix(slash.ts): import discord and add Guild type for better type chec…
Browse files Browse the repository at this point in the history
…king

feat(slash.ts): add util module import for better utility functions in the codebase
feat(slash.ts): add support for ISlashCommand interface to improve type safety
feat(slash.ts): add support for process.env.BOT_OWNER for bot owner specific commands
feat(slash.ts): add support for guild specific commands and permissions
feat(slash.ts): add support for channel types in slash commands for better control
feat(slash.ts): add support for user permissions and role restrictions in slash commands
feat(slash.ts): add support for guild owner only and bot owner only commands
feat(slash.ts): add support for ISlashCommandInteraction interface for better interaction handling
feat(slash.ts): add support for validating slash commands with proper logging
feat(slash.ts): add support for preparing slash commands with proper validation
feat(slash.ts): add support for PermissionsNames type for better permission handling
  • Loading branch information
GhomKrosmonaute committed Mar 5, 2024
1 parent 73908ab commit 907c1a4
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 20 deletions.
173 changes: 156 additions & 17 deletions src/app/slash.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
// system file, please don't modify it

import discord from "discord.js"
import discord, { Guild } from "discord.js"
import * as rest from "@discordjs/rest"
import v10 from "discord-api-types/v10"
import path from "path"
import chalk from "chalk"

import * as handler from "@ghom/handler"
import * as logger from "./logger.js"
import * as util from "./util.js"

import { filename } from "dirname-filename-esm"

Expand All @@ -19,7 +20,7 @@ export const slashCommandHandler = new handler.Handler(
pattern: /\.js$/,
loader: async (filepath) => {
const file = await import("file://" + filepath)
return file.default as SlashCommand
return file.default as ISlashCommand
},
onLoad: async (filepath, command) => {
command.filepath = filepath
Expand All @@ -31,42 +32,113 @@ export const slashCommandHandler = new handler.Handler(

export const slashCommands = new (class extends discord.Collection<
string,
SlashCommand
ISlashCommand
> {
add(command: SlashCommand) {
add(command: ISlashCommand) {
validateSlashCommand(command)
this.set(command.options.name, command)
}
})()

export interface SlashCommandOptions {
export type SlashCommandChannelType = "guild" | "dm" | "thread"

export interface ISlashCommandOptions {
name: string
description: string
channelType?: SlashCommandChannelType
guildOnly?: boolean
threadOnly?: boolean
guildOwnerOnly?: boolean
botOwnerOnly?: boolean
userPermissions?: util.PermissionsNames[]
allowRoles?: discord.RoleResolvable[]
denyRoles?: discord.RoleResolvable[]
run: (interaction: ISlashCommandInteraction) => unknown | Promise<unknown>
build?: (builder: discord.SlashCommandBuilder) => unknown | Promise<unknown>
}

export interface SlashCommandOptions<
GuildOnly extends boolean,
ChannelType extends SlashCommandChannelType,
> {
name: string
description: string
channelType?: ChannelType
guildOnly?: GuildOnly
guildOwnerOnly?: boolean
botOwnerOnly?: boolean
userPermissions?: discord.PermissionsString[]
allowRoles?: discord.RoleResolvable[]
denyRoles?: discord.RoleResolvable[]
build?: (
this: discord.SlashCommandBuilder,
builder: discord.SlashCommandBuilder,
) => unknown
run: (
this: discord.CommandInteraction,
interaction: discord.CommandInteraction,
this: SlashCommandInteraction<GuildOnly, ChannelType>,
interaction: SlashCommandInteraction<GuildOnly, ChannelType>,
) => unknown
}

export class SlashCommand {
export class SlashCommand<
GuildOnly extends boolean,
ChannelType extends SlashCommandChannelType,
> {
filepath?: string
native = false
builder = new discord.SlashCommandBuilder()

constructor(public options: SlashCommandOptions) {}
constructor(public options: SlashCommandOptions<GuildOnly, ChannelType>) {}
}

export interface ISlashCommand {
filepath?: string
native: boolean
builder: discord.SlashCommandBuilder
options: ISlashCommandOptions
}

export interface ISlashCommandInteraction
extends Omit<discord.CommandInteraction, "guild" | "guildId" | "channel"> {
guild?: discord.Guild
guildId?: string
channel:
| discord.ThreadChannel
| discord.NewsChannel
| discord.DMChannel
| discord.TextChannel
| discord.TextBasedChannel
| discord.GuildTextBasedChannel
}

export function validateSlashCommand(command: SlashCommand) {
export interface SlashCommandInteraction<
GuildOnly extends boolean,
ChannelType extends SlashCommandChannelType,
> extends Omit<discord.CommandInteraction, "guild" | "guildId" | "channel"> {
guild: GuildOnly extends true ? discord.Guild : undefined
guildId: GuildOnly extends true ? string : undefined
channel: ChannelType extends "dm"
? discord.DMChannel
: ChannelType extends "thread"
? discord.ThreadChannel
: GuildOnly extends true
? discord.GuildTextBasedChannel
: ChannelType extends "guild"
? discord.GuildTextBasedChannel
: discord.TextBasedChannel
}

export function validateSlashCommand(command: ISlashCommand) {
command.builder
.setName(command.options.name)
.setDescription(command.options.description)

if (command.options.userPermissions)
command.builder.setDefaultMemberPermissions(
command.options.userPermissions.reduce((bit, name) => {
return bit | v10.PermissionFlagsBits[name]
}, 0n),
)

command.options.build?.bind(command.builder)(command.builder)

logger.log(
Expand Down Expand Up @@ -101,20 +173,87 @@ export async function registerSlashCommands(guildId?: string) {

export async function prepareSlashCommand(
interaction: discord.CommandInteraction,
command: SlashCommand,
): Promise<boolean | discord.EmbedBuilder> {
if (command.options.guildOnly && !interaction.inGuild()) {
command: ISlashCommand,
): Promise<ISlashCommandInteraction | discord.EmbedBuilder> {
// @ts-ignore
const output: ISlashCommandInteraction = {
...interaction,
guild: undefined,
guildId: undefined,
channel: interaction.channel!,
}

if (
command.options.botOwnerOnly &&
interaction.user.id !== process.env.BOT_OWNER
)
return new discord.EmbedBuilder()
.setColor("Red")
.setDescription("This command can only be used in a server")
.setDescription("This command can only be used by the bot owner")

if (
command.options.guildOnly ||
(command.options.guildOnly !== false &&
command.options.channelType !== "dm")
) {
if (!interaction.inGuild() || !interaction.guild)
return new discord.EmbedBuilder()
.setColor("Red")
.setDescription("This command can only be used in a guild")

output.guild = interaction.guild
output.guildId = interaction.guildId

if (
command.options.guildOwnerOnly &&
interaction.user.id !== interaction.guild.ownerId
)
return new discord.EmbedBuilder()
.setColor("Red")
.setDescription("This command can only be used by the guild owner")

if (command.options.allowRoles || command.options.denyRoles) {
const member = await interaction.guild.members.fetch(interaction.user.id)

if (command.options.allowRoles) {
if (
!member.roles.cache.some((role) =>
command.options.allowRoles?.includes(role.id),
)
)
return new discord.EmbedBuilder()
.setColor("Red")
.setDescription(
"You don't have the required role to use this command",
)
}

if (command.options.denyRoles) {
if (
member.roles.cache.some((role) =>
command.options.denyRoles?.includes(role.id),
)
)
return new discord.EmbedBuilder()
.setColor("Red")
.setDescription(
"You have a role that is not allowed to use this command",
)
}
}
}

if (command.options.threadOnly) {
if (command.options.channelType === "thread") {
if (!interaction.channel || !interaction.channel.isThread())
return new discord.EmbedBuilder()
.setColor("Red")
.setDescription("This command can only be used in a thread")
} else if (command.options.channelType === "dm") {
if (!interaction.channel || !interaction.channel.isDMBased())
return new discord.EmbedBuilder()
.setColor("Red")
.setDescription("This command can only be used in a DM")
}

return true
return output
}
4 changes: 3 additions & 1 deletion src/app/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ import axios from "axios"
import chalk from "chalk"
import EventEmitter from "events"

import v10 from "discord-api-types/v10"
import utc from "dayjs/plugin/utc.js"
import relative from "dayjs/plugin/relativeTime.js"
import timezone from "dayjs/plugin/timezone.js"
import toObject from "dayjs/plugin/toObject.js"

import * as logger from "./logger.js"
import * as util from "./util.js"
import { ModuleLinker } from "vm"

export type PermissionsNames = keyof typeof v10.PermissionFlagsBits

export async function checkUpdates() {
// fetch latest bot.ts codebase
Expand Down
4 changes: 2 additions & 2 deletions src/listeners/slash.interactionCreate.native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ const listener: app.Listener<"interactionCreate"> = {

const prepared = await app.prepareSlashCommand(interaction, cmd)

if (typeof prepared !== "boolean")
if (prepared instanceof app.EmbedBuilder)
return interaction.reply({ embeds: [prepared] }).catch()

if (!prepared) return

try {
await cmd.options.run.bind(interaction)(interaction)
await cmd.options.run.bind(prepared)(prepared)
} catch (error: any) {
app.error(error, cmd.filepath!, true)

Expand Down

0 comments on commit 907c1a4

Please sign in to comment.