blob: c50c6f4d5d9860198f95dc72bfb361a532ca95ef [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
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"}
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
pineafane23c4ec2022-07-27 21:56:27 +0100146 ) + (currentYear === new Date().getFullYear() ? monthNames[new Date().getMonth()] : "December"
pineafan4edb7762022-06-26 19:21:04 +0100147 ) + " " + 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")
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 });
236 } catch (e) { return }
237 if (i.customId === "modify") {
238 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 )
247 ))
248 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 {
pineafane23c4ec2022-07-27 21:56:27 +0100263 out = await modalInteractionCollector(m, (m) => m.channel.id === interaction.channel.id, (m) => m.customId === "modify")
pineafan4edb7762022-06-26 19:21:04 +0100264 } catch (e) { continue }
265 if (out.fields) {
266 let toAdd = out.fields.getTextInputValue("note") || null;
267 await client.database.notes.create(member.guild.id, member.id, toAdd);
268 } else { continue }
269 } else if (i.customId === "history") {
270 i.deferUpdate();
271 if (!await showHistory(member, interaction) ) return
272 }
273 }
274}
275
276const check = (interaction: CommandInteraction, defaultCheck: WrappedCheck) => {
277 let 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";
pineafan4edb7762022-06-26 19:21:04 +0100279 return true
280}
281
282export { command };
283export { callback };
284export { check };