1- import type { BaseMessageOptions , CommandInteraction , InteractionCallbackResponse , InteractionEditReplyOptions , InteractionReplyOptions , InteractionResponse , Message } from 'discord.js'
1+ import type {
2+ BaseMessageOptions ,
3+ ButtonComponentData ,
4+ ButtonInteraction ,
5+ CommandInteraction ,
6+ InteractionCallbackResponse ,
7+ InteractionCollector ,
8+ InteractionEditReplyOptions ,
9+ InteractionReplyOptions ,
10+ InteractionResponse ,
11+ Message ,
12+ MessageCollectorOptionsParams ,
13+ } from 'discord.js'
214import type { MaybeRef } from 'vue'
315import type { SlashCommandCustomReturnHandler } from '../../../types'
4- import { MessageFlags } from 'discord.js'
5- import { isRef , reactive , watch } from 'vue'
16+ import { ActionRowBuilder , ButtonBuilder , ButtonStyle , ComponentType , MessageFlags } from 'discord.js'
17+ import { computed , isRef , reactive , toValue , watch } from 'vue'
618
719type OmitPreservingCallSignature < T , K extends keyof T = keyof T >
820 = Omit < T , K > & ( T extends ( ...args : infer R ) => infer S ? ( ...args : R ) => S : unknown )
9-
1021type File = NonNullable < BaseMessageOptions [ 'files' ] > [ number ]
22+ type ButtonCollectorOptions = Partial < Omit < MessageCollectorOptionsParams < ComponentType . Button > , 'componentType' > >
23+ type MaybeRefObject < T > = T extends any
24+ ? { [ K in keyof T ] : MaybeRef < T [ K ] > }
25+ : never
26+ type DistributiveOmit < T , K extends keyof T > = T extends any
27+ ? Omit < T , K >
28+ : never
1129
1230type ReplyFunction = SlashCommandCustomReturnHandler & {
1331 ( text ?: MaybeRef < string > , options ?: InteractionReplyOptions ) :
@@ -31,21 +49,75 @@ type ReplyFunction = SlashCommandCustomReturnHandler & {
3149 flags : ( flags : MaybeRef < InteractionReplyOptions [ 'flags' ] > ) => ReplyFunction
3250 file : ( file : MaybeRef < File > ) => ReplyFunction
3351 files : ( files : MaybeRef < File [ ] > ) => ReplyFunction
52+ button : {
53+ (
54+ label : MaybeRef < string > ,
55+ handler : ( interaction : ButtonInteraction ) => void | Promise < void > ,
56+ options ?:
57+ & Partial < MaybeRefObject < DistributiveOmit < ButtonComponentData , 'label' | 'type' > > >
58+ & {
59+ defer ?: boolean
60+ hide ?: MaybeRef < boolean >
61+ collector ?: ButtonCollectorOptions
62+ } ,
63+ ) : ReplyFunction
64+ // TODO: support reply.button.link/primary like interface
65+ }
3466 send : ReplyFunction
3567}
3668
3769function createReplyFunction (
38- { ...defaultOptions } : ( InteractionReplyOptions ) = { } ,
70+ {
71+ buttons,
72+ ...defaultOptions
73+ } : ( InteractionReplyOptions & {
74+ buttons ?: Parameters < ReplyFunction [ 'button' ] > [ ]
75+ } ) = { } ,
3976) : ReplyFunction {
4077 type Msg = Message | InteractionCallbackResponse | InteractionResponse
4178
4279 const reply = ( ( ...args ) => {
43- if ( typeof args [ 0 ] === 'string' || typeof args [ 0 ] === 'undefined' || isRef ( args [ 0 ] ) || typeof args [ 0 ] === 'function' ) {
80+ const buttonComponents = computed ( ( ) => getButtonComponent ( buttons ?? [ ] ) )
81+ let buttonInteractionCollectors : InteractionCollector < ButtonInteraction > [ ] = [ ]
82+ function registerButtonCollectors ( msg : Msg ) {
83+ for ( const collector of buttonInteractionCollectors ) {
84+ collector . stop ( )
85+ }
86+ buttonInteractionCollectors = [ ]
87+
88+ for ( const [ _ , handler , options ] of buttons ?? [ ] ) {
89+ let collectorOptions : ButtonCollectorOptions | undefined
90+ if ( options && 'collector' in options && options . collector ) {
91+ collectorOptions = options . collector
92+ }
93+ if ( 'resource' in msg ) {
94+ buttonInteractionCollectors . push ( msg . resource ! . message ! . createMessageComponentCollector ( {
95+ componentType : ComponentType . Button ,
96+ ...collectorOptions ,
97+ } ) )
98+ }
99+ else {
100+ buttonInteractionCollectors . push ( msg . createMessageComponentCollector ( {
101+ componentType : ComponentType . Button ,
102+ ...collectorOptions ,
103+ } ) )
104+ }
105+ buttonInteractionCollectors [ buttonInteractionCollectors . length - 1 ]
106+ . on ( 'collect' , async ( buttonInteraction ) => {
107+ if ( options ?. defer ?? true )
108+ buttonInteraction . deferUpdate ( )
109+ await handler ( buttonInteraction )
110+ } )
111+ }
112+ }
113+
114+ if ( typeof args [ 0 ] === 'string' || typeof args [ 0 ] === 'undefined' || isRef ( args [ 0 ] ) ) {
44115 const [ text , options ] = args
45116 let message : Promise < Msg >
46117
47118 const replyOptions = reactive ( {
48119 content : text ,
120+ ...buttonComponents . value . length > 0 ? { components : buttonComponents } : { } ,
49121 ...defaultOptions ,
50122 ...options ,
51123 } ) as InteractionReplyOptions
@@ -56,7 +128,12 @@ function createReplyFunction(
56128 : interaction . deferred ? 'editReply' : 'reply'
57129 message = interaction [ method ] (
58130 replyOptions as InteractionReplyOptions & InteractionEditReplyOptions ,
59- ) . catch ( err => err )
131+ )
132+ . then ( ( msg : Msg ) => {
133+ registerButtonCollectors ( msg )
134+ return msg
135+ } )
136+ . catch ( err => err )
60137 return message
61138 }
62139
@@ -82,6 +159,7 @@ function createReplyFunction(
82159 msg . edit ( options as InteractionEditReplyOptions )
83160 else
84161 msg . resource ?. message ?. edit ( options as InteractionEditReplyOptions )
162+ registerButtonCollectors ( msg )
85163 } , { deep : true , flush : 'sync' } )
86164
87165 return handler
@@ -103,6 +181,7 @@ function createReplyFunction(
103181 message . edit ( opts as InteractionEditReplyOptions )
104182 else
105183 message . resource ?. message ?. edit ( opts as InteractionEditReplyOptions )
184+ registerButtonCollectors ( message )
106185 } , { deep : true , flush : 'sync' } )
107186
108187 return {
@@ -137,35 +216,89 @@ function createReplyFunction(
137216 } ,
138217 } ,
139218 flags : {
140- get ( ) {
141- return ( flags : InteractionReplyOptions [ 'flags' ] & InteractionEditReplyOptions [ 'flags' ] ) => createReplyFunction ( {
142- ...defaultOptions ,
143- flags,
144- } )
145- } ,
219+ value : ( flags : InteractionReplyOptions [ 'flags' ] & InteractionEditReplyOptions [ 'flags' ] ) => createReplyFunction ( {
220+ ...defaultOptions ,
221+ flags,
222+ } ) ,
146223 } ,
147224 file : {
148- get ( ) {
149- return ( file : File ) => createReplyFunction ( {
150- ...defaultOptions ,
151- files : defaultOptions . files != null ? [ ...defaultOptions . files , file ] : [ file ] ,
152- } )
153- } ,
225+ value : ( file : File ) => createReplyFunction ( {
226+ ...defaultOptions ,
227+ files : defaultOptions . files != null ? [ ...defaultOptions . files , file ] : [ file ] ,
228+ } ) ,
154229 } ,
155230 files : {
156- get ( ) {
157- return ( files : File [ ] ) => createReplyFunction ( {
158- ...defaultOptions ,
159- files : defaultOptions . files != null ? [ ...defaultOptions . files , ...files ] : files ,
160- } )
161- } ,
231+ value : ( files : File [ ] ) => createReplyFunction ( {
232+ ...defaultOptions ,
233+ files : defaultOptions . files != null ? [ ...defaultOptions . files , ...files ] : files ,
234+ } ) ,
162235 } ,
163236 send : {
164237 value : reply ,
165238 } ,
239+ button : {
240+ value : ( ...args : Parameters < ReplyFunction [ 'button' ] > ) => {
241+ return createReplyFunction ( {
242+ ...defaultOptions ,
243+ buttons : [
244+ ...( buttons ?? [ ] ) ,
245+ args ,
246+ ] ,
247+ } )
248+ } ,
249+ } ,
166250 } )
167251
168252 return reply
169253}
170254
171255export const reply : ReplyFunction = createReplyFunction ( )
256+
257+ function getButtonComponent ( buttons : Parameters < ReplyFunction [ 'button' ] > [ ] ) : NonNullable < BaseMessageOptions [ 'components' ] > {
258+ if ( buttons . length === 0 )
259+ return [ ]
260+
261+ let row = new ActionRowBuilder < ButtonBuilder > ( )
262+ const rows = [ row ]
263+
264+ const it = Iterator . from ( buttons )
265+ . filter ( ( [ , , options ] ) => ! ( toValue ( options ?. hide ) ?? false ) )
266+ . map ( ( [ label , , options ] ) => {
267+ if ( options == null ) {
268+ options = { } as NonNullable < typeof options >
269+ }
270+
271+ if ( toValue ( options . style ) == null ) {
272+ options . style = ButtonStyle . Primary
273+ }
274+ if ( options . style === ButtonStyle . Primary ) {
275+ if ( ! ( 'customId' in options ) || options . customId == null ) {
276+ ( options as any ) . customId = `${ Date . now ( ) } `
277+ }
278+ }
279+
280+ return new ButtonBuilder (
281+ Object . fromEntries (
282+ Object . entries ( {
283+ label,
284+ ...options ,
285+ type : ComponentType . Button ,
286+ } ) . map ( ( [ key , value ] ) => [ key , toValue ( value ) ] ) ,
287+ ) ,
288+ )
289+ } )
290+
291+ for ( let i = 0 ; i < 5 ; i ++ ) {
292+ row . addComponents ( ...Array . from ( it . take ( 5 ) ) )
293+
294+ if ( row . components . length >= 5 ) {
295+ row = new ActionRowBuilder ( )
296+ rows . push ( row )
297+ }
298+ }
299+
300+ if ( row . components . length === 0 )
301+ rows . pop ( )
302+
303+ return rows
304+ }
0 commit comments