blob: c3a438829476a61494de312d0dc503441c658926 [file] [log] [blame]
pineafan4edb7762022-06-26 19:21:04 +01001import { HistorySchema } from '../../utils/database';
2import Discord, { CommandInteraction, GuildMember, MessageActionRow, MessageButton, TextInputComponent } from "discord.js";
3import { SlashCommandSubcommandBuilder, SelectMenuOption } from "@discordjs/builders";
4import { WrappedCheck } from "jshaiku";
5import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
6import getEmojiByName from "../../utils/getEmojiByName.js";
7import client from "../../utils/client.js";
8import { modalInteractionCollector } from '../../utils/dualCollector.js';
9import pageIndicator from '../../utils/createPageIndicator.js';
10
11const command = (builder: SlashCommandSubcommandBuilder) =>
12 builder
13 .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))
16
17
18const types = {
19 "warn": {emoji: "PUNISH.WARN.YELLOW", text: "Warned"},
20 "mute": {emoji: "PUNISH.MUTE.YELLOW", text: "Muted"},
21 "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"}
29}
30
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;
47 content: {data: HistorySchema, rendered: string}[] = []
48
49 addContent = (content: {data: HistorySchema, rendered: string}) => { this.content.push(content); return this; }
50 contentLength = () => { return this.content.reduce((acc, cur) => acc + cur.rendered.length, 0); };
51 generateName = () => {
52 let first = Math.round(this.content[0].data.occurredAt.getTime() / 1000)
53 let last = Math.round(this.content[this.content.length - 1].data.occurredAt.getTime() / 1000)
54 if (first === last) { return this.name = `<t:${first}:F>`; }
55 return this.name = `<t:${first}:F> - <t:${last}:F>`;
56 }
57}
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) {
73 let tempFilteredTypes = filteredTypes
74 if (filteredTypes.length === 0) { tempFilteredTypes = Object.keys(types); }
75 history = history.filter(h => tempFilteredTypes.includes(h.type))
76 };
77 refresh = false;
78 }
79 let groups: TimelineSection[] = []
80 if (history.length > 0) {
81 current = new TimelineSection()
82 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 }
94 let components = (
95 openFilterPane ? [
96 new MessageActionRow().addComponents([new Discord.MessageSelectMenu().setOptions(
97 Object.entries(types).map(([key, value]) => ({
98 label: value.text,
99 value: key,
100 default: filteredTypes.includes(key),
101 emoji: client.emojis.resolve(getEmojiByName(value.emoji, "id"))
102 }))
103 ).setMinValues(1).setMaxValues(Object.keys(types).length).setCustomId("filter").setPlaceholder("Select at least one event")])
104 ] : []).concat([
105 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")
129 .setDisabled(currentYear === new Date().getFullYear()),
130 ]), 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 ])
142 ])
143 let end = "\n\nJanuary " + currentYear.toString() + pageIndicator(
144 Math.max(groups.length, 1),
145 groups.length === 0 ? 1 : pageIndex
146 ) + (currentYear == new Date().getFullYear() ? monthNames[new Date().getMonth()] : "December"
147 ) + " " + currentYear.toString()
148 if (groups.length > 0) {
149 let toRender = groups[Math.min(pageIndex, groups.length - 1)]
150 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)
161 .setDescription(`**${currentYear}**\n\n*No events*` + `\n\n` + end)
162 .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 ]});
177 return 0
178 }
179 i.deferUpdate()
180 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; }
196 if (i.customId === "modNotes") { return 1 }
197 if (i.customId === "openFilter") { openFilterPane = !openFilterPane; refresh = true }
198 }
199}
200
201
202const callback = async (interaction: CommandInteraction): Promise<any> => {
203 let m;
204 let member = (interaction.options.getMember("user")) as Discord.GuildMember;
205 await interaction.reply({embeds: [new EmojiEmbed()
206 .setEmoji("NUCLEUS.LOADING")
207 .setTitle("Downloading data...")
208 .setStatus("Success")
209 ], 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 });
236 } catch (e) { return }
237 if (i.customId === "modify") {
238 await i.showModal(new Discord.Modal().setCustomId("modal").setTitle(`Editing moderator note`).addComponents(
239 // @ts-ignore
240 new MessageActionRow().addComponents(new TextInputComponent()
241 .setCustomId("note")
242 .setLabel("Note")
243 .setMaxLength(4000)
244 .setRequired(false)
245 .setStyle("PARAGRAPH")
246 .setValue(note ? note : "")
247 )
248 ))
249 await interaction.editReply({
250 embeds: [new EmojiEmbed()
251 .setTitle("Mod notes for " + member.user.username)
252 .setDescription("Modal opened. If you can't see it, click back and try again.")
253 .setStatus("Success")
254 .setEmoji("GUILD.TICKET.OPEN")
255 ], components: [new MessageActionRow().addComponents([new MessageButton()
256 .setLabel("Back")
257 .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
258 .setStyle("PRIMARY")
259 .setCustomId("back")
260 ])]
261 });
262 let out;
263 try {
264 out = await modalInteractionCollector(m, (m) => m.channel.id == interaction.channel.id, (m) => m.customId == "modify")
265 } catch (e) { continue }
266 if (out.fields) {
267 let toAdd = out.fields.getTextInputValue("note") || null;
268 await client.database.notes.create(member.guild.id, member.id, toAdd);
269 } else { continue }
270 } else if (i.customId === "history") {
271 i.deferUpdate();
272 if (!await showHistory(member, interaction) ) return
273 }
274 }
275}
276
277const check = (interaction: CommandInteraction, defaultCheck: WrappedCheck) => {
278 let member = (interaction.member as GuildMember)
279 if (! member.permissions.has("MODERATE_MEMBERS")) throw "You do not have the Moderate members permission";
280 return true
281}
282
283export { command };
284export { callback };
285export { check };