blob: 482af5bdeaa1e848227b19e4432d3c987b806f70 [file] [log] [blame]
pineafan63fc5e22022-08-04 22:04:10 +01001import { HistorySchema } from "../../utils/database";
pineafan4edb7762022-06-26 19:21:04 +01002import Discord, { CommandInteraction, GuildMember, MessageActionRow, MessageButton, TextInputComponent } from "discord.js";
pineafan73a7c4a2022-07-24 10:38:04 +01003import { SlashCommandSubcommandBuilder } from "@discordjs/builders";
pineafan4edb7762022-06-26 19:21:04 +01004import { WrappedCheck } from "jshaiku";
5import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
6import getEmojiByName from "../../utils/getEmojiByName.js";
7import client from "../../utils/client.js";
pineafan63fc5e22022-08-04 22:04:10 +01008import { modalInteractionCollector } from "../../utils/dualCollector.js";
9import pageIndicator from "../../utils/createPageIndicator.js";
pineafan4edb7762022-06-26 19:21:04 +010010
11const command = (builder: SlashCommandSubcommandBuilder) =>
12 builder
pineafan63fc5e22022-08-04 22:04:10 +010013 .setName("info")
14 .setDescription("Shows moderator information about a user")
15 .addUserOption(option => option.setName("user").setDescription("The user to get information about").setRequired(true));
pineafan4edb7762022-06-26 19:21:04 +010016
pineafan4edb7762022-06-26 19:21:04 +010017const types = {
18 "warn": {emoji: "PUNISH.WARN.YELLOW", text: "Warned"},
19 "mute": {emoji: "PUNISH.MUTE.YELLOW", text: "Muted"},
pineafan73a7c4a2022-07-24 10:38:04 +010020 "unmute": {emoji: "PUNISH.MUTE.GREEN", text: "Unmuted"},
pineafan4edb7762022-06-26 19:21:04 +010021 "join": {emoji: "MEMBER.JOIN", text: "Joined"},
22 "leave": {emoji: "MEMBER.LEAVE", text: "Left"},
23 "kick": {emoji: "MEMBER.KICK", text: "Kicked"},
24 "softban": {emoji: "PUNISH.SOFTBAN", text: "Softbanned"},
25 "ban": {emoji: "MEMBER.BAN", text: "Banned"},
26 "unban": {emoji: "MEMBER.UNBAN", text: "Unbanned"},
27 "purge": {emoji: "PUNISH.CLEARHISTORY", text: "Messages cleared"},
28 "nickname": {emoji: "PUNISH.NICKNAME.YELLOW", text: "Nickname changed"}
pineafan63fc5e22022-08-04 22:04:10 +010029};
pineafan4edb7762022-06-26 19:21:04 +010030
31function historyToString(history: HistorySchema) {
32 let s = `${getEmojiByName(types[history.type].emoji)} ${
33 history.amount ? (history.amount + " ") : ""
34 }${
35 types[history.type].text
36 } on <t:${Math.round(history.occurredAt.getTime() / 1000)}:F>`;
37 if (history.moderator) { s += ` by <@${history.moderator}>`; }
38 if (history.reason) { s += `\n**Reason:**\n> ${history.reason}`; }
39 if (history.before) { s += `\n**Before:**\n> ${history.before}`; }
40 if (history.after) { s += `\n**After:**\n> ${history.after}`; }
41 return s + "\n";
42}
43
44
45class TimelineSection {
46 name: string;
pineafan63fc5e22022-08-04 22:04:10 +010047 content: {data: HistorySchema, rendered: string}[] = [];
pineafan4edb7762022-06-26 19:21:04 +010048
pineafan63fc5e22022-08-04 22:04:10 +010049 addContent = (content: {data: HistorySchema, rendered: string}) => { this.content.push(content); return this; };
pineafan4edb7762022-06-26 19:21:04 +010050 contentLength = () => { return this.content.reduce((acc, cur) => acc + cur.rendered.length, 0); };
51 generateName = () => {
pineafan63fc5e22022-08-04 22:04:10 +010052 const first = Math.round(this.content[0].data.occurredAt.getTime() / 1000);
53 const last = Math.round(this.content[this.content.length - 1].data.occurredAt.getTime() / 1000);
pineafan4edb7762022-06-26 19:21:04 +010054 if (first === last) { return this.name = `<t:${first}:F>`; }
55 return this.name = `<t:${first}:F> - <t:${last}:F>`;
pineafan63fc5e22022-08-04 22:04:10 +010056 };
pineafan4edb7762022-06-26 19:21:04 +010057}
58
59const monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
60
61async function showHistory(member, interaction: CommandInteraction) {
62 let currentYear = new Date().getFullYear();
63 let pageIndex = null;
64 let m, history, current;
65 let refresh = true;
66 let filteredTypes = [];
67 let openFilterPane = false;
68 while (true) {
69 if (refresh) {
70 history = await client.database.history.read(member.guild.id, member.id, currentYear);
71 history = history.sort((a, b) => b.occurredAt.getTime() - a.occurredAt.getTime()).reverse();
72 if (openFilterPane) {
pineafan63fc5e22022-08-04 22:04:10 +010073 let tempFilteredTypes = filteredTypes;
pineafan4edb7762022-06-26 19:21:04 +010074 if (filteredTypes.length === 0) { tempFilteredTypes = Object.keys(types); }
pineafan63fc5e22022-08-04 22:04:10 +010075 history = history.filter(h => tempFilteredTypes.includes(h.type));
76 }
pineafan4edb7762022-06-26 19:21:04 +010077 refresh = false;
78 }
pineafan63fc5e22022-08-04 22:04:10 +010079 const groups: TimelineSection[] = [];
pineafan4edb7762022-06-26 19:21:04 +010080 if (history.length > 0) {
pineafan63fc5e22022-08-04 22:04:10 +010081 current = new TimelineSection();
pineafan4edb7762022-06-26 19:21:04 +010082 history.forEach(event => {
83 if (current.contentLength() + historyToString(event).length > 2000 || current.content.length === 5) {
84 groups.push(current);
85 current.generateName();
86 current = new TimelineSection();
87 }
88 current.addContent({data: event, rendered: historyToString(event)});
89 });
90 current.generateName();
91 groups.push(current);
92 if (pageIndex === null) { pageIndex = groups.length - 1; }
93 }
pineafan63fc5e22022-08-04 22:04:10 +010094 const components = (
pineafan4edb7762022-06-26 19:21:04 +010095 openFilterPane ? [
pineafan63fc5e22022-08-04 22:04:10 +010096 new MessageActionRow().addComponents([new Discord.MessageSelectMenu().setOptions(
97 Object.entries(types).map(([key, value]) => ({
pineafan4edb7762022-06-26 19:21:04 +010098 label: value.text,
99 value: key,
100 default: filteredTypes.includes(key),
101 emoji: client.emojis.resolve(getEmojiByName(value.emoji, "id"))
pineafan63fc5e22022-08-04 22:04:10 +0100102 }))
103 ).setMinValues(1).setMaxValues(Object.keys(types).length).setCustomId("filter").setPlaceholder("Select at least one event")])
104 ] : []).concat([
pineafan4edb7762022-06-26 19:21:04 +0100105 new MessageActionRow().addComponents([
106 new MessageButton()
107 .setCustomId("prevYear")
108 .setLabel((currentYear - 1).toString())
109 .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
110 .setStyle("SECONDARY"),
111 new MessageButton()
112 .setCustomId("prevPage")
113 .setLabel("Previous page")
114 .setStyle("PRIMARY"),
115 new MessageButton()
116 .setCustomId("today")
117 .setLabel("Today")
118 .setStyle("PRIMARY"),
119 new MessageButton()
120 .setCustomId("nextPage")
121 .setLabel("Next page")
122 .setStyle("PRIMARY")
123 .setDisabled(pageIndex >= groups.length - 1 && currentYear === new Date().getFullYear()),
124 new MessageButton()
125 .setCustomId("nextYear")
126 .setLabel((currentYear + 1).toString())
127 .setEmoji(getEmojiByName("CONTROL.RIGHT", "id"))
128 .setStyle("SECONDARY")
pineafan63fc5e22022-08-04 22:04:10 +0100129 .setDisabled(currentYear === new Date().getFullYear())
pineafan4edb7762022-06-26 19:21:04 +0100130 ]), new MessageActionRow().addComponents([
131 new MessageButton()
132 .setLabel("Mod notes")
133 .setCustomId("modNotes")
134 .setStyle("PRIMARY")
135 .setEmoji(getEmojiByName("ICONS.EDIT", "id")),
136 new MessageButton()
137 .setLabel("Filter")
138 .setCustomId("openFilter")
139 .setStyle(openFilterPane ? "SUCCESS" : "PRIMARY")
140 .setEmoji(getEmojiByName("ICONS.FILTER", "id"))
141 ])
pineafan63fc5e22022-08-04 22:04:10 +0100142 ]);
143 const end = "\n\nJanuary " + currentYear.toString() + pageIndicator(
pineafan4edb7762022-06-26 19:21:04 +0100144 Math.max(groups.length, 1),
145 groups.length === 0 ? 1 : pageIndex
pineafane23c4ec2022-07-27 21:56:27 +0100146 ) + (currentYear === new Date().getFullYear() ? monthNames[new Date().getMonth()] : "December"
pineafan63fc5e22022-08-04 22:04:10 +0100147 ) + " " + currentYear.toString();
pineafan4edb7762022-06-26 19:21:04 +0100148 if (groups.length > 0) {
pineafan63fc5e22022-08-04 22:04:10 +0100149 const toRender = groups[Math.min(pageIndex, groups.length - 1)];
pineafan4edb7762022-06-26 19:21:04 +0100150 m = await interaction.editReply({embeds: [new EmojiEmbed()
151 .setEmoji("MEMBER.JOIN")
152 .setTitle("Moderation history for " + member.user.username)
153 .setDescription(`**${toRender.name}**\n\n` + toRender.content.map(c => c.rendered).join("\n") + end)
154 .setStatus("Success")
155 .setFooter({text: (openFilterPane && filteredTypes.length) ? "Filters are currently enabled" : ""})
156 ], components: components});
157 } else {
158 m = await interaction.editReply({embeds: [new EmojiEmbed()
159 .setEmoji("MEMBER.JOIN")
160 .setTitle("Moderation history for " + member.user.username)
pineafan63fc5e22022-08-04 22:04:10 +0100161 .setDescription(`**${currentYear}**\n\n*No events*` + "\n\n" + end)
pineafan4edb7762022-06-26 19:21:04 +0100162 .setStatus("Success")
163 .setFooter({text: (openFilterPane && filteredTypes.length) ? "Filters are currently enabled" : ""})
164 ], components: components});
165 }
166 let i;
167 try {
168 i = await m.awaitMessageComponent({ time: 300000 });
169 } catch (e) {
170 interaction.editReply({embeds: [new EmojiEmbed()
171 .setEmoji("MEMBER.JOIN")
172 .setTitle("Moderation history for " + member.user.username)
173 .setDescription(m.embeds[0].description)
174 .setStatus("Danger")
175 .setFooter({text: "Message timed out"})
176 ]});
pineafan63fc5e22022-08-04 22:04:10 +0100177 return 0;
pineafan4edb7762022-06-26 19:21:04 +0100178 }
pineafan63fc5e22022-08-04 22:04:10 +0100179 i.deferUpdate();
pineafan4edb7762022-06-26 19:21:04 +0100180 if (i.customId === "filter") {
181 filteredTypes = i.values;
182 pageIndex = null;
183 refresh = true;
184 }
185 if (i.customId === "prevYear") { currentYear--; pageIndex = null; refresh = true; }
186 if (i.customId === "nextYear") { currentYear++; pageIndex = null; refresh = true; }
187 if (i.customId === "prevPage") {
188 pageIndex--;
189 if (pageIndex < 0) { pageIndex = null; currentYear--; refresh = true; }
190 }
191 if (i.customId === "nextPage") {
192 pageIndex++;
193 if (pageIndex >= groups.length) { pageIndex = 0; currentYear++; refresh = true; }
194 }
195 if (i.customId === "today") { currentYear = new Date().getFullYear(); pageIndex = null; refresh = true; }
pineafan63fc5e22022-08-04 22:04:10 +0100196 if (i.customId === "modNotes") { return 1; }
197 if (i.customId === "openFilter") { openFilterPane = !openFilterPane; refresh = true; }
pineafan4edb7762022-06-26 19:21:04 +0100198 }
199}
200
201
202const callback = async (interaction: CommandInteraction): Promise<any> => {
203 let m;
pineafan63fc5e22022-08-04 22:04:10 +0100204 const member = (interaction.options.getMember("user")) as Discord.GuildMember;
pineafan4edb7762022-06-26 19:21:04 +0100205 await interaction.reply({embeds: [new EmojiEmbed()
206 .setEmoji("NUCLEUS.LOADING")
pineafane23c4ec2022-07-27 21:56:27 +0100207 .setTitle("Downloading Data")
208 .setStatus("Danger")
pineafan4edb7762022-06-26 19:21:04 +0100209 ], ephemeral: true, fetchReply: true});
210 let note;
211 let firstLoad = true;
212 while (true) {
213 note = await client.database.notes.read(member.guild.id, member.id);
214 if (firstLoad && !note) { await showHistory(member, interaction); }
215 firstLoad = false;
216 m = await interaction.editReply({embeds: [new EmojiEmbed()
217 .setEmoji("MEMBER.JOIN")
218 .setTitle("Mod notes for " + member.user.username)
219 .setDescription(note ? note : "*No note set*")
220 .setStatus("Success")
221 ], components: [new MessageActionRow().addComponents([
222 new MessageButton()
223 .setLabel(`${note ? "Modify" : "Create"} note`)
224 .setStyle("PRIMARY")
225 .setCustomId("modify")
226 .setEmoji(getEmojiByName("ICONS.EDIT", "id")),
227 new MessageButton()
228 .setLabel("View moderation history")
229 .setStyle("PRIMARY")
230 .setCustomId("history")
231 .setEmoji(getEmojiByName("ICONS.HISTORY", "id"))
232 ])]});
233 let i;
234 try {
235 i = await m.awaitMessageComponent({ time: 300000 });
pineafan63fc5e22022-08-04 22:04:10 +0100236 } catch (e) { return; }
pineafan4edb7762022-06-26 19:21:04 +0100237 if (i.customId === "modify") {
pineafan63fc5e22022-08-04 22:04:10 +0100238 await i.showModal(new Discord.Modal().setCustomId("modal").setTitle("Editing moderator note").addComponents(
pineafan02ba0232022-07-24 22:16:15 +0100239 new MessageActionRow<TextInputComponent>().addComponents(new TextInputComponent()
pineafan4edb7762022-06-26 19:21:04 +0100240 .setCustomId("note")
241 .setLabel("Note")
242 .setMaxLength(4000)
243 .setRequired(false)
244 .setStyle("PARAGRAPH")
245 .setValue(note ? note : "")
246 )
pineafan63fc5e22022-08-04 22:04:10 +0100247 ));
pineafan4edb7762022-06-26 19:21:04 +0100248 await interaction.editReply({
249 embeds: [new EmojiEmbed()
250 .setTitle("Mod notes for " + member.user.username)
251 .setDescription("Modal opened. If you can't see it, click back and try again.")
252 .setStatus("Success")
253 .setEmoji("GUILD.TICKET.OPEN")
254 ], components: [new MessageActionRow().addComponents([new MessageButton()
255 .setLabel("Back")
256 .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
257 .setStyle("PRIMARY")
258 .setCustomId("back")
259 ])]
260 });
261 let out;
262 try {
pineafan63fc5e22022-08-04 22:04:10 +0100263 out = await modalInteractionCollector(m, (m) => m.channel.id === interaction.channel.id, (m) => m.customId === "modify");
264 } catch (e) { continue; }
pineafan4edb7762022-06-26 19:21:04 +0100265 if (out.fields) {
pineafan63fc5e22022-08-04 22:04:10 +0100266 const toAdd = out.fields.getTextInputValue("note") || null;
pineafan4edb7762022-06-26 19:21:04 +0100267 await client.database.notes.create(member.guild.id, member.id, toAdd);
pineafan63fc5e22022-08-04 22:04:10 +0100268 } else { continue; }
pineafan4edb7762022-06-26 19:21:04 +0100269 } else if (i.customId === "history") {
270 i.deferUpdate();
pineafan63fc5e22022-08-04 22:04:10 +0100271 if (!await showHistory(member, interaction) ) return;
pineafan4edb7762022-06-26 19:21:04 +0100272 }
273 }
pineafan63fc5e22022-08-04 22:04:10 +0100274};
pineafan4edb7762022-06-26 19:21:04 +0100275
276const check = (interaction: CommandInteraction, defaultCheck: WrappedCheck) => {
pineafan63fc5e22022-08-04 22:04:10 +0100277 const member = (interaction.member as GuildMember);
pineafane23c4ec2022-07-27 21:56:27 +0100278 if (! member.permissions.has("MODERATE_MEMBERS")) throw "You do not have the *Moderate Members* permission";
pineafan63fc5e22022-08-04 22:04:10 +0100279 return true;
280};
pineafan4edb7762022-06-26 19:21:04 +0100281
282export { command };
283export { callback };
284export { check };