Skip to content

Commit 494ce35

Browse files
committed
feat: implementing commands preconditions (ownerOnly, userPermissions)
1 parent f53ebd2 commit 494ce35

File tree

5 files changed

+67
-18
lines changed

5 files changed

+67
-18
lines changed

src/commands.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { filename } from 'pathe/utils'
44
import {
55
ApplicationCommandType,
66
ContextMenuCommandBuilder,
7+
PermissionFlagsBits,
78
SlashCommandBuilder
89
} from 'discord.js'
910
import {
@@ -52,6 +53,12 @@ export const slashToJSON = (cmd: HarmonixCommand<true, CommandArg[]>) => {
5253
const builder = new SlashCommandBuilder()
5354
.setName(cmd.options.name!)
5455
.setDescription(cmd.options.description || 'No description provided')
56+
.setDefaultMemberPermissions(
57+
cmd.options.userPermissions?.reduce(
58+
(acc, perm) => acc | PermissionFlagsBits[perm],
59+
0n
60+
)
61+
)
5562

5663
if (cmd.options.args) {
5764
for (const arg of cmd.options.args) {

src/discord.ts

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
} from './types'
1919
import 'dotenv/config'
2020
import { slashToJSON, contextMenuToJSON } from './commands'
21+
import { createError } from './harmonix'
2122

2223
export const initCient = (harmonixOptions: Harmonix['options']) => {
2324
const client = new Client({ intents: harmonixOptions.intents })
@@ -37,7 +38,7 @@ export const refreshApplicationCommands = async (
3738
consola.info('Started refreshing application commands.')
3839
await rest.put(
3940
Routes.applicationCommands(
40-
harmonix.options.clientId || process.env.HARMONIX_CLIENT_ID!
41+
harmonix.options.clientId || process.env.HARMONIX_CLIENT_ID || ''
4142
),
4243
{
4344
body: commands.map((cmd) =>
@@ -47,7 +48,7 @@ export const refreshApplicationCommands = async (
4748
)
4849
consola.success('Successfully reloaded application commands.')
4950
} catch {
50-
consola.error('Failed to reload application commands.')
51+
createError('Failed to reload application commands.')
5152
}
5253
}
5354

@@ -83,6 +84,8 @@ export const registerCommands = (
8384

8485
if (!command) return
8586
const cmd = commands.find((cmd) => cmd.options.name === command)
87+
88+
if (!cmd || cmd.options.slash) return
8689
const resolvedArgs = await Promise.all(
8790
(cmd?.options.args || []).map(async (arg, i) => [
8891
arg.name,
@@ -91,7 +94,17 @@ export const registerCommands = (
9194
)
9295
const fullArgs = Object.fromEntries(resolvedArgs)
9396

94-
if (!cmd || cmd.options.slash) return
97+
if (cmd.options.ownerOnly) {
98+
if (!harmonix.options.ownerId) {
99+
createError('Owner ID is required for ownerOnly commands.')
100+
}
101+
if (!isOwner(harmonix, message.author.id)) return // TODO: Make possibility to customize ownerOnly message
102+
}
103+
if (cmd.options.userPermissions) {
104+
for (const perm of cmd.options.userPermissions) {
105+
if (!message.member?.permissions.has(perm)) return // TODO: Make possibility to customize userPermissions message
106+
}
107+
}
95108
cmd.execute(harmonix.client!, message, { slash: false, args: fullArgs })
96109
})
97110
}
@@ -112,15 +125,18 @@ export const registerSlashCommands = (
112125
opt.name,
113126
await resolveArgument(
114127
interaction,
115-
commandArgType(opt.type)!,
128+
commandArgType(opt.type),
116129
String(opt.value)
117130
)
118131
])
119132
)
120133
const fullArgs = Object.fromEntries(resolvedArgs)
121134

135+
if (cmd.options.ownerOnly && harmonix.options.ownerId) {
136+
if (!isOwner(harmonix, interaction.user.id)) return // TODO: Make possibility to customize ownerOnly message
137+
}
122138
cmd.execute(harmonix.client!, interaction, {
123-
slash: true,
139+
slash: false,
124140
args: fullArgs
125141
})
126142
})
@@ -141,6 +157,20 @@ export const registerContextMenu = (
141157
})
142158
}
143159

160+
const isOwner = (harmonix: Harmonix, authorId: string) => {
161+
if (
162+
Array.isArray(harmonix.options.ownerId) &&
163+
!harmonix.options.ownerId.includes(authorId)
164+
)
165+
return false
166+
if (
167+
typeof harmonix.options.ownerId === 'string' &&
168+
authorId !== harmonix.options.ownerId
169+
)
170+
return false
171+
return true
172+
}
173+
144174
const isHarmonixCommand = (
145175
command: HarmonixCommand<true, CommandArg[]> | HarmonixContextMenu
146176
): command is HarmonixCommand<true, CommandArg[]> => {
@@ -151,7 +181,7 @@ const isHarmonixCommand = (
151181

152182
export const resolveArgument = async (
153183
entity: MessageOrInteraction,
154-
type: CommandArgType,
184+
type: CommandArgType | null,
155185
value: string
156186
) => {
157187
switch (type) {
@@ -208,7 +238,7 @@ const resolveRole = async (entity: MessageOrInteraction, value: string) => {
208238
)
209239
}
210240

211-
const commandArgType = (type: ApplicationCommandOptionType) => {
241+
const commandArgType = (type: ApplicationCommandOptionType | null) => {
212242
switch (type) {
213243
case ApplicationCommandOptionType.String:
214244
return CommandArgType.String
@@ -224,5 +254,7 @@ const commandArgType = (type: ApplicationCommandOptionType) => {
224254
return CommandArgType.Role
225255
case ApplicationCommandOptionType.Number:
226256
return CommandArgType.Number
257+
default:
258+
return null
227259
}
228260
}

src/harmonix.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { LoadConfigOptions } from 'c12'
2+
import consola from 'consola'
23
import { loadOptions } from './options'
34
import { scanCommands, scanContextMenus, scanEvents } from './scan'
45
import { resolveHarmonixCommand } from './commands'
@@ -45,21 +46,21 @@ export const createHarmonix = async (
4546
)
4647

4748
if (!process.env.HARMONIX_CLIENT_TOKEN) {
48-
throw new Error(
49+
createError(
4950
'Client token is required. Please provide it in the environment variable HARMONIX_CLIENT_TOKEN.'
5051
)
5152
}
5253
if (!harmonix.options.clientId && !process.env.HARMONIX_CLIENT_ID) {
53-
throw new Error(
54+
createError(
5455
'Client ID is required. You can provide it in the configuration file or in the environment variable HARMONIX_CLIENT_ID.'
5556
)
5657
}
5758
harmonix.client = initCient(harmonix.options)
58-
refreshApplicationCommands(harmonix, [
59+
registerEvents(harmonix, events)
60+
await refreshApplicationCommands(harmonix, [
5961
...commands.filter((cmd) => cmd.options.slash),
6062
...contextMenus
6163
])
62-
registerEvents(harmonix, events)
6364
registerCommands(
6465
harmonix,
6566
commands.filter((cmd) => !cmd.options.slash)
@@ -72,3 +73,8 @@ export const createHarmonix = async (
7273

7374
return harmonix
7475
}
76+
77+
export const createError = (message: string) => {
78+
consola.error(new Error(message))
79+
process.exit(1)
80+
}

src/options.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { LoadConfigOptions, loadConfig } from 'c12'
22
import { resolve } from 'pathe'
33
import type { HarmonixConfig } from './types'
4+
import { createError } from './harmonix'
45

56
const HarmonixDefaults: HarmonixConfig = {
67
scanDirs: [],
7-
ignore: [],
8-
intents: ['Guilds', 'GuildMessages', 'MessageContent']
8+
ignore: []
99
}
1010

1111
export const loadOptions = async (
@@ -24,7 +24,7 @@ export const loadOptions = async (
2424
})
2525

2626
if (!config) {
27-
throw new Error('No configuration found')
27+
return createError('No configuration found')
2828
}
2929
const options = config
3030

@@ -36,6 +36,11 @@ export const loadOptions = async (
3636
)
3737
options.scanDirs = [...new Set(options.scanDirs)]
3838
options.defaultPrefix = options.defaultPrefix || '!'
39+
options.intents = options.intents || [
40+
'Guilds',
41+
'GuildMessages',
42+
'MessageContent'
43+
]
3944

4045
return options
4146
}

src/types/commands.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import type {
22
Client,
33
Message,
44
ChatInputCommandInteraction,
5-
CacheType
5+
CacheType,
6+
PermissionsString
67
} from 'discord.js'
78

89
export enum CommandArgType {
@@ -65,9 +66,7 @@ export interface CommandOptions {
6566
nsfw?: boolean
6667
slash?: boolean
6768
ownerOnly?: boolean
68-
guildOnly?: boolean
69-
userPermissions?: any[]
70-
botPermissions?: any[]
69+
userPermissions?: PermissionsString[]
7170
cooldown?: number
7271
}
7372

0 commit comments

Comments
 (0)