Skip to content

Commit 2896dfd

Browse files
committed
feat: add context menu and improve events types
1 parent 57d7074 commit 2896dfd

File tree

11 files changed

+242
-45
lines changed

11 files changed

+242
-45
lines changed

src/commands.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import jiti from 'jiti'
22
import { dirname } from 'pathe'
33
import { filename } from 'pathe/utils'
4-
import { SlashCommandBuilder } from 'discord.js'
4+
import {
5+
ApplicationCommandType,
6+
ContextMenuCommandBuilder,
7+
SlashCommandBuilder
8+
} from 'discord.js'
59
import {
610
CommandArgType,
711
type HarmonixCommandArgType,
@@ -10,7 +14,8 @@ import {
1014
type Harmonix,
1115
type HarmonixCommand,
1216
type HarmonixCommandInput,
13-
type CommandArg
17+
type CommandArg,
18+
HarmonixContextMenu
1419
} from './types'
1520

1621
export const resolveHarmonixCommand = (
@@ -43,7 +48,7 @@ export const defineCommand = <Slash extends boolean, Args extends CommandArg[]>(
4348
return { options, execute }
4449
}
4550

46-
export const toJSON = (cmd: HarmonixCommand<true, CommandArg[]>) => {
51+
export const slashToJSON = (cmd: HarmonixCommand<true, CommandArg[]>) => {
4752
const builder = new SlashCommandBuilder()
4853
.setName(cmd.options.name!)
4954
.setDescription(cmd.options.description || 'No description provided')
@@ -114,6 +119,20 @@ export const toJSON = (cmd: HarmonixCommand<true, CommandArg[]>) => {
114119
return builder.toJSON()
115120
}
116121

122+
export const contextMenuToJSON = (ctm: HarmonixContextMenu) => {
123+
const builder = new ContextMenuCommandBuilder()
124+
.setName(ctm.options.name!)
125+
.setType(
126+
ctm.options.type === 'message'
127+
? ApplicationCommandType.Message
128+
: ctm.options.type === 'user'
129+
? ApplicationCommandType.User
130+
: ApplicationCommandType.Message
131+
)
132+
133+
return builder.toJSON()
134+
}
135+
117136
export const defineArgument = <
118137
Type extends keyof HarmonixCommandArgType
119138
>(options: {

src/contextMenus.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import jiti from 'jiti'
2+
import type {
3+
ContextMenuCallback,
4+
ContextMenuOptions,
5+
DefineContextMenu,
6+
DefineContextMenuWithOptions,
7+
Harmonix,
8+
HarmonixContextMenu,
9+
HarmonixContextMenuInput
10+
} from './types'
11+
import { filename } from 'pathe/utils'
12+
13+
export const resolveHarmonixContextMenu = (
14+
ctm: HarmonixContextMenuInput,
15+
harmonixOptions: Harmonix['options']
16+
): HarmonixContextMenu => {
17+
if (typeof ctm === 'string') {
18+
const _jiti = jiti(harmonixOptions.rootDir, {
19+
interopDefault: true
20+
})
21+
const _ctmPath = _jiti.resolve(ctm)
22+
const contextMenu = _jiti(_ctmPath) as HarmonixContextMenu
23+
const options: ContextMenuOptions = {
24+
name: contextMenu.options.name || filename(_ctmPath).split('.')[0],
25+
type: contextMenu.options.type || 'message'
26+
}
27+
28+
return { options, callback: contextMenu.callback }
29+
} else {
30+
return ctm
31+
}
32+
}
33+
34+
export const defineContextMenu: DefineContextMenu &
35+
DefineContextMenuWithOptions = (
36+
...args: [ContextMenuOptions | ContextMenuCallback, ContextMenuCallback?]
37+
): HarmonixContextMenu => {
38+
let options: ContextMenuOptions = {}
39+
40+
if (args.length === 1) {
41+
const [callback] = args as [ContextMenuCallback]
42+
43+
return {
44+
options,
45+
callback
46+
}
47+
} else {
48+
const [opts, callback] = args as [ContextMenuOptions, ContextMenuCallback]
49+
50+
options = opts
51+
return {
52+
options,
53+
callback
54+
}
55+
}
56+
}

src/discord.ts

Lines changed: 69 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,18 @@ import {
66
type User,
77
ApplicationCommandOptionType
88
} from 'discord.js'
9+
import consola from 'consola'
910
import {
1011
type CommandArg,
1112
CommandArgType,
1213
type Harmonix,
1314
type HarmonixCommand,
1415
type HarmonixEvent,
15-
type MessageOrInteraction
16+
type MessageOrInteraction,
17+
HarmonixContextMenu
1618
} from './types'
1719
import 'dotenv/config'
18-
import { toJSON } from './commands'
20+
import { slashToJSON, contextMenuToJSON } from './commands'
1921

2022
export const initCient = (harmonixOptions: Harmonix['options']) => {
2123
const client = new Client({ intents: harmonixOptions.intents })
@@ -25,6 +27,51 @@ export const initCient = (harmonixOptions: Harmonix['options']) => {
2527
return client
2628
}
2729

30+
export const refreshApplicationCommands = async (
31+
harmonix: Harmonix,
32+
commands: (HarmonixCommand<true, CommandArg[]> | HarmonixContextMenu)[]
33+
) => {
34+
if (commands.length === 0) return
35+
const rest = new REST().setToken(process.env.HARMONIX_CLIENT_TOKEN!)
36+
37+
try {
38+
consola.info('Started refreshing application commands.')
39+
await rest.put(
40+
Routes.applicationCommands(
41+
harmonix.options.clientId || process.env.HARMONIX_CLIENT_ID!
42+
),
43+
{
44+
body: commands.map((cmd) =>
45+
isHarmonixCommand(cmd) ? slashToJSON(cmd) : contextMenuToJSON(cmd)
46+
)
47+
}
48+
)
49+
consola.success('Successfully reloaded application commands.')
50+
} catch {
51+
consola.error('Failed to reload application commands.')
52+
}
53+
}
54+
55+
export const registerEvents = (harmonix: Harmonix, events: HarmonixEvent[]) => {
56+
for (const event of events.filter((evt) => !evt.options.type)) {
57+
if (event.options.once) {
58+
harmonix.client?.once(event.options.name!, event.callback)
59+
} else {
60+
harmonix.client?.on(event.options.name!, event.callback)
61+
}
62+
}
63+
64+
harmonix.client?.on(Events.InteractionCreate, (interaction) => {
65+
if (!interaction.isModalSubmit()) return
66+
const event = events
67+
.filter((evt) => evt.options.type === 'modal')
68+
.find((evt) => evt.options.name === interaction.customId)
69+
70+
if (!event) return
71+
event.callback(interaction)
72+
})
73+
}
74+
2875
export const registerCommands = (
2976
harmonix: Harmonix,
3077
commands: HarmonixCommand<false, CommandArg[]>[]
@@ -50,19 +97,10 @@ export const registerCommands = (
5097
})
5198
}
5299

53-
export const registerSlashCommands = async (
100+
export const registerSlashCommands = (
54101
harmonix: Harmonix,
55102
commands: HarmonixCommand<true, CommandArg[]>[]
56103
) => {
57-
if (commands.length === 0) return
58-
const rest = new REST().setToken(process.env.HARMONIX_CLIENT_TOKEN!)
59-
60-
await rest.put(
61-
Routes.applicationCommands(
62-
harmonix.options.clientId || process.env.HARMONIX_CLIENT_ID!
63-
),
64-
{ body: commands.map((cmd) => toJSON(cmd)) }
65-
)
66104
harmonix.client?.on(Events.InteractionCreate, async (interaction) => {
67105
if (!interaction.isChatInputCommand()) return
68106
const cmd = commands.find(
@@ -89,26 +127,29 @@ export const registerSlashCommands = async (
89127
})
90128
}
91129

92-
export const registerEvents = (harmonix: Harmonix, events: HarmonixEvent[]) => {
93-
for (const event of events.filter((evt) => !evt.options.type)) {
94-
if (event.options.once) {
95-
harmonix.client?.once(event.options.name!, event.callback)
96-
} else {
97-
harmonix.client?.on(event.options.name!, event.callback)
98-
}
99-
}
100-
101-
harmonix.client?.on(Events.InteractionCreate, (interaction) => {
102-
if (!interaction.isModalSubmit()) return
103-
const event = events
104-
.filter((evt) => evt.options.type === 'modal')
105-
.find((evt) => evt.options.name === interaction.customId)
130+
export const registerContextMenu = (
131+
harmonix: Harmonix,
132+
contextMenus: HarmonixContextMenu[]
133+
) => {
134+
harmonix.client?.on(Events.InteractionCreate, async (interaction) => {
135+
if (!interaction.isContextMenuCommand()) return
136+
const ctm = contextMenus.find(
137+
(ctm) => ctm.options.name === interaction.commandName
138+
)
106139

107-
if (!event) return
108-
event.callback(interaction)
140+
if (!ctm) return
141+
ctm.callback(interaction)
109142
})
110143
}
111144

145+
const isHarmonixCommand = (
146+
command: HarmonixCommand<true, CommandArg[]> | HarmonixContextMenu
147+
): command is HarmonixCommand<true, CommandArg[]> => {
148+
return (
149+
(command as HarmonixCommand<true, CommandArg[]>).options.slash !== undefined
150+
)
151+
}
152+
112153
export const resolveArgument = async (
113154
entity: MessageOrInteraction,
114155
type: CommandArgType,

src/events.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import type {
88
EventOptions,
99
Harmonix,
1010
HarmonixEvent,
11-
HarmonixEventInput
11+
HarmonixEventInput,
12+
HarmonixEvents
1213
} from './types'
1314

1415
export const resolveHarmonixEvent = (
@@ -36,20 +37,25 @@ export const resolveHarmonixEvent = (
3637
}
3738
}
3839

39-
export const defineEvent: DefineEvent & DefineEventWithOptions = (
40-
...args: [EventOptions | EventCallback, EventCallback?]
40+
export const defineEvent: DefineEvent & DefineEventWithOptions = <
41+
Event extends keyof HarmonixEvents = any
42+
>(
43+
...args: [EventOptions | EventCallback<Event>, EventCallback<Event>?]
4144
): HarmonixEvent => {
4245
let options: EventOptions = {}
4346

4447
if (args.length === 1) {
45-
const [callback] = args as [EventCallback]
48+
const [callback] = args as [EventCallback<keyof HarmonixEvents>]
4649

4750
return {
4851
options,
4952
callback
5053
}
5154
} else {
52-
const [opts, callback] = args as [EventOptions, EventCallback]
55+
const [opts, callback] = args as [
56+
EventOptions,
57+
EventCallback<keyof HarmonixEvents>
58+
]
5359

5460
options = opts
5561
return {

src/harmonix.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import type { LoadConfigOptions } from 'c12'
22
import { loadOptions } from './options'
3-
import { scanCommands, scanEvents } from './scan'
3+
import { scanCommands, scanContextMenus, scanEvents } from './scan'
44
import { resolveHarmonixCommand } from './commands'
55
import { resolveHarmonixEvent } from './events'
6+
import { resolveHarmonixContextMenu } from './contextMenus'
67
import {
78
initCient,
9+
refreshApplicationCommands,
810
registerCommands,
11+
registerContextMenu,
912
registerEvents,
1013
registerSlashCommands
1114
} from './discord'
@@ -32,6 +35,15 @@ export const createHarmonix = async (
3235
resolveHarmonixEvent(evt, harmonix.options)
3336
)
3437

38+
const scannedContextMenus = await scanContextMenus(harmonix)
39+
const _contextMenus = [
40+
...(harmonix.options.contextMenus || []),
41+
...scannedContextMenus
42+
]
43+
const contextMenus = _contextMenus.map((ctm) =>
44+
resolveHarmonixContextMenu(ctm, harmonix.options)
45+
)
46+
3547
if (!process.env.HARMONIX_CLIENT_TOKEN) {
3648
throw new Error(
3749
'Client token is required. Please provide it in the environment variable HARMONIX_CLIENT_TOKEN.'
@@ -43,15 +55,20 @@ export const createHarmonix = async (
4355
)
4456
}
4557
harmonix.client = initCient(harmonix.options)
58+
refreshApplicationCommands(harmonix, [
59+
...commands.filter((cmd) => cmd.options.slash),
60+
...contextMenus
61+
])
62+
registerEvents(harmonix, events)
4663
registerCommands(
4764
harmonix,
4865
commands.filter((cmd) => !cmd.options.slash)
4966
)
50-
await registerSlashCommands(
67+
registerSlashCommands(
5168
harmonix,
5269
commands.filter((cmd) => cmd.options.slash)
5370
)
54-
registerEvents(harmonix, events)
71+
registerContextMenu(harmonix, contextMenus)
5572

5673
return harmonix
5774
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './events'
22
export * from './commands'
3+
export * from './contextMenus'
34
export * from './modals'
45
export * from './options'
56
export * from './harmonix'

src/scan.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,19 @@ export const scanCommands = async (harmonix: Harmonix) => {
1111
}
1212

1313
export const scanEvents = async (harmonix: Harmonix) => {
14-
const files = await scanFiles(harmonix, 'events')
14+
const files = await Promise.all([
15+
scanFiles(harmonix, 'events'),
16+
scanFiles(harmonix, 'listeners')
17+
]).then((r) => r.flat())
18+
19+
return files.map((f) => f.fullPath)
20+
}
21+
22+
export const scanContextMenus = async (harmonix: Harmonix) => {
23+
const files = await Promise.all([
24+
scanFiles(harmonix, 'context-menus'),
25+
scanFiles(harmonix, 'contextMenus')
26+
]).then((r) => r.flat())
1527

1628
return files.map((f) => f.fullPath)
1729
}

0 commit comments

Comments
 (0)