blob: 94b09a477f2972c361103bfefba216c4814335ea [file] [log] [blame]
TheCodedProfc2acbcc2023-01-20 17:23:51 -05001import { LoadingEmbed } from "../../utils/defaults.js";
2import Discord, { CommandInteraction, Message, ActionRowBuilder, StringSelectMenuBuilder, ButtonBuilder, ButtonStyle, StringSelectMenuOptionBuilder, APIMessageComponentEmoji, MessageComponentInteraction, TextInputBuilder } from "discord.js";
3import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
4import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
5import client from "../../utils/client.js";
6import convertCurlyBracketString from "../../utils/convertCurlyBracketString.js";
7import singleNotify from "../../utils/singleNotify.js";
8import getEmojiByName from "../../utils/getEmojiByName.js";
9import createPageIndicator from "../../utils/createPageIndicator.js";
10import { modalInteractionCollector } from "../../utils/dualCollector.js";
11import type { GuildConfig } from "../../utils/database.js";
12
13const command = (builder: SlashCommandSubcommandBuilder) =>
14 builder
15 .setName("stats")
16 .setDescription("Controls channels which update when someone joins or leaves the server")
17
18type ChangesType = Record<string, { name?: string; enabled?: boolean; }>
19
20const applyChanges = (baseObject: GuildConfig['stats'], changes: ChangesType): GuildConfig['stats'] => {
21 for (const [id, { name, enabled }] of Object.entries(changes)) {
22 if (!baseObject[id]) baseObject[id] = { name: "", enabled: false};
23 if (name) baseObject[id]!.name = name;
24 if (enabled) baseObject[id]!.enabled = enabled;
25 }
26 return baseObject;
27}
28
29
30const callback = async (interaction: CommandInteraction) => {
31 try{
32 if (!interaction.guild) return;
33 const { renderChannel } = client.logger;
34 let closed = false;
35 let page = 0;
36 const m: Message = await interaction.reply({ embeds: LoadingEmbed, ephemeral: true, fetchReply: true });
37 let changes: ChangesType = {};
38 do {
39 const config = await client.database.guilds.read(interaction.guild.id);
40 const stats = config.stats;
41 let currentID = "";
42 let current: {
43 name: string;
44 enabled: boolean;
45 } = {
46 name: "",
47 enabled: false
48 };
49 let description = "";
PineaFana35b71b2023-01-24 19:33:27 +000050 const pageSelect = new StringSelectMenuBuilder()
TheCodedProfc2acbcc2023-01-20 17:23:51 -050051 .setCustomId("page")
52 .setPlaceholder("Select a stats channel to manage")
53 .setDisabled(Object.keys(stats).length === 0)
54 .setMinValues(1)
55 .setMaxValues(1);
PineaFana35b71b2023-01-24 19:33:27 +000056 const actionSelect = new StringSelectMenuBuilder()
TheCodedProfc2acbcc2023-01-20 17:23:51 -050057 .setCustomId("action")
58 .setPlaceholder("Perform an action")
59 .setMinValues(1)
60 .setMaxValues(1)
61 .setDisabled(Object.keys(stats).length === 0)
62 .addOptions(
63 new StringSelectMenuOptionBuilder()
64 .setLabel("Edit")
65 .setValue("edit")
66 .setDescription("Edit the name of this stats channel")
67 .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji),
68 new StringSelectMenuOptionBuilder()
69 .setLabel("Delete")
70 .setValue("delete")
71 .setDescription("Delete this stats channel")
72 .setEmoji(getEmojiByName("TICKETS.ISSUE", "id") as APIMessageComponentEmoji)
73 );
74 if (Object.keys(stats).length === 0) {
75 description = "You do not have any stats channels set up yet"
76 pageSelect.addOptions(new StringSelectMenuOptionBuilder().setLabel("No stats channels").setValue("none"))
77 } else {
78 currentID = Object.keys(stats)[page]!
79 current = stats[currentID]!;
80 current = applyChanges({ [currentID]: current }, changes)[currentID]!;
81 for (const [id, { name, enabled }] of Object.entries(stats)) {
82 pageSelect.addOptions(
83 new StringSelectMenuOptionBuilder()
84 .setLabel(name)
85 .setValue(id)
86 .setDescription(`Enabled: ${enabled}`)
87 );
88 }
89 actionSelect.addOptions(new StringSelectMenuOptionBuilder()
90 .setLabel(current.enabled ? "Disable" : "Enable")
91 .setValue("toggleEnabled")
92 .setDescription(`Currently ${current.enabled ? "Enabled" : "Disabled"}, click to ${current.enabled ? "disable" : "enable"} this channel`)
93 .setEmoji(getEmojiByName(current.enabled ? "CONTROL.TICK" : "CONTROL.CROSS", "id") as APIMessageComponentEmoji)
94 );
95 description = `**Currently Editing:** ${renderChannel(currentID)}\n\n` +
96 `${getEmojiByName(current.enabled ? "CONTROL.TICK" : "CONTROL.CROSS")} Currently ${current.enabled ? "Enabled" : "Disabled"}\n` +
97 `**Name:** \`${current.name}\`\n` +
98 `**Preview:** ${await convertCurlyBracketString(current.name, interaction.user.id, interaction.user.username, interaction.guild.name, interaction.guild.members)}`
99 }
100 const row = new ActionRowBuilder<ButtonBuilder>()
101 .addComponents(
102 new ButtonBuilder()
103 .setCustomId("back")
104 .setStyle(ButtonStyle.Primary)
105 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji)
106 .setDisabled(page === 0),
107 new ButtonBuilder()
108 .setCustomId("next")
109 .setEmoji(getEmojiByName("CONTROL.RIGHT", "id") as APIMessageComponentEmoji)
110 .setStyle(ButtonStyle.Primary)
111 .setDisabled(page === Object.keys(stats).length - 1),
112 new ButtonBuilder()
113 .setCustomId("add")
114 .setLabel("Create new")
115 .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id") as APIMessageComponentEmoji)
116 .setStyle(ButtonStyle.Secondary)
117 .setDisabled(Object.keys(stats).length >= 24),
118 new ButtonBuilder()
119 .setCustomId("save")
120 .setLabel("Save")
121 .setEmoji(getEmojiByName("ICONS.SAVE", "id") as APIMessageComponentEmoji)
122 .setStyle(ButtonStyle.Success)
123 .setDisabled(Object.keys(changes).length === 0),
124 );
125
PineaFana35b71b2023-01-24 19:33:27 +0000126 const embed = new EmojiEmbed()
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500127 .setTitle("Stats Channels")
128 .setDescription(description + "\n\n" + createPageIndicator(Object.keys(stats).length, page))
129 .setEmoji("SETTINGS.STATS.GREEN")
130 .setStatus("Success")
131
132 interaction.editReply({
133 embeds: [embed],
134 components: [
135 new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(pageSelect),
136 new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(actionSelect),
137 row
138 ]
139 });
140 let i: MessageComponentInteraction;
141 try {
TheCodedProf267563a2023-01-21 17:00:57 -0500142 i = await m.awaitMessageComponent({ filter: (i) => i.user.id === interaction.user.id && i.message.id === m.id, time: 30000 });
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500143 } catch (e) {
144 closed = true;
145 continue;
146 }
147 if (i.isStringSelectMenu()) {
148 switch(i.customId) {
149 case "page":
150 page = Object.keys(stats).indexOf(i.values[0]!);
TheCodedProf267563a2023-01-21 17:00:57 -0500151 await i.deferUpdate();
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500152 break;
153 case "action":
154 if(!changes[currentID]) changes[currentID] = {};
155 switch(i.values[0]!) {
156 case "edit":
157 await i.showModal(
158 new Discord.ModalBuilder()
159 .setCustomId("modal")
160 .setTitle(`Stats channel name`)
161 .addComponents(
162 new ActionRowBuilder<TextInputBuilder>().addComponents(
163 new TextInputBuilder()
164 .setCustomId("ex1")
165 .setLabel("Server Info (1/3)")
166 .setPlaceholder(
167 `{serverName} - This server's name\n\n` +
168 `These placeholders will be replaced with the server's name, etc..`
169 )
170 .setMaxLength(1)
171 .setRequired(false)
172 .setStyle(Discord.TextInputStyle.Paragraph)
173 ),
174 new ActionRowBuilder<TextInputBuilder>().addComponents(
175 new TextInputBuilder()
176 .setCustomId("ex2")
177 .setLabel("Member Counts (2/3) - {MemberCount:...}")
178 .setPlaceholder(
179 `{:all} - Total member count\n` +
180 `{:humans} - Total non-bot users\n` +
181 `{:bots} - Number of bots\n`
182 )
183 .setMaxLength(1)
184 .setRequired(false)
185 .setStyle(Discord.TextInputStyle.Paragraph)
186 ),
187 new ActionRowBuilder<TextInputBuilder>().addComponents(
188 new TextInputBuilder()
189 .setCustomId("ex3")
190 .setLabel("Latest Member (3/3) - {member:...}")
191 .setPlaceholder(
192 `{:name} - The members name\n`
193 )
194 .setMaxLength(1)
195 .setRequired(false)
196 .setStyle(Discord.TextInputStyle.Paragraph)
197 ),
198 new ActionRowBuilder<TextInputBuilder>().addComponents(
199 new TextInputBuilder()
200 .setCustomId("text")
201 .setLabel("Channel name input")
202 .setMaxLength(1000)
203 .setRequired(true)
204 .setStyle(Discord.TextInputStyle.Short)
205 .setValue(current.name)
206 )
207 )
208 );
209 await interaction.editReply({
210 embeds: [
211 new EmojiEmbed()
212 .setTitle("Stats Channel")
213 .setDescription("Modal opened. If you can't see it, click back and try again.")
214 .setStatus("Success")
215 .setEmoji("SETTINGS.STATS.GREEN")
216 ],
217 components: [
218 new ActionRowBuilder<ButtonBuilder>().addComponents(
219 new ButtonBuilder()
220 .setLabel("Back")
221 .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
222 .setStyle(ButtonStyle.Primary)
223 .setCustomId("back")
224 )
225 ]
226 });
227 let out: Discord.ModalSubmitInteraction | null;
228 try {
229 out = await modalInteractionCollector(
230 m,
231 (m) => m.channel!.id === interaction.channel!.id,
232 (_) => true
233 ) as Discord.ModalSubmitInteraction | null;
234 } catch (e) {
235 continue;
236 }
237 if (!out) continue
238 if (!out.fields) continue
239 if (out.isButton()) continue;
240 const newString = out.fields.getTextInputValue("text");
241 if (!newString) continue;
242 changes[currentID]!.name = newString;
243 break;
244 case "delete":
245 changes[currentID] = {};
TheCodedProf267563a2023-01-21 17:00:57 -0500246 await i.deferUpdate();
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500247 break;
248 case "toggleEnabled":
249 changes[currentID]!.enabled = !stats[currentID]!.enabled;
TheCodedProf267563a2023-01-21 17:00:57 -0500250 await i.deferUpdate();
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500251 break;
252 }
253 break;
254 }
255 } else if (i.isButton()) {
TheCodedProf267563a2023-01-21 17:00:57 -0500256 await i.deferUpdate();
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500257 switch(i.customId) {
258 case "back":
259 page--;
260 break;
261 case "next":
262 page++;
263 break;
264 case "add":
265 break;
266 case "save":
PineaFana35b71b2023-01-24 19:33:27 +0000267 const changed = applyChanges(config.stats, changes);
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500268 singleNotify("statsChannelDeleted", interaction.guild.id, true)
269 config.stats = changed;
270 changes = {}
271 await client.database.guilds.write(interaction.guildId!, config);
272 }
273 }
274 console.log(changes, config.stats);
275 } while (!closed);
276 } catch(e) {
277 console.log(e)
278 }
279};
280
281const check = (interaction: CommandInteraction) => {
282 const member = interaction.member as Discord.GuildMember;
283 if (!member.permissions.has("ManageChannels"))
284 return "You must have the *Manage Channels* permission to use this command";
285 return true;
286};
287
288
289export { command };
290export { callback };
291export { check };