Skip to content

Commit 3ec2eef

Browse files
committed
Refactor auto response add command
1 parent b729c78 commit 3ec2eef

11 files changed

Lines changed: 346 additions & 174 deletions

File tree

src/commands/settings/auto-response/AddAutoResponseCommand.js

Lines changed: 116 additions & 174 deletions
Original file line numberDiff line numberDiff line change
@@ -1,209 +1,135 @@
11
import SubCommand from '../../SubCommand.js';
2-
import {
3-
ActionRowBuilder,
4-
ChannelSelectMenuBuilder,
5-
ChannelType,
6-
MessageFlags,
7-
ModalBuilder,
8-
TextInputBuilder,
9-
TextInputStyle
10-
} from 'discord.js';
112
import Confirmation from '../../../database/Confirmation.js';
12-
import {timeAfter} from '../../../util/timeutils.js';
133
import AutoResponse from '../../../database/AutoResponse.js';
144
import ErrorEmbed from '../../../formatting/embeds/ErrorEmbed.js';
155
import 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

1922
export 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
}

src/database/Confirmation.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import database from './Database.js';
66
export default class Confirmation {
77

88
/**
9+
* @template T
910
* @param {T} data
1011
* @param {number} expires
1112
* @param {?number} id
13+
* @returns {Confirmation<T>}
1214
*/
1315
constructor(data, expires, id = null) {
1416
this.data = data;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import {CheckboxGroupBuilder} from "discord.js";
2+
3+
export default class BetterCheckboxGroupBuilder extends CheckboxGroupBuilder {
4+
/**
5+
* @param {import('discord.js').APICheckboxGroupOption | import('discord.js').CheckboxGroupOptionBuilder} option
6+
* @returns {this}
7+
*/
8+
addOption(option) {
9+
this.addOptions(
10+
// eslint-disable-next-line jsdoc/reject-any-type
11+
/** @type {*} */
12+
option
13+
);
14+
return this;
15+
}
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import {ModalBuilder} from "discord.js";
2+
3+
export default class BetterModalBuilder extends ModalBuilder {
4+
/**
5+
* @param {import('discord.js').APILabelComponent|import('discord.js').LabelBuilder} label
6+
* @returns {this}
7+
*/
8+
addLabelComponent(label) {
9+
this.addLabelComponents(
10+
// eslint-disable-next-line jsdoc/reject-any-type
11+
/** @type {*} */
12+
label
13+
);
14+
return this;
15+
}
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import {StringSelectMenuBuilder} from "discord.js";
2+
3+
export default class BetterStringSelectMenuBuilder extends StringSelectMenuBuilder {
4+
/**
5+
* @param {import('discord.js').APISelectMenuOption|import('discord.js').StringSelectMenuOptionBuilder} option
6+
* @returns {this}
7+
*/
8+
addOption(option) {
9+
this.addOptions(
10+
// eslint-disable-next-line jsdoc/reject-any-type
11+
/** @type {*} */
12+
option
13+
);
14+
return this;
15+
}
16+
}

0 commit comments

Comments
 (0)