1 |
/** |
2 |
* This file is part of SudoBot. |
3 |
* |
4 |
* Copyright (C) 2021-2024 OSN Developers. |
5 |
* |
6 |
* SudoBot is free software; you can redistribute it and/or modify it |
7 |
* under the terms of the GNU Affero General Public License as published by |
8 |
* the Free Software Foundation, either version 3 of the License, or |
9 |
* (at your option) any later version. |
10 |
* |
11 |
* SudoBot is distributed in the hope that it will be useful, but |
12 |
* WITHOUT ANY WARRANTY; without even the implied warranty of |
13 |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
14 |
* GNU Affero General Public License for more details. |
15 |
* |
16 |
* You should have received a copy of the GNU Affero General Public License |
17 |
* along with SudoBot. If not, see <https://www.gnu.org/licenses/>. |
18 |
*/ |
19 |
|
20 |
import { |
21 |
ActionRowBuilder, |
22 |
ButtonInteraction, |
23 |
CacheType, |
24 |
ChatInputCommandInteraction, |
25 |
GuildMember, |
26 |
HeadingLevel, |
27 |
Interaction, |
28 |
ModalBuilder, |
29 |
ModalSubmitInteraction, |
30 |
PermissionsString, |
31 |
TextInputBuilder, |
32 |
TextInputStyle, |
33 |
heading |
34 |
} from "discord.js"; |
35 |
import Service from "../core/Service"; |
36 |
import { GatewayEventListener } from "../decorators/GatewayEventListener"; |
37 |
import { HasEventListeners } from "../types/HasEventListeners"; |
38 |
import { userInfo } from "../utils/embed"; |
39 |
import { safeChannelFetch } from "../utils/fetch"; |
40 |
import { chunkedString } from "../utils/utils"; |
41 |
import { GuildConfig } from "./ConfigManager"; |
42 |
|
43 |
export const name = "surveyService"; |
44 |
|
45 |
export default class SurveyService extends Service implements HasEventListeners { |
46 |
@GatewayEventListener("interactionCreate") |
47 |
public async onInteractionCreate(interaction: Interaction<CacheType>) { |
48 |
const config = this.client.configManager.config[interaction.guildId!]?.survey; |
49 |
|
50 |
if (!config?.enabled) { |
51 |
return; |
52 |
} |
53 |
|
54 |
if (interaction.isButton() && interaction.customId.startsWith("survey_")) { |
55 |
await this.onSurveyShowRequest(interaction); |
56 |
return; |
57 |
} |
58 |
|
59 |
if (interaction.isModalSubmit() && interaction.customId.startsWith("survey_")) { |
60 |
await this.onModalSubmit(interaction); |
61 |
return; |
62 |
} |
63 |
} |
64 |
|
65 |
public async onSurveyShowRequest(interaction: ButtonInteraction | ChatInputCommandInteraction) { |
66 |
const config = this.client.configManager.config[interaction.guildId!]?.survey; |
67 |
|
68 |
if (!config?.enabled) { |
69 |
await interaction.reply({ |
70 |
content: "Sorry, surveys are not enabled on this server.", |
71 |
ephemeral: true |
72 |
}); |
73 |
|
74 |
return; |
75 |
} |
76 |
|
77 |
const customId = interaction.isButton() |
78 |
? interaction.customId.replace(/^survey_/, "") |
79 |
: interaction.options.getString("survey", true); |
80 |
const surveyConfig = config.surveys[customId as unknown as keyof typeof config.surveys]; |
81 |
|
82 |
if (!surveyConfig) { |
83 |
await interaction.reply({ |
84 |
content: "That survey does not exist!", |
85 |
ephemeral: true |
86 |
}); |
87 |
|
88 |
return; |
89 |
} |
90 |
|
91 |
if (!this.checkPreconditions(interaction, surveyConfig)) { |
92 |
await interaction.reply({ |
93 |
content: "Sorry, you do not meet the requirements to fill out this survey.", |
94 |
ephemeral: true |
95 |
}); |
96 |
|
97 |
return; |
98 |
} |
99 |
|
100 |
const modal = this.buildModal(customId.toLowerCase(), surveyConfig); |
101 |
await interaction.showModal(modal); |
102 |
} |
103 |
|
104 |
public async onModalSubmit(interaction: ModalSubmitInteraction<CacheType>) { |
105 |
const config = this.client.configManager.config[interaction.guildId!]?.survey; |
106 |
|
107 |
if (!config?.enabled) { |
108 |
await interaction.reply({ |
109 |
content: "Sorry, surveys are not enabled on this server.", |
110 |
ephemeral: true |
111 |
}); |
112 |
|
113 |
return; |
114 |
} |
115 |
|
116 |
const customId = interaction.customId.replace(/^survey_/, ""); |
117 |
const surveyConfig = config.surveys[customId as unknown as keyof typeof config.surveys]; |
118 |
|
119 |
if (!surveyConfig) { |
120 |
await interaction.reply({ |
121 |
content: "That survey does not exist!", |
122 |
ephemeral: true |
123 |
}); |
124 |
|
125 |
return; |
126 |
} |
127 |
|
128 |
if (!this.checkPreconditions(interaction, surveyConfig)) { |
129 |
await interaction.reply({ |
130 |
content: "Sorry, you do not meet the requirements to fill out this survey.", |
131 |
ephemeral: true |
132 |
}); |
133 |
|
134 |
return; |
135 |
} |
136 |
|
137 |
let i = 0; |
138 |
let summary = `${heading( |
139 |
`Survey completed by <@${interaction.user.id}> (${interaction.user.id})`, |
140 |
HeadingLevel.One |
141 |
)}\n${userInfo(interaction.user)}\n\n`; |
142 |
|
143 |
for (const question of surveyConfig.questions) { |
144 |
const answer = interaction.fields.getTextInputValue( |
145 |
`survey_${customId}_question_${i++}` |
146 |
); |
147 |
|
148 |
if (question.required && !answer) { |
149 |
await interaction.reply({ |
150 |
content: "You must fill out all required questions!", |
151 |
ephemeral: true |
152 |
}); |
153 |
|
154 |
return; |
155 |
} |
156 |
|
157 |
summary += `${heading(question.question, HeadingLevel.Three)}\n${answer}\n`; |
158 |
} |
159 |
|
160 |
await interaction.reply({ |
161 |
content: surveyConfig.end_message ?? "Thank you for filling out the form!", |
162 |
ephemeral: true |
163 |
}); |
164 |
|
165 |
const logChannelId = surveyConfig.log_channel ?? config.default_log_channel; |
166 |
|
167 |
if (!logChannelId) { |
168 |
this.client.logger.warn("No log channel specified for survey completion."); |
169 |
return; |
170 |
} |
171 |
|
172 |
const logChannel = await safeChannelFetch(interaction.guild!, logChannelId); |
173 |
|
174 |
if (!logChannel?.isTextBased()) { |
175 |
this.client.logger.warn( |
176 |
"Log channel specified for survey completion does not exist or is not text-based." |
177 |
); |
178 |
|
179 |
return; |
180 |
} |
181 |
|
182 |
chunkedString(summary, 2000).forEach(chunk => |
183 |
logChannel.send({ |
184 |
content: chunk, |
185 |
allowedMentions: { parse: [], roles: [], users: [] } |
186 |
}) |
187 |
); |
188 |
} |
189 |
|
190 |
private checkPreconditions( |
191 |
interaction: Interaction, |
192 |
surveyConfig: NonNullable<GuildConfig["survey"]>["surveys"][string] |
193 |
) { |
194 |
return ( |
195 |
surveyConfig && |
196 |
(!surveyConfig.required_channels?.length || |
197 |
surveyConfig.required_channels.includes(interaction.channelId!)) && |
198 |
(!surveyConfig.required_roles?.length || |
199 |
(interaction.member instanceof GuildMember && |
200 |
surveyConfig.required_roles.some(role => |
201 |
(interaction.member as GuildMember)?.roles.cache.has(role) |
202 |
))) && |
203 |
(!surveyConfig.required_permissions?.length || |
204 |
(interaction.member as GuildMember).permissions.has( |
205 |
surveyConfig.required_permissions as PermissionsString[], |
206 |
true |
207 |
)) && |
208 |
(!surveyConfig.required_users?.length || |
209 |
surveyConfig.required_users.includes(interaction.user.id)) |
210 |
); |
211 |
} |
212 |
|
213 |
public buildModal( |
214 |
customId: string, |
215 |
surveyConfig: NonNullable<GuildConfig["survey"]>["surveys"][string] |
216 |
) { |
217 |
const components: ActionRowBuilder<TextInputBuilder>[] = []; |
218 |
let i = 0; |
219 |
|
220 |
for (const question of surveyConfig.questions) { |
221 |
const input = new TextInputBuilder() |
222 |
.setCustomId(`survey_${customId}_question_${i}`) |
223 |
.setLabel(question.question); |
224 |
|
225 |
if (question.maxLength) { |
226 |
input.setMaxLength(question.maxLength); |
227 |
} |
228 |
|
229 |
if (question.minLength) { |
230 |
input.setMinLength(question.minLength); |
231 |
} |
232 |
|
233 |
if (question.placeholder) { |
234 |
input.setPlaceholder(question.placeholder); |
235 |
} |
236 |
|
237 |
if (question.default_value) { |
238 |
input.setValue(question.default_value); |
239 |
} |
240 |
|
241 |
const row = new ActionRowBuilder<TextInputBuilder>().addComponents( |
242 |
input |
243 |
.setRequired(question.required) |
244 |
.setStyle( |
245 |
question.type === "paragraph" |
246 |
? TextInputStyle.Paragraph |
247 |
: TextInputStyle.Short |
248 |
) |
249 |
); |
250 |
|
251 |
components.push(row); |
252 |
i++; |
253 |
} |
254 |
|
255 |
return new ModalBuilder() |
256 |
.setCustomId(`survey_${customId}`) |
257 |
.setTitle(surveyConfig.name) |
258 |
.setComponents(components); |
259 |
} |
260 |
} |