blob: b7f4b7479624bd9a3a3800cdcbe694814b6b84ee [file] [log] [blame]
pineafan4edb7762022-06-26 19:21:04 +01001import { HistorySchema } from '../../utils/database';
2import 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";
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"},
pineafan73a7c4a2022-07-24 10:38:04 +010021 "unmute": {emoji: "PUNISH.MUTE.GREEN", text: "Unmuted"},
pineafan4edb7762022-06-26 19:21:04 +010022 "join": {emoji: "MEMBER.JOIN", text: "Joined"},
23 "leave": {emoji: "MEMBER.LEAVE", text: "Left"},
24 "kick": {emoji: "MEMBER.KICK", text: "Kicked"},
25 "softban": {emoji: "PUNISH.SOFTBAN", text: "Softbanned"},
26 "ban": {emoji: "MEMBER.BAN", text: "Banned"},
27 "unban": {emoji: "MEMBER.UNBAN", text: "Unbanned"},
28 "purge": {emoji: "PUNISH.CLEARHISTORY", text: "Messages cleared"},
29 "nickname": {emoji: "PUNISH.NICKNAME.YELLOW", text: "Nickname changed"}
30}
31
32function historyToString(history: HistorySchema) {
33 let s = `${getEmojiByName(types[history.type].emoji)} ${
34 history.amount ? (history.amount + " ") : ""
35 }${
36 types[history.type].text
37 } on <t:${Math.round(history.occurredAt.getTime() / 1000)}:F>`;
38 if (history.moderator) { s += ` by <@${history.moderator}>`; }
39 if (history.reason) { s += `\n**Reason:**\n> ${history.reason}`; }
40 if (history.before) { s += `\n**Before:**\n> ${history.before}`; }
41 if (history.after) { s += `\n**After:**\n> ${history.after}`; }
42 return s + "\n";
43}
44
45
46class TimelineSection {
47 name: string;
48 content: {data: HistorySchema, rendered: string}[] = []
49
50 addContent = (content: {data: HistorySchema, rendered: string}) => { this.content.push(content); return this; }
51 contentLength = () => { return this.content.reduce((acc, cur) => acc + cur.rendered.length, 0); };
52 generateName = () => {
53 let first = Math.round(this.content[0].data.occurredAt.getTime() / 1000)
54 let last = Math.round(this.content[this.content.length - 1].data.occurredAt.getTime() / 1000)
55 if (first === last) { return this.name = `<t:${first}:F>`; }
56 return this.name = `<t:${first}:F> - <t:${last}:F>`;
57 }
58}
59
60const monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
61
62async function showHistory(member, interaction: CommandInteraction) {
63 let currentYear = new Date().getFullYear();
64 let pageIndex = null;
65 let m, history, current;
66 let refresh = true;
67 let filteredTypes = [];
68 let openFilterPane = false;
69 while (true) {
70 if (refresh) {
71 history = await client.database.history.read(member.guild.id, member.id, currentYear);
72 history = history.sort((a, b) => b.occurredAt.getTime() - a.occurredAt.getTime()).reverse();
73 if (openFilterPane) {
74 let tempFilteredTypes = filteredTypes
75 if (filteredTypes.length === 0) { tempFilteredTypes = Object.keys(types); }
76 history = history.filter(h => tempFilteredTypes.includes(h.type))
77 };
78 refresh = false;
79 }
80 let groups: TimelineSection[] = []
81 if (history.length > 0) {
82 current = new TimelineSection()
83 history.forEach(event => {
84 if (current.contentLength() + historyToString(event).length > 2000 || current.content.length === 5) {
85 groups.push(current);
86 current.generateName();
87 current = new TimelineSection();
88 }
89 current.addContent({data: event, rendered: historyToString(event)});
90 });
91 current.generateName();
92 groups.push(current);
93 if (pageIndex === null) { pageIndex = groups.length - 1; }
94 }
95 let components = (
96 openFilterPane ? [
97 new MessageActionRow().addComponents([new Discord.MessageSelectMenu().setOptions(
98 Object.entries(types).map(([key, value]) => ({
99 label: value.text,
100 value: key,
101 default: filteredTypes.includes(key),
102 emoji: client.emojis.resolve(getEmojiByName(value.emoji, "id"))
103 }))
104 ).setMinValues(1).setMaxValues(Object.keys(types).length).setCustomId("filter").setPlaceholder("Select at least one event")])
105 ] : []).concat([
106 new MessageActionRow().addComponents([
107 new MessageButton()
108 .setCustomId("prevYear")
109 .setLabel((currentYear - 1).toString())
110 .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
111 .setStyle("SECONDARY"),
112 new MessageButton()
113 .setCustomId("prevPage")
114 .setLabel("Previous page")
115 .setStyle("PRIMARY"),
116 new MessageButton()
117 .setCustomId("today")
118 .setLabel("Today")
119 .setStyle("PRIMARY"),
120 new MessageButton()
121 .setCustomId("nextPage")
122 .setLabel("Next page")
123 .setStyle("PRIMARY")
124 .setDisabled(pageIndex >= groups.length - 1 && currentYear === new Date().getFullYear()),
125 new MessageButton()
126 .setCustomId("nextYear")
127 .setLabel((currentYear + 1).toString())
128 .setEmoji(getEmojiByName("CONTROL.RIGHT", "id"))
129 .setStyle("SECONDARY")
130 .setDisabled(currentYear === new Date().getFullYear()),
131 ]), new MessageActionRow().addComponents([
132 new MessageButton()
133 .setLabel("Mod notes")
134 .setCustomId("modNotes")
135 .setStyle("PRIMARY")
136 .setEmoji(getEmojiByName("ICONS.EDIT", "id")),
137 new MessageButton()
138 .setLabel("Filter")
139 .setCustomId("openFilter")
140 .setStyle(openFilterPane ? "SUCCESS" : "PRIMARY")
141 .setEmoji(getEmojiByName("ICONS.FILTER", "id"))
142 ])
143 ])
144 let end = "\n\nJanuary " + currentYear.toString() + pageIndicator(
145 Math.max(groups.length, 1),
146 groups.length === 0 ? 1 : pageIndex
147 ) + (currentYear == new Date().getFullYear() ? monthNames[new Date().getMonth()] : "December"
148 ) + " " + currentYear.toString()
149 if (groups.length > 0) {
150 let toRender = groups[Math.min(pageIndex, groups.length - 1)]
151 m = await interaction.editReply({embeds: [new EmojiEmbed()
152 .setEmoji("MEMBER.JOIN")
153 .setTitle("Moderation history for " + member.user.username)
154 .setDescription(`**${toRender.name}**\n\n` + toRender.content.map(c => c.rendered).join("\n") + end)
155 .setStatus("Success")
156 .setFooter({text: (openFilterPane && filteredTypes.length) ? "Filters are currently enabled" : ""})
157 ], components: components});
158 } else {
159 m = await interaction.editReply({embeds: [new EmojiEmbed()
160 .setEmoji("MEMBER.JOIN")
161 .setTitle("Moderation history for " + member.user.username)
162 .setDescription(`**${currentYear}**\n\n*No events*` + `\n\n` + end)
163 .setStatus("Success")
164 .setFooter({text: (openFilterPane && filteredTypes.length) ? "Filters are currently enabled" : ""})
165 ], components: components});
166 }
167 let i;
168 try {
169 i = await m.awaitMessageComponent({ time: 300000 });
170 } catch (e) {
171 interaction.editReply({embeds: [new EmojiEmbed()
172 .setEmoji("MEMBER.JOIN")
173 .setTitle("Moderation history for " + member.user.username)
174 .setDescription(m.embeds[0].description)
175 .setStatus("Danger")
176 .setFooter({text: "Message timed out"})
177 ]});
178 return 0
179 }
180 i.deferUpdate()
181 if (i.customId === "filter") {
182 filteredTypes = i.values;
183 pageIndex = null;
184 refresh = true;
185 }
186 if (i.customId === "prevYear") { currentYear--; pageIndex = null; refresh = true; }
187 if (i.customId === "nextYear") { currentYear++; pageIndex = null; refresh = true; }
188 if (i.customId === "prevPage") {
189 pageIndex--;
190 if (pageIndex < 0) { pageIndex = null; currentYear--; refresh = true; }
191 }
192 if (i.customId === "nextPage") {
193 pageIndex++;
194 if (pageIndex >= groups.length) { pageIndex = 0; currentYear++; refresh = true; }
195 }
196 if (i.customId === "today") { currentYear = new Date().getFullYear(); pageIndex = null; refresh = true; }
197 if (i.customId === "modNotes") { return 1 }
198 if (i.customId === "openFilter") { openFilterPane = !openFilterPane; refresh = true }
199 }
200}
201
202
203const callback = async (interaction: CommandInteraction): Promise<any> => {
204 let m;
205 let member = (interaction.options.getMember("user")) as Discord.GuildMember;
206 await interaction.reply({embeds: [new EmojiEmbed()
207 .setEmoji("NUCLEUS.LOADING")
208 .setTitle("Downloading data...")
209 .setStatus("Success")
210 ], ephemeral: true, fetchReply: true});
211 let note;
212 let firstLoad = true;
213 while (true) {
214 note = await client.database.notes.read(member.guild.id, member.id);
215 if (firstLoad && !note) { await showHistory(member, interaction); }
216 firstLoad = false;
217 m = await interaction.editReply({embeds: [new EmojiEmbed()
218 .setEmoji("MEMBER.JOIN")
219 .setTitle("Mod notes for " + member.user.username)
220 .setDescription(note ? note : "*No note set*")
221 .setStatus("Success")
222 ], components: [new MessageActionRow().addComponents([
223 new MessageButton()
224 .setLabel(`${note ? "Modify" : "Create"} note`)
225 .setStyle("PRIMARY")
226 .setCustomId("modify")
227 .setEmoji(getEmojiByName("ICONS.EDIT", "id")),
228 new MessageButton()
229 .setLabel("View moderation history")
230 .setStyle("PRIMARY")
231 .setCustomId("history")
232 .setEmoji(getEmojiByName("ICONS.HISTORY", "id"))
233 ])]});
234 let i;
235 try {
236 i = await m.awaitMessageComponent({ time: 300000 });
237 } catch (e) { return }
238 if (i.customId === "modify") {
239 await i.showModal(new Discord.Modal().setCustomId("modal").setTitle(`Editing moderator note`).addComponents(
240 // @ts-ignore
241 new MessageActionRow().addComponents(new TextInputComponent()
242 .setCustomId("note")
243 .setLabel("Note")
244 .setMaxLength(4000)
245 .setRequired(false)
246 .setStyle("PARAGRAPH")
247 .setValue(note ? note : "")
248 )
249 ))
250 await interaction.editReply({
251 embeds: [new EmojiEmbed()
252 .setTitle("Mod notes for " + member.user.username)
253 .setDescription("Modal opened. If you can't see it, click back and try again.")
254 .setStatus("Success")
255 .setEmoji("GUILD.TICKET.OPEN")
256 ], components: [new MessageActionRow().addComponents([new MessageButton()
257 .setLabel("Back")
258 .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
259 .setStyle("PRIMARY")
260 .setCustomId("back")
261 ])]
262 });
263 let out;
264 try {
265 out = await modalInteractionCollector(m, (m) => m.channel.id == interaction.channel.id, (m) => m.customId == "modify")
266 } catch (e) { continue }
267 if (out.fields) {
268 let toAdd = out.fields.getTextInputValue("note") || null;
269 await client.database.notes.create(member.guild.id, member.id, toAdd);
270 } else { continue }
271 } else if (i.customId === "history") {
272 i.deferUpdate();
273 if (!await showHistory(member, interaction) ) return
274 }
275 }
276}
277
278const check = (interaction: CommandInteraction, defaultCheck: WrappedCheck) => {
279 let member = (interaction.member as GuildMember)
280 if (! member.permissions.has("MODERATE_MEMBERS")) throw "You do not have the Moderate members permission";
281 return true
282}
283
284export { command };
285export { callback };
286export { check };