blob: dd4027dea81b5788885121d82a4390364c518013 [file] [log] [blame]
PineaFan0d06edc2023-01-17 22:10:31 +00001import { LoadingEmbed } from "../../utils/defaults.js";
TheCodedProff4facde2023-01-28 13:42:48 -05002import Discord, { CommandInteraction, Message, ActionRowBuilder, StringSelectMenuBuilder, ButtonBuilder, ButtonStyle, StringSelectMenuOptionBuilder, APIMessageComponentEmoji, TextInputBuilder, StringSelectMenuInteraction, ButtonInteraction, MessageComponentInteraction, ChannelSelectMenuBuilder, ChannelSelectMenuInteraction, ModalBuilder } from "discord.js";
pineafan0bc04162022-07-25 17:22:26 +01003import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
TheCodedProff86ba092023-01-27 17:10:07 -05004import type { SlashCommandSubcommandBuilder } from "discord.js";
pineafan0bc04162022-07-25 17:22:26 +01005import client from "../../utils/client.js";
pineafan63fc5e22022-08-04 22:04:10 +01006import convertCurlyBracketString from "../../utils/convertCurlyBracketString.js";
pineafan63fc5e22022-08-04 22:04:10 +01007import singleNotify from "../../utils/singleNotify.js";
TheCodedProf4a6d5712023-01-19 15:54:40 -05008import getEmojiByName from "../../utils/getEmojiByName.js";
9import createPageIndicator from "../../utils/createPageIndicator.js";
10import { modalInteractionCollector } from "../../utils/dualCollector.js";
TheCodedProfc2acbcc2023-01-20 17:23:51 -050011
pineafan708692b2022-07-24 22:16:22 +010012
13const command = (builder: SlashCommandSubcommandBuilder) =>
14 builder
pineafan63fc5e22022-08-04 22:04:10 +010015 .setName("stats")
Skyler Grey11236ba2022-08-08 21:13:33 +010016 .setDescription("Controls channels which update when someone joins or leaves the server")
pineafan708692b2022-07-24 22:16:22 +010017
TheCodedProf4a6d5712023-01-19 15:54:40 -050018
TheCodedProf9bc84752023-01-21 05:19:57 -050019const showModal = async (interaction: MessageComponentInteraction, current: { enabled: boolean; name: string; }) => {
TheCodedProfc2acbcc2023-01-20 17:23:51 -050020 await interaction.showModal(
TheCodedProff4facde2023-01-28 13:42:48 -050021 new ModalBuilder()
TheCodedProfc2acbcc2023-01-20 17:23:51 -050022 .setCustomId("modal")
23 .setTitle(`Stats channel name`)
24 .addComponents(
25 new ActionRowBuilder<TextInputBuilder>().addComponents(
26 new TextInputBuilder()
27 .setCustomId("ex1")
28 .setLabel("Server Info (1/3)")
29 .setPlaceholder(
30 `{serverName} - This server's name\n\n` +
31 `These placeholders will be replaced with the server's name, etc..`
32 )
33 .setMaxLength(1)
34 .setRequired(false)
35 .setStyle(Discord.TextInputStyle.Paragraph)
36 ),
37 new ActionRowBuilder<TextInputBuilder>().addComponents(
38 new TextInputBuilder()
39 .setCustomId("ex2")
40 .setLabel("Member Counts (2/3) - {MemberCount:...}")
41 .setPlaceholder(
42 `{:all} - Total member count\n` +
43 `{:humans} - Total non-bot users\n` +
44 `{:bots} - Number of bots\n`
45 )
46 .setMaxLength(1)
47 .setRequired(false)
48 .setStyle(Discord.TextInputStyle.Paragraph)
49 ),
50 new ActionRowBuilder<TextInputBuilder>().addComponents(
51 new TextInputBuilder()
52 .setCustomId("ex3")
53 .setLabel("Latest Member (3/3) - {member:...}")
54 .setPlaceholder(
55 `{:name} - The members name\n`
56 )
57 .setMaxLength(1)
58 .setRequired(false)
59 .setStyle(Discord.TextInputStyle.Paragraph)
60 ),
61 new ActionRowBuilder<TextInputBuilder>().addComponents(
62 new TextInputBuilder()
63 .setCustomId("text")
64 .setLabel("Channel name input")
65 .setMaxLength(1000)
66 .setRequired(true)
67 .setStyle(Discord.TextInputStyle.Short)
68 .setValue(current.name)
69 )
70 )
71 );
TheCodedProf4a6d5712023-01-19 15:54:40 -050072}
73
TheCodedProf9bc84752023-01-21 05:19:57 -050074type ObjectSchema = Record<string, {name: string, enabled: boolean}>
75
TheCodedProf267563a2023-01-21 17:00:57 -050076
TheCodedProf9bc84752023-01-21 05:19:57 -050077
78const addStatsChannel = async (interaction: CommandInteraction, m: Message, currentObject: ObjectSchema): Promise<ObjectSchema> => {
79 let closed = false;
80 let cancelled = false;
81 const originalObject = Object.fromEntries(Object.entries(currentObject).map(([k, v]) => [k, {...v}]));
82 let newChannel: string | undefined;
83 let newChannelName: string = "{memberCount:all}-members";
84 let newChannelEnabled: boolean = true;
85 do {
TheCodedProf267563a2023-01-21 17:00:57 -050086 m = await interaction.editReply({
TheCodedProf9bc84752023-01-21 05:19:57 -050087 embeds: [new EmojiEmbed()
88 .setTitle("Stats Channel")
89 .setDescription(
90 `New stats channel` + (newChannel ? ` in <#${newChannel}>` : "") + "\n\n" +
91 `**Name:** \`${newChannelName}\`\n` +
92 `**Preview:** ${await convertCurlyBracketString(newChannelName, interaction.user!.id, interaction.user.username, interaction.guild!.name, interaction.guild!.members)}\n` +
93 `**Enabled:** ${newChannelEnabled ? "Yes" : "No"}\n\n`
94 )
95 .setEmoji("SETTINGS.STATS.GREEN")
96 .setStatus("Success")
97 ], components: [
98 new ActionRowBuilder<ChannelSelectMenuBuilder>().addComponents(
99 new ChannelSelectMenuBuilder()
100 .setCustomId("channel")
TheCodedProf267563a2023-01-21 17:00:57 -0500101 .setPlaceholder("Select a channel to use")
TheCodedProf9bc84752023-01-21 05:19:57 -0500102 ),
103 new ActionRowBuilder<ButtonBuilder>().addComponents(
104 new ButtonBuilder()
105 .setLabel("Cancel")
106 .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
107 .setStyle(ButtonStyle.Danger)
108 .setCustomId("back"),
109 new ButtonBuilder()
110 .setLabel("Save")
111 .setEmoji(getEmojiByName("ICONS.SAVE", "id"))
112 .setStyle(ButtonStyle.Success)
113 .setCustomId("save"),
114 new ButtonBuilder()
115 .setLabel("Edit name")
116 .setEmoji(getEmojiByName("ICONS.EDIT", "id"))
117 .setStyle(ButtonStyle.Primary)
118 .setCustomId("editName"),
119 new ButtonBuilder()
120 .setLabel(newChannelEnabled ? "Enabled" : "Disabled")
121 .setEmoji(getEmojiByName(newChannelEnabled ? "CONTROL.TICK" : "CONTROL.CROSS", "id"))
122 .setStyle(ButtonStyle.Secondary)
123 .setCustomId("toggleEnabled")
124 )
125 ]
126 });
127 let i: ButtonInteraction | ChannelSelectMenuInteraction;
128 try {
TheCodedProf267563a2023-01-21 17:00:57 -0500129 i = await m.awaitMessageComponent({ time: 300000, filter: (i) => {
130 return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.message.id === m.id;
131 }}) as ButtonInteraction | ChannelSelectMenuInteraction;
TheCodedProf9bc84752023-01-21 05:19:57 -0500132 } catch (e) {
133 closed = true;
134 cancelled = true;
135 break;
136 }
137 if (i.isButton()) {
138 switch (i.customId) {
139 case "back":
TheCodedProf267563a2023-01-21 17:00:57 -0500140 await i.deferUpdate();
TheCodedProf9bc84752023-01-21 05:19:57 -0500141 closed = true;
142 break;
143 case "save":
TheCodedProf267563a2023-01-21 17:00:57 -0500144 await i.deferUpdate();
TheCodedProf9bc84752023-01-21 05:19:57 -0500145 if (newChannel) {
146 currentObject[newChannel] = {
147 name: newChannelName,
148 enabled: newChannelEnabled
149 }
150 }
151 closed = true;
152 break;
153 case "editName":
154 await interaction.editReply({
155 embeds: [new EmojiEmbed()
156 .setTitle("Stats Channel")
157 .setDescription("Modal opened. If you can't see it, click back and try again.")
158 .setStatus("Success")
159 .setEmoji("SETTINGS.STATS.GREEN")
160 ],
161 components: [
162 new ActionRowBuilder<ButtonBuilder>().addComponents(
163 new ButtonBuilder()
164 .setLabel("Back")
165 .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
166 .setStyle(ButtonStyle.Primary)
167 .setCustomId("back")
168 )
169 ]
170 });
171 showModal(i, {name: newChannelName, enabled: newChannelEnabled})
172
173 const out = await modalInteractionCollector(
174 m,
TheCodedProf267563a2023-01-21 17:00:57 -0500175 (m) => m.channel!.id === interaction.channel!.id && m.user!.id === interaction.user!.id,
176 (i) => i.channel!.id === interaction.channel!.id && i.user!.id === interaction.user!.id && i.message!.id === m.id
TheCodedProf9bc84752023-01-21 05:19:57 -0500177 ) as Discord.ModalSubmitInteraction | null;
178 if (!out) continue;
179 if (!out.fields) continue;
180 if (out.isButton()) continue;
181 newChannelName = out.fields.getTextInputValue("text");
182 break;
183 case "toggleEnabled":
TheCodedProf267563a2023-01-21 17:00:57 -0500184 await i.deferUpdate();
TheCodedProf9bc84752023-01-21 05:19:57 -0500185 newChannelEnabled = !newChannelEnabled;
186 break;
187 }
188 } else {
TheCodedProf267563a2023-01-21 17:00:57 -0500189 await i.deferUpdate();
TheCodedProf9bc84752023-01-21 05:19:57 -0500190 if (i.customId === "channel") {
191 newChannel = i.values[0];
192 }
193 }
194 } while (!closed)
195 if (cancelled) return originalObject;
196 if (!(newChannel && newChannelName && newChannelEnabled)) return originalObject;
TheCodedProf267563a2023-01-21 17:00:57 -0500197 return currentObject;
TheCodedProf9bc84752023-01-21 05:19:57 -0500198}
PineaFan5d98a4b2023-01-19 16:15:47 +0000199const callback = async (interaction: CommandInteraction) => {
200 if (!interaction.guild) return;
TheCodedProf4a6d5712023-01-19 15:54:40 -0500201 const { renderChannel } = client.logger;
TheCodedProf4a6d5712023-01-19 15:54:40 -0500202 const m: Message = await interaction.reply({ embeds: LoadingEmbed, ephemeral: true, fetchReply: true });
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500203 let page = 0;
204 let closed = false;
205 const config = await client.database.guilds.read(interaction.guild.id);
TheCodedProf9bc84752023-01-21 05:19:57 -0500206 let currentObject: ObjectSchema = config.stats;
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500207 let modified = false;
PineaFan5d98a4b2023-01-19 16:15:47 +0000208 do {
PineaFana35b71b2023-01-24 19:33:27 +0000209 const embed = new EmojiEmbed()
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500210 .setTitle("Stats Settings")
211 .setEmoji("SETTINGS.STATS.GREEN")
212 .setStatus("Success");
213 const noStatsChannels = Object.keys(currentObject).length === 0;
214 let current: { enabled: boolean; name: string; };
215
216 const pageSelect = new StringSelectMenuBuilder()
PineaFan5d98a4b2023-01-19 16:15:47 +0000217 .setCustomId("page")
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500218 .setPlaceholder("Select a stats channel to manage");
219 const actionSelect = new StringSelectMenuBuilder()
TheCodedProf4a6d5712023-01-19 15:54:40 -0500220 .setCustomId("action")
221 .setPlaceholder("Perform an action")
TheCodedProf4a6d5712023-01-19 15:54:40 -0500222 .addOptions(
223 new StringSelectMenuOptionBuilder()
224 .setLabel("Edit")
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500225 .setDescription("Edit the stats channel")
TheCodedProf4a6d5712023-01-19 15:54:40 -0500226 .setValue("edit")
TheCodedProf4a6d5712023-01-19 15:54:40 -0500227 .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji),
228 new StringSelectMenuOptionBuilder()
229 .setLabel("Delete")
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500230 .setDescription("Delete the stats channel")
TheCodedProf4a6d5712023-01-19 15:54:40 -0500231 .setValue("delete")
TheCodedProf4a6d5712023-01-19 15:54:40 -0500232 .setEmoji(getEmojiByName("TICKETS.ISSUE", "id") as APIMessageComponentEmoji)
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500233 );
234 const buttonRow = new ActionRowBuilder<ButtonBuilder>()
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500235 .addComponents(
236 new ButtonBuilder()
237 .setCustomId("back")
238 .setStyle(ButtonStyle.Primary)
239 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji)
240 .setDisabled(page === 0),
241 new ButtonBuilder()
242 .setCustomId("next")
243 .setEmoji(getEmojiByName("CONTROL.RIGHT", "id") as APIMessageComponentEmoji)
244 .setStyle(ButtonStyle.Primary)
245 .setDisabled(page === Object.keys(currentObject).length - 1),
246 new ButtonBuilder()
247 .setCustomId("add")
248 .setLabel("Create new")
249 .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id") as APIMessageComponentEmoji)
250 .setStyle(ButtonStyle.Secondary)
251 .setDisabled(Object.keys(currentObject).length >= 24),
252 new ButtonBuilder()
253 .setCustomId("save")
254 .setLabel("Save")
255 .setEmoji(getEmojiByName("ICONS.SAVE", "id") as APIMessageComponentEmoji)
256 .setStyle(ButtonStyle.Success)
257 .setDisabled(modified),
TheCodedProf4a6d5712023-01-19 15:54:40 -0500258 );
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500259 if (noStatsChannels) {
260 embed.setDescription("No stats channels have been set up yet. Use the button below to add one.\n\n" +
261 createPageIndicator(1, 1, undefined, true)
262 );
263 pageSelect.setDisabled(true);
264 actionSelect.setDisabled(true);
265 pageSelect.addOptions(new StringSelectMenuOptionBuilder()
266 .setLabel("No stats channels")
267 .setValue("none")
268 );
TheCodedProf4a6d5712023-01-19 15:54:40 -0500269 } else {
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500270 page = Math.min(page, Object.keys(currentObject).length - 1);
271 current = currentObject[Object.keys(config.stats)[page]!]!
TheCodedProf4a6d5712023-01-19 15:54:40 -0500272 actionSelect.addOptions(new StringSelectMenuOptionBuilder()
273 .setLabel(current.enabled ? "Disable" : "Enable")
274 .setValue("toggleEnabled")
275 .setDescription(`Currently ${current.enabled ? "Enabled" : "Disabled"}, click to ${current.enabled ? "disable" : "enable"} this channel`)
276 .setEmoji(getEmojiByName(current.enabled ? "CONTROL.TICK" : "CONTROL.CROSS", "id") as APIMessageComponentEmoji)
277 );
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500278 embed.setDescription(`**Currently Editing:** ${renderChannel(Object.keys(currentObject)[page]!)}\n\n` +
TheCodedProf4a6d5712023-01-19 15:54:40 -0500279 `${getEmojiByName(current.enabled ? "CONTROL.TICK" : "CONTROL.CROSS")} Currently ${current.enabled ? "Enabled" : "Disabled"}\n` +
280 `**Name:** \`${current.name}\`\n` +
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500281 `**Preview:** ${await convertCurlyBracketString(current.name, interaction.user.id, interaction.user.username, interaction.guild.name, interaction.guild.members)}` + '\n\n' +
282 createPageIndicator(Object.keys(config.stats).length, page)
TheCodedProf4a6d5712023-01-19 15:54:40 -0500283 );
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500284 for (const [id, { name, enabled }] of Object.entries(currentObject)) {
285 pageSelect.addOptions(new StringSelectMenuOptionBuilder()
286 .setLabel(`${name} (${renderChannel(id)})`)
287 .setEmoji(getEmojiByName(enabled ? "CONTROL.TICK" : "CONTROL.CROSS", "id") as APIMessageComponentEmoji)
288 .setDescription(`${enabled ? "Enabled" : "Disabled"}`)
289 .setValue(id)
290 );
291 }
292 }
TheCodedProf4a6d5712023-01-19 15:54:40 -0500293
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500294 interaction.editReply({embeds: [embed], components: [
295 new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(pageSelect),
296 new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(actionSelect),
297 buttonRow
298 ]});
TheCodedProf4a6d5712023-01-19 15:54:40 -0500299
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500300 let i: StringSelectMenuInteraction | ButtonInteraction;
TheCodedProf4a6d5712023-01-19 15:54:40 -0500301 try {
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500302 i = await m.awaitMessageComponent({ filter: (interaction) => interaction.user.id === interaction.user.id, time: 60000 }) as StringSelectMenuInteraction | ButtonInteraction;
TheCodedProf4a6d5712023-01-19 15:54:40 -0500303 } catch (e) {
304 closed = true;
305 continue;
306 }
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500307
308 if(i.isStringSelectMenu()) {
TheCodedProf4a6d5712023-01-19 15:54:40 -0500309 switch(i.customId) {
310 case "page":
TheCodedProf267563a2023-01-21 17:00:57 -0500311 await i.deferUpdate();
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500312 page = Object.keys(currentObject).indexOf(i.values[0]!);
TheCodedProf4a6d5712023-01-19 15:54:40 -0500313 break;
314 case "action":
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500315 modified = true;
TheCodedProf4a6d5712023-01-19 15:54:40 -0500316 switch(i.values[0]!) {
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500317 case "edit": {
318 showModal(i, current!)
TheCodedProf4a6d5712023-01-19 15:54:40 -0500319 await interaction.editReply({
320 embeds: [
321 new EmojiEmbed()
322 .setTitle("Stats Channel")
323 .setDescription("Modal opened. If you can't see it, click back and try again.")
324 .setStatus("Success")
325 .setEmoji("SETTINGS.STATS.GREEN")
326 ],
327 components: [
328 new ActionRowBuilder<ButtonBuilder>().addComponents(
329 new ButtonBuilder()
330 .setLabel("Back")
331 .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
332 .setStyle(ButtonStyle.Primary)
333 .setCustomId("back")
334 )
335 ]
336 });
TheCodedProf9bc84752023-01-21 05:19:57 -0500337 let out: Discord.ModalSubmitInteraction | null;
338 try {
339 out = await modalInteractionCollector(
340 m,
341 (m) => m.channel!.id === interaction.channel!.id,
342 (_) => true
343 ) as Discord.ModalSubmitInteraction | null;
344 } catch (e) {
345 continue;
346 }
347 if (!out) continue
348 if (!out.fields) continue
349 if (out.isButton()) continue;
350 currentObject[Object.keys(currentObject)[page]!]!.name = out.fields.getTextInputValue("text");
TheCodedProf4a6d5712023-01-19 15:54:40 -0500351 break;
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500352 }
353 case "toggleEnabled": {
TheCodedProf267563a2023-01-21 17:00:57 -0500354 await i.deferUpdate();
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500355 currentObject[Object.keys(currentObject)[page]!]!.enabled = !currentObject[Object.keys(currentObject)[page]!]!.enabled;
356 modified = true;
TheCodedProf4a6d5712023-01-19 15:54:40 -0500357 break;
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500358 }
359 case "delete": {
TheCodedProf267563a2023-01-21 17:00:57 -0500360 await i.deferUpdate();
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500361 delete currentObject[Object.keys(currentObject)[page]!];
TheCodedProf267563a2023-01-21 17:00:57 -0500362 page = Math.min(page, Object.keys(currentObject).length - 1);
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500363 modified = true;
TheCodedProf4a6d5712023-01-19 15:54:40 -0500364 break;
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500365 }
TheCodedProf4a6d5712023-01-19 15:54:40 -0500366 }
367 break;
368 }
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500369 } else {
TheCodedProf267563a2023-01-21 17:00:57 -0500370 await i.deferUpdate();
TheCodedProf4a6d5712023-01-19 15:54:40 -0500371 switch(i.customId) {
372 case "back":
373 page--;
374 break;
375 case "next":
376 page++;
377 break;
378 case "add":
TheCodedProf9bc84752023-01-21 05:19:57 -0500379 currentObject = await addStatsChannel(interaction, m, currentObject);
TheCodedProf267563a2023-01-21 17:00:57 -0500380 page = Object.keys(currentObject).length - 1;
TheCodedProf4a6d5712023-01-19 15:54:40 -0500381 break;
382 case "save":
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500383 client.database.guilds.write(interaction.guild.id, {stats: currentObject});
384 singleNotify("statsChannelDeleted", interaction.guild.id, true);
385 modified = false;
386 break;
TheCodedProf4a6d5712023-01-19 15:54:40 -0500387 }
388 }
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500389
PineaFan5d98a4b2023-01-19 16:15:47 +0000390 } while (!closed);
pineafan63fc5e22022-08-04 22:04:10 +0100391};
pineafan708692b2022-07-24 22:16:22 +0100392
TheCodedProff86ba092023-01-27 17:10:07 -0500393const check = (interaction: CommandInteraction, _partial: boolean = false) => {
Skyler Grey75ea9172022-08-06 10:22:23 +0100394 const member = interaction.member as Discord.GuildMember;
TheCodedProfafca98b2023-01-17 22:25:43 -0500395 if (!member.permissions.has("ManageChannels"))
PineaFan0d06edc2023-01-17 22:10:31 +0000396 return "You must have the *Manage Channels* permission to use this command";
pineafan708692b2022-07-24 22:16:22 +0100397 return true;
pineafan63fc5e22022-08-04 22:04:10 +0100398};
pineafan708692b2022-07-24 22:16:22 +0100399
PineaFan538d3752023-01-12 21:48:23 +0000400
pineafan708692b2022-07-24 22:16:22 +0100401export { command };
402export { callback };
PineaFan5d98a4b2023-01-19 16:15:47 +0000403export { check };