11import SubCommand from '../../SubCommand.js' ;
2- import {
3- ActionRowBuilder ,
4- ChannelSelectMenuBuilder ,
5- ChannelType ,
6- MessageFlags ,
7- ModalBuilder ,
8- TextInputBuilder ,
9- TextInputStyle
10- } from 'discord.js' ;
112import Confirmation from '../../../database/Confirmation.js' ;
12- import { timeAfter } from '../../../util/timeutils.js' ;
133import AutoResponse from '../../../database/AutoResponse.js' ;
144import ErrorEmbed from '../../../formatting/embeds/ErrorEmbed.js' ;
155import colors from '../../../util/colors.js' ;
16- import { SELECT_MENU_OPTIONS_LIMIT } from '../../../util/apiLimits.js' ;
17- import config from '../../../bot/Config.js' ;
6+ import BetterModalBuilder from "../../../formatting/components/BetterModalBuilder.js" ;
7+ import TriggerTypeSelect from "../../../formatting/components/ctf/component/TriggerTypeSelect.js" ;
8+ import CTFCheckboxes from "../../../formatting/components/ctf/component/CTFCheckboxes.js" ;
9+ import NextStepMessage from "../../../formatting/messages/NextStepMessage.js" ;
10+ import { timeAfter } from "../../../util/timeutils.js" ;
11+ import TriggerInput from "../../../formatting/components/ctf/component/TriggerInput.js" ;
12+ import ResponseInput from "../../../formatting/components/ctf/component/ResponseInput.js" ;
13+ import ChannelsInput from "../../../formatting/components/ctf/component/ChannelsInput.js" ;
14+
15+ /**
16+ * @typedef {object } AutoResponseConfirmationData
17+ * @property {string } triggerType
18+ * @property {boolean } global
19+ * @property {boolean } imageDetection
20+ */
1821
1922export default class AddAutoResponseCommand extends SubCommand {
20-
21- buildOptions ( builder ) {
22- builder . addStringOption ( option => option
23- . setName ( 'type' )
24- . setChoices (
25- {
26- name : 'Regular expression' ,
27- value : 'regex'
28- } , {
29- name : 'Include (ignore case) [default]' ,
30- value : 'include'
31- } , {
32- name : 'Match full message (ignore case)' ,
33- value : 'match'
34- } , {
35- name : 'Phishing domains (e.g. "discord.com(gg):0.8")' ,
36- value : 'phishing'
37- }
38- )
39- . setDescription ( 'How is this auto-response triggered?' )
40- ) ;
41- builder . addBooleanOption ( option => option
42- . setName ( 'global' )
43- . setDescription ( 'Use auto-response in all channels' )
44- . setRequired ( false ) ) ;
45-
46- if ( config . data . googleCloud . vision . enabled ) {
47- builder . addBooleanOption ( option => option
48- . setName ( 'image-detection' )
49- . setDescription ( 'Respond to images containing text that matches the trigger' )
50- . setRequired ( false ) ) ;
51- }
52-
53- return super . buildOptions ( builder ) ;
54- }
55-
5623 async execute ( interaction ) {
57- const global = interaction . options . getBoolean ( 'global' ) ?? false ,
58- type = interaction . options . getString ( 'type' ) ?? 'include' ,
59- vision = interaction . options . getBoolean ( 'image-detection' ) ?? false ;
60-
61- const confirmation = new Confirmation ( { global, type, vision} , timeAfter ( '1 hour' ) ) ;
62- await interaction . showModal ( new ModalBuilder ( )
63- . setTitle ( `Create new Auto-response of type ${ type } ` )
64- . setCustomId ( `auto-response:add:${ await confirmation . save ( ) } ` )
65- . addComponents (
66- // eslint-disable-next-line jsdoc/reject-any-type
67- /** @type {* } */
68- new ActionRowBuilder ( )
69- . addComponents (
70- // eslint-disable-next-line jsdoc/reject-any-type
71- /** @type {* } */
72- new TextInputBuilder ( )
73- . setRequired ( true )
74- . setCustomId ( 'trigger' )
75- . setStyle ( TextInputStyle . Short )
76- . setPlaceholder ( AutoResponse . getTriggerPlaceholder ( type ) )
77- . setLabel ( 'Trigger' )
78- . setMinLength ( 1 )
79- . setMaxLength ( 4000 ) ,
80- ) ,
81- // eslint-disable-next-line jsdoc/reject-any-type
82- /** @type {* } */
83- new ActionRowBuilder ( )
84- . addComponents (
85- // eslint-disable-next-line jsdoc/reject-any-type
86- /** @type {* } */
87- new TextInputBuilder ( )
88- . setRequired ( true )
89- . setCustomId ( 'response' )
90- . setStyle ( TextInputStyle . Paragraph )
91- . setPlaceholder ( 'Hi there :wave:' )
92- . setLabel ( 'Response' )
93- . setMinLength ( 1 )
94- . setMaxLength ( 4000 )
95- )
96- ) ) ;
97- }
24+ const modal = new BetterModalBuilder ( )
25+ . setTitle ( "Add auto-response" )
26+ . setCustomId ( `auto-response:add` )
27+ . addLabelComponent ( new TriggerTypeSelect ( ) )
28+ . addLabelComponent ( new CTFCheckboxes ( "auto-response" ) ) ;
9829
99- async executeModal ( interaction ) {
100- const confirmationId = interaction . customId . split ( ':' ) [ 2 ] ;
101- const confirmation = await Confirmation . get ( confirmationId ) ;
30+ await interaction . showModal ( modal ) ;
31+ }
10232
33+ async executeButton ( interaction ) {
34+ const confirmation = await this . getConfirmation ( interaction ) ;
10335 if ( ! confirmation ) {
104- await interaction . reply ( ErrorEmbed . message ( 'This confirmation has expired.' ) ) ;
10536 return ;
10637 }
10738
108- let trigger , response ;
109- for ( let component of interaction . components ) {
110- component = component . components [ 0 ] ;
111- if ( component . customId === 'trigger' ) {
112- trigger = component . value ;
113- }
114- else if ( component . customId === 'response' ) {
115- response = component . value ;
116- }
117- }
39+ const modal = new BetterModalBuilder ( )
40+ . setTitle ( "Add auto-response" )
41+ . setCustomId ( interaction . customId )
42+ . addLabelComponent ( new TriggerInput ( ) )
43+ . addLabelComponent ( new ResponseInput ( true ) ) ;
11844
119- if ( confirmation . data . global ) {
120- await confirmation . delete ( ) ;
121- await this . create (
122- interaction ,
123- confirmation . data . global ,
124- [ ] ,
125- confirmation . data . type ,
126- trigger ,
127- response ,
128- confirmation . data . vision ,
129- ) ;
130- }
131- else {
132- confirmation . data . trigger = trigger ;
133- confirmation . data . response = response ;
134- confirmation . expires = timeAfter ( '30 min' ) ;
135-
136- await interaction . reply ( {
137- flags : MessageFlags . Ephemeral ,
138- content : 'Select channels for the auto-response' ,
139- components : [
140- /** @type {ActionRowBuilder } */
141- // eslint-disable-next-line jsdoc/reject-any-type
142- new ActionRowBuilder ( ) . addComponents ( /** @type {* } */ new ChannelSelectMenuBuilder ( )
143- // eslint-disable-next-line jsdoc/reject-any-type
144- . addChannelTypes ( /** @type {* } */ [
145- ChannelType . GuildText ,
146- ChannelType . GuildForum ,
147- ChannelType . GuildAnnouncement ,
148- ChannelType . GuildStageVoice ,
149- ] )
150- . setMinValues ( 1 )
151- . setMaxValues ( SELECT_MENU_OPTIONS_LIMIT )
152- . setCustomId ( `auto-response:add:${ await confirmation . save ( ) } ` )
153- ) ,
154- ]
155- } ) ;
45+ if ( ! confirmation . data . global ) {
46+ modal . addLabelComponent ( new ChannelsInput ( "auto-response" ) ) ;
15647 }
48+
49+ return await interaction . showModal ( modal ) ;
15750 }
15851
159- async executeSelectMenu ( interaction ) {
160- const confirmationId = interaction . customId . split ( ':' ) [ 2 ] ;
161- const confirmation = await Confirmation . get ( confirmationId ) ;
52+ async executeModal ( interaction ) {
53+ if ( interaction . customId === "auto-response:add" ) {
54+ return this . handleFirstStageModal ( interaction ) ;
55+ }
16256
57+ const confirmation = await this . getConfirmation ( interaction ) ;
16358 if ( ! confirmation ) {
164- await interaction . update ( ErrorEmbed . message ( 'This confirmation has expired.' ) ) ;
16559 return ;
16660 }
16761
168- await this . create (
169- interaction ,
170- confirmation . data . global ,
171- interaction . values ,
172- confirmation . data . type ,
173- confirmation . data . trigger ,
174- confirmation . data . response ,
175- confirmation . data . vision ,
176- ) ;
62+ return this . handleSecondStageModal ( interaction , confirmation ) ;
17763 }
17864
17965 /**
180- * create the auto response
181- * @param {import('discord.js').Interaction } interaction
182- * @param {boolean } global
183- * @param {import('discord.js').Snowflake[] } channels
184- * @param {string } type
185- * @param {string } trigger
186- * @param {string } response
187- * @param {?boolean } enableVision
188- * @returns {Promise<void> }
66+ * handle data submitted from a modal
67+ * @param {import('discord.js').ModalSubmitInteraction } interaction
68+ * @returns {Promise<unknown> }
18969 */
190- async create (
191- interaction ,
192- global ,
193- channels ,
194- type ,
195- trigger ,
196- response ,
197- enableVision ,
198- ) {
70+ async handleFirstStageModal ( interaction ) {
71+ /** @type {AutoResponseConfirmationData } */
72+ const confirmationData = { } ;
73+ confirmationData . imageDetection = false ;
74+ for ( let label of interaction . components ) {
75+ switch ( label . component . customId ) {
76+ case 'trigger-type' :
77+ confirmationData . triggerType = ( /** @type {import('discord.js').SelectMenuModalData } */ label . component ) . values [ 0 ] ;
78+ break ;
79+ case CTFCheckboxes . GLOBAL_ID :
80+ confirmationData . global = ( /** @type {import('discord.js').CheckboxModalData } */ label . component ) . value ;
81+ break ;
82+ case CTFCheckboxes . OPTIONS_ID : {
83+ const group = /** @type {import('discord.js').CheckboxGroupModalData } */ label . component ;
84+ confirmationData . global = group . values . includes ( CTFCheckboxes . GLOBAL_ID ) ;
85+ confirmationData . imageDetection = group . values . includes ( CTFCheckboxes . IMAGE_DETECTION_ID ) ;
86+ }
87+ }
88+ }
89+
90+ const confirmation = new Confirmation ( confirmationData , timeAfter ( "1 hour" ) ) ;
91+ const id = "auto-response:add:" + await confirmation . save ( ) ;
92+ await interaction . reply ( new NextStepMessage ( 1 , 2 , id ) ) ;
93+ }
94+
95+ /**
96+ * handle data submitted from a modal
97+ * @param {import('discord.js').ModalSubmitInteraction } interaction
98+ * @param {Confirmation<AutoResponseConfirmationData> } confirmation
99+ * @returns {Promise<unknown> }
100+ */
101+ async handleSecondStageModal ( interaction , confirmation ) {
102+ let trigger ,
103+ response ,
104+ channels = [ ] ;
105+ for ( let label of interaction . components ) {
106+ switch ( label . component . customId ) {
107+ case 'trigger' :
108+ trigger = ( /** @type {import('discord.js').TextInputModalData } */ label . component ) . value ;
109+ break ;
110+ case 'response' :
111+ response = ( /** @type {import('discord.js').TextInputModalData } */ label . component ) . value ;
112+ break ;
113+ case 'channels' :
114+ channels = ( /** @type {import('discord.js').SelectMenuModalData } */ label . component ) . values ;
115+ break ;
116+ }
117+ }
118+
119+ await confirmation . delete ( ) ;
120+ if ( ! trigger || ! response ||
121+ ( ! confirmation . data . global && ! channels . length ) ) {
122+ await interaction . reply ( ErrorEmbed . message ( "Failed to parse modal data!" ) ) ;
123+ }
124+
199125 const result = await AutoResponse . new (
200126 interaction . guild . id ,
201- global ,
127+ confirmation . data . global ,
202128 channels ,
203- type ,
129+ confirmation . data . triggerType ,
204130 trigger ,
205131 response ,
206- enableVision ,
132+ confirmation . data . imageDetection ,
207133 ) ;
208134 if ( ! result . success ) {
209135 await interaction . reply ( ErrorEmbed . message ( result . message ) ) ;
@@ -216,6 +142,22 @@ export default class AddAutoResponseCommand extends SubCommand {
216142 ) ;
217143 }
218144
145+ /**
146+ * Get a confirmation from the interactions custom id
147+ * @param {import('discord.js').ModalSubmitInteraction|import('discord.js').ButtonInteraction } interaction
148+ * @returns {Promise<?Confirmation<AutoResponseConfirmationData>> }
149+ */
150+ async getConfirmation ( interaction ) {
151+ const confirmationId = interaction . customId . split ( ':' ) [ 2 ] ;
152+ const confirmation = await Confirmation . get ( confirmationId ) ;
153+
154+ if ( ! confirmation ) {
155+ await interaction . reply ( ErrorEmbed . message ( 'This confirmation has expired.' ) ) ;
156+ return null ;
157+ }
158+ return confirmation ;
159+ }
160+
219161 getDescription ( ) {
220162 return 'Add a new auto-response' ;
221163 }
0 commit comments