blob: 68b3ce72250b093c974855ce6a35b7c8be38dcfc [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 EmojiEmbed from "../../utils/generateEmojiEmbed.js";
5import getEmojiByName from "../../utils/getEmojiByName.js";
6import client from "../../utils/client.js";
pineafan63fc5e22022-08-04 22:04:10 +01007import { modalInteractionCollector } from "../../utils/dualCollector.js";
8import pageIndicator from "../../utils/createPageIndicator.js";
pineafan4edb7762022-06-26 19:21:04 +01009
10const command = (builder: SlashCommandSubcommandBuilder) =>
11 builder
pineafan63fc5e22022-08-04 22:04:10 +010012 .setName("info")
13 .setDescription("Shows moderator information about a user")
14 .addUserOption(option => option.setName("user").setDescription("The user to get information about").setRequired(true));
pineafan4edb7762022-06-26 19:21:04 +010015
pineafan4edb7762022-06-26 19:21:04 +010016const types = {
17 "warn": {emoji: "PUNISH.WARN.YELLOW", text: "Warned"},
18 "mute": {emoji: "PUNISH.MUTE.YELLOW", text: "Muted"},
pineafan73a7c4a2022-07-24 10:38:04 +010019 "unmute": {emoji: "PUNISH.MUTE.GREEN", text: "Unmuted"},
pineafan4edb7762022-06-26 19:21:04 +010020 "join": {emoji: "MEMBER.JOIN", text: "Joined"},
21 "leave": {emoji: "MEMBER.LEAVE", text: "Left"},
22 "kick": {emoji: "MEMBER.KICK", text: "Kicked"},
23 "softban": {emoji: "PUNISH.SOFTBAN", text: "Softbanned"},
24 "ban": {emoji: "MEMBER.BAN", text: "Banned"},
25 "unban": {emoji: "MEMBER.UNBAN", text: "Unbanned"},
26 "purge": {emoji: "PUNISH.CLEARHISTORY", text: "Messages cleared"},
27 "nickname": {emoji: "PUNISH.NICKNAME.YELLOW", text: "Nickname changed"}
pineafan63fc5e22022-08-04 22:04:10 +010028};
pineafan4edb7762022-06-26 19:21:04 +010029
30function historyToString(history: HistorySchema) {
31 let s = `${getEmojiByName(types[history.type].emoji)} ${
32 history.amount ? (history.amount + " ") : ""
33 }${
34 types[history.type].text
35 } on <t:${Math.round(history.occurredAt.getTime() / 1000)}:F>`;
36 if (history.moderator) { s += ` by <@${history.moderator}>`; }
37 if (history.reason) { s += `\n**Reason:**\n> ${history.reason}`; }
38 if (history.before) { s += `\n**Before:**\n> ${history.before}`; }
39 if (history.after) { s += `\n**After:**\n> ${history.after}`; }
40 return s + "\n";
41}
42
43
44class TimelineSection {
45 name: string;
pineafan63fc5e22022-08-04 22:04:10 +010046 content: {data: HistorySchema, rendered: string}[] = [];
pineafan4edb7762022-06-26 19:21:04 +010047
pineafan63fc5e22022-08-04 22:04:10 +010048 addContent = (content: {data: HistorySchema, rendered: string}) => { this.content.push(content); return this; };
pineafan4edb7762022-06-26 19:21:04 +010049 contentLength = () => { return this.content.reduce((acc, cur) => acc + cur.rendered.length, 0); };
50 generateName = () => {
pineafan63fc5e22022-08-04 22:04:10 +010051 const first = Math.round(this.content[0].data.occurredAt.getTime() / 1000);
52 const last = Math.round(this.content[this.content.length - 1].data.occurredAt.getTime() / 1000);
pineafan4edb7762022-06-26 19:21:04 +010053 if (first === last) { return this.name = `<t:${first}:F>`; }
54 return this.name = `<t:${first}:F> - <t:${last}:F>`;
pineafan63fc5e22022-08-04 22:04:10 +010055 };
pineafan4edb7762022-06-26 19:21:04 +010056}
57
58const monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
59
60async function showHistory(member, interaction: CommandInteraction) {
61 let currentYear = new Date().getFullYear();
62 let pageIndex = null;
63 let m, history, current;
64 let refresh = true;
65 let filteredTypes = [];
66 let openFilterPane = false;
67 while (true) {
68 if (refresh) {
69 history = await client.database.history.read(member.guild.id, member.id, currentYear);
70 history = history.sort((a, b) => b.occurredAt.getTime() - a.occurredAt.getTime()).reverse();
71 if (openFilterPane) {
pineafan63fc5e22022-08-04 22:04:10 +010072 let tempFilteredTypes = filteredTypes;
pineafan4edb7762022-06-26 19:21:04 +010073 if (filteredTypes.length === 0) { tempFilteredTypes = Object.keys(types); }
pineafan63fc5e22022-08-04 22:04:10 +010074 history = history.filter(h => tempFilteredTypes.includes(h.type));
75 }
pineafan4edb7762022-06-26 19:21:04 +010076 refresh = false;
77 }
pineafan63fc5e22022-08-04 22:04:10 +010078 const groups: TimelineSection[] = [];
pineafan4edb7762022-06-26 19:21:04 +010079 if (history.length > 0) {
pineafan63fc5e22022-08-04 22:04:10 +010080 current = new TimelineSection();
pineafan4edb7762022-06-26 19:21:04 +010081 history.forEach(event => {
82 if (current.contentLength() + historyToString(event).length > 2000 || current.content.length === 5) {
83 groups.push(current);
84 current.generateName();
85 current = new TimelineSection();
86 }
87 current.addContent({data: event, rendered: historyToString(event)});
88 });
89 current.generateName();
90 groups.push(current);
91 if (pageIndex === null) { pageIndex = groups.length - 1; }
92 }
pineafan63fc5e22022-08-04 22:04:10 +010093 const components = (
pineafan4edb7762022-06-26 19:21:04 +010094 openFilterPane ? [
pineafan63fc5e22022-08-04 22:04:10 +010095 new MessageActionRow().addComponents([new Discord.MessageSelectMenu().setOptions(
96 Object.entries(types).map(([key, value]) => ({
pineafan4edb7762022-06-26 19:21:04 +010097 label: value.text,
98 value: key,
99 default: filteredTypes.includes(key),
100 emoji: client.emojis.resolve(getEmojiByName(value.emoji, "id"))
pineafan63fc5e22022-08-04 22:04:10 +0100101 }))
102 ).setMinValues(1).setMaxValues(Object.keys(types).length).setCustomId("filter").setPlaceholder("Select at least one event")])
103 ] : []).concat([
pineafan4edb7762022-06-26 19:21:04 +0100104 new MessageActionRow().addComponents([
105 new MessageButton()
106 .setCustomId("prevYear")
107 .setLabel((currentYear - 1).toString())
108 .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
109 .setStyle("SECONDARY"),
110 new MessageButton()
111 .setCustomId("prevPage")
112 .setLabel("Previous page")
113 .setStyle("PRIMARY"),
114 new MessageButton()
115 .setCustomId("today")
116 .setLabel("Today")
117 .setStyle("PRIMARY"),
118 new MessageButton()
119 .setCustomId("nextPage")
120 .setLabel("Next page")
121 .setStyle("PRIMARY")
122 .setDisabled(pageIndex >= groups.length - 1 && currentYear === new Date().getFullYear()),
123 new MessageButton()
124 .setCustomId("nextYear")
125 .setLabel((currentYear + 1).toString())
126 .setEmoji(getEmojiByName("CONTROL.RIGHT", "id"))
127 .setStyle("SECONDARY")
pineafan63fc5e22022-08-04 22:04:10 +0100128 .setDisabled(currentYear === new Date().getFullYear())
pineafan4edb7762022-06-26 19:21:04 +0100129 ]), new MessageActionRow().addComponents([
130 new MessageButton()
131 .setLabel("Mod notes")
132 .setCustomId("modNotes")
133 .setStyle("PRIMARY")
134 .setEmoji(getEmojiByName("ICONS.EDIT", "id")),
135 new MessageButton()
136 .setLabel("Filter")
137 .setCustomId("openFilter")
138 .setStyle(openFilterPane ? "SUCCESS" : "PRIMARY")
139 .setEmoji(getEmojiByName("ICONS.FILTER", "id"))
140 ])
pineafan63fc5e22022-08-04 22:04:10 +0100141 ]);
142 const end = "\n\nJanuary " + currentYear.toString() + pageIndicator(
pineafan4edb7762022-06-26 19:21:04 +0100143 Math.max(groups.length, 1),
144 groups.length === 0 ? 1 : pageIndex
pineafane23c4ec2022-07-27 21:56:27 +0100145 ) + (currentYear === new Date().getFullYear() ? monthNames[new Date().getMonth()] : "December"
pineafan63fc5e22022-08-04 22:04:10 +0100146 ) + " " + currentYear.toString();
pineafan4edb7762022-06-26 19:21:04 +0100147 if (groups.length > 0) {
pineafan63fc5e22022-08-04 22:04:10 +0100148 const toRender = groups[Math.min(pageIndex, groups.length - 1)];
pineafan4edb7762022-06-26 19:21:04 +0100149 m = await interaction.editReply({embeds: [new EmojiEmbed()
150 .setEmoji("MEMBER.JOIN")
151 .setTitle("Moderation history for " + member.user.username)
152 .setDescription(`**${toRender.name}**\n\n` + toRender.content.map(c => c.rendered).join("\n") + end)
153 .setStatus("Success")
154 .setFooter({text: (openFilterPane && filteredTypes.length) ? "Filters are currently enabled" : ""})
155 ], components: components});
156 } else {
157 m = await interaction.editReply({embeds: [new EmojiEmbed()
158 .setEmoji("MEMBER.JOIN")
159 .setTitle("Moderation history for " + member.user.username)
pineafan63fc5e22022-08-04 22:04:10 +0100160 .setDescription(`**${currentYear}**\n\n*No events*` + "\n\n" + end)
pineafan4edb7762022-06-26 19:21:04 +0100161 .setStatus("Success")
162 .setFooter({text: (openFilterPane && filteredTypes.length) ? "Filters are currently enabled" : ""})
163 ], components: components});
164 }
165 let i;
166 try {
167 i = await m.awaitMessageComponent({ time: 300000 });
168 } catch (e) {
169 interaction.editReply({embeds: [new EmojiEmbed()
170 .setEmoji("MEMBER.JOIN")
171 .setTitle("Moderation history for " + member.user.username)
172 .setDescription(m.embeds[0].description)
173 .setStatus("Danger")
174 .setFooter({text: "Message timed out"})
175 ]});
pineafan63fc5e22022-08-04 22:04:10 +0100176 return 0;
pineafan4edb7762022-06-26 19:21:04 +0100177 }
pineafan63fc5e22022-08-04 22:04:10 +0100178 i.deferUpdate();
pineafan4edb7762022-06-26 19:21:04 +0100179 if (i.customId === "filter") {
180 filteredTypes = i.values;
181 pageIndex = null;
182 refresh = true;
183 }
184 if (i.customId === "prevYear") { currentYear--; pageIndex = null; refresh = true; }
185 if (i.customId === "nextYear") { currentYear++; pageIndex = null; refresh = true; }
186 if (i.customId === "prevPage") {
187 pageIndex--;
188 if (pageIndex < 0) { pageIndex = null; currentYear--; refresh = true; }
189 }
190 if (i.customId === "nextPage") {
191 pageIndex++;
192 if (pageIndex >= groups.length) { pageIndex = 0; currentYear++; refresh = true; }
193 }
194 if (i.customId === "today") { currentYear = new Date().getFullYear(); pageIndex = null; refresh = true; }
pineafan63fc5e22022-08-04 22:04:10 +0100195 if (i.customId === "modNotes") { return 1; }
196 if (i.customId === "openFilter") { openFilterPane = !openFilterPane; refresh = true; }
pineafan4edb7762022-06-26 19:21:04 +0100197 }
198}
199
200
pineafanbd02b4a2022-08-05 22:01:38 +0100201const callback = async (interaction: CommandInteraction): Promise<void | unknown> => {
pineafan4edb7762022-06-26 19:21:04 +0100202 let m;
pineafan63fc5e22022-08-04 22:04:10 +0100203 const member = (interaction.options.getMember("user")) as Discord.GuildMember;
pineafan4edb7762022-06-26 19:21:04 +0100204 await interaction.reply({embeds: [new EmojiEmbed()
205 .setEmoji("NUCLEUS.LOADING")
pineafane23c4ec2022-07-27 21:56:27 +0100206 .setTitle("Downloading Data")
207 .setStatus("Danger")
pineafan4edb7762022-06-26 19:21:04 +0100208 ], ephemeral: true, fetchReply: true});
209 let note;
210 let firstLoad = true;
211 while (true) {
212 note = await client.database.notes.read(member.guild.id, member.id);
213 if (firstLoad && !note) { await showHistory(member, interaction); }
214 firstLoad = false;
215 m = await interaction.editReply({embeds: [new EmojiEmbed()
216 .setEmoji("MEMBER.JOIN")
217 .setTitle("Mod notes for " + member.user.username)
218 .setDescription(note ? note : "*No note set*")
219 .setStatus("Success")
220 ], components: [new MessageActionRow().addComponents([
221 new MessageButton()
222 .setLabel(`${note ? "Modify" : "Create"} note`)
223 .setStyle("PRIMARY")
224 .setCustomId("modify")
225 .setEmoji(getEmojiByName("ICONS.EDIT", "id")),
226 new MessageButton()
227 .setLabel("View moderation history")
228 .setStyle("PRIMARY")
229 .setCustomId("history")
230 .setEmoji(getEmojiByName("ICONS.HISTORY", "id"))
231 ])]});
232 let i;
233 try {
234 i = await m.awaitMessageComponent({ time: 300000 });
pineafan63fc5e22022-08-04 22:04:10 +0100235 } catch (e) { return; }
pineafan4edb7762022-06-26 19:21:04 +0100236 if (i.customId === "modify") {
pineafan63fc5e22022-08-04 22:04:10 +0100237 await i.showModal(new Discord.Modal().setCustomId("modal").setTitle("Editing moderator note").addComponents(
pineafan02ba0232022-07-24 22:16:15 +0100238 new MessageActionRow<TextInputComponent>().addComponents(new TextInputComponent()
pineafan4edb7762022-06-26 19:21:04 +0100239 .setCustomId("note")
240 .setLabel("Note")
241 .setMaxLength(4000)
242 .setRequired(false)
243 .setStyle("PARAGRAPH")
244 .setValue(note ? note : "")
245 )
pineafan63fc5e22022-08-04 22:04:10 +0100246 ));
pineafan4edb7762022-06-26 19:21:04 +0100247 await interaction.editReply({
248 embeds: [new EmojiEmbed()
249 .setTitle("Mod notes for " + member.user.username)
250 .setDescription("Modal opened. If you can't see it, click back and try again.")
251 .setStatus("Success")
252 .setEmoji("GUILD.TICKET.OPEN")
253 ], components: [new MessageActionRow().addComponents([new MessageButton()
254 .setLabel("Back")
255 .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
256 .setStyle("PRIMARY")
257 .setCustomId("back")
258 ])]
259 });
260 let out;
261 try {
pineafan63fc5e22022-08-04 22:04:10 +0100262 out = await modalInteractionCollector(m, (m) => m.channel.id === interaction.channel.id, (m) => m.customId === "modify");
263 } catch (e) { continue; }
pineafan4edb7762022-06-26 19:21:04 +0100264 if (out.fields) {
pineafan63fc5e22022-08-04 22:04:10 +0100265 const toAdd = out.fields.getTextInputValue("note") || null;
pineafan4edb7762022-06-26 19:21:04 +0100266 await client.database.notes.create(member.guild.id, member.id, toAdd);
pineafan63fc5e22022-08-04 22:04:10 +0100267 } else { continue; }
pineafan4edb7762022-06-26 19:21:04 +0100268 } else if (i.customId === "history") {
269 i.deferUpdate();
pineafan63fc5e22022-08-04 22:04:10 +0100270 if (!await showHistory(member, interaction) ) return;
pineafan4edb7762022-06-26 19:21:04 +0100271 }
272 }
pineafan63fc5e22022-08-04 22:04:10 +0100273};
pineafan4edb7762022-06-26 19:21:04 +0100274
pineafanbd02b4a2022-08-05 22:01:38 +0100275const check = (interaction: CommandInteraction) => {
pineafan63fc5e22022-08-04 22:04:10 +0100276 const member = (interaction.member as GuildMember);
pineafane23c4ec2022-07-27 21:56:27 +0100277 if (! member.permissions.has("MODERATE_MEMBERS")) throw "You do not have the *Moderate Members* permission";
pineafan63fc5e22022-08-04 22:04:10 +0100278 return true;
279};
pineafan4edb7762022-06-26 19:21:04 +0100280
281export { command };
282export { callback };
283export { check };