blob: c1728c343f63c3a0458b845f94ce7554410c6b25 [file] [log] [blame]
TheCodedProf9c51a7e2023-02-27 17:11:13 -05001import { ButtonStyle, CommandInteraction, ComponentType, GuildMember, Message, MessageComponentInteraction } from "discord.js";
pineafan63fc5e22022-08-04 22:04:10 +01002import type Discord from "discord.js";
3import { Collection, MongoClient } from "mongodb";
pineafana2e39c72023-02-21 18:37:32 +00004import config from "../config/main.js";
TheCodedProf633866f2023-02-03 17:06:00 -05005import client from "../utils/client.js";
TheCodedProf088b1b22023-02-28 17:31:11 -05006import * as crypto from "crypto";
TheCodedProff8ef7942023-03-03 15:32:32 -05007import _ from "lodash";
8import defaultData from '../config/default.js';
TheCodedProf75276572023-03-04 13:49:16 -05009
TheCodedProffaae5332023-03-01 18:16:05 -050010const username = encodeURIComponent(config.mongoOptions.username);
11const password = encodeURIComponent(config.mongoOptions.password);
Samuel Shuertd66098b2023-03-04 14:05:26 -050012
TheCodedProf78b90332023-03-04 14:02:21 -050013const mongoClient = new MongoClient(username ? `mongodb://${username}:${password}@${config.mongoOptions.host}?authMechanism=DEFAULT` : `mongodb://${config.mongoOptions.host}`, {authSource: config.mongoOptions.authSource});
pineafan63fc5e22022-08-04 22:04:10 +010014await mongoClient.connect();
TheCodedProf8d577fa2023-03-01 13:06:40 -050015const database = mongoClient.db();
pineafan6fb3e072022-05-20 19:27:23 +010016
TheCodedProf78b90332023-03-04 14:02:21 -050017const collectionOptions = { authdb: config.mongoOptions.authSource, w: "majority" };
TheCodedProf75c51be2023-03-03 17:18:18 -050018const getIV = () => crypto.randomBytes(16);
TheCodedProffaae5332023-03-01 18:16:05 -050019
Samuel Shuertcc63dee2023-03-03 18:54:29 -050020
pineafan4edb7762022-06-26 19:21:04 +010021export class Guilds {
pineafan6fb3e072022-05-20 19:27:23 +010022 guilds: Collection<GuildConfig>;
TheCodedProff8ef7942023-03-03 15:32:32 -050023 defaultData: GuildConfig;
pineafan63fc5e22022-08-04 22:04:10 +010024
25 constructor() {
pineafan4edb7762022-06-26 19:21:04 +010026 this.guilds = database.collection<GuildConfig>("guilds");
TheCodedProff8ef7942023-03-03 15:32:32 -050027 this.defaultData = defaultData;
pineafan63fc5e22022-08-04 22:04:10 +010028 }
29
Skyler Greyad002172022-08-16 18:48:26 +010030 async read(guild: string): Promise<GuildConfig> {
TheCodedProff8ef7942023-03-03 15:32:32 -050031 // console.log("Guild read")
pineafan63fc5e22022-08-04 22:04:10 +010032 const entry = await this.guilds.findOne({ id: guild });
TheCodedProf9f4cf9f2023-03-04 14:18:19 -050033 const data = _.cloneDeep(this.defaultData);
TheCodedProff8ef7942023-03-03 15:32:32 -050034 return _.merge(data, entry ?? {});
pineafan6fb3e072022-05-20 19:27:23 +010035 }
36
Skyler Grey11236ba2022-08-08 21:13:33 +010037 async write(guild: string, set: object | null, unset: string[] | string = []) {
TheCodedProff8ef7942023-03-03 15:32:32 -050038 // console.log("Guild write")
pineafan63fc5e22022-08-04 22:04:10 +010039 // eslint-disable-next-line @typescript-eslint/no-explicit-any
40 const uo: Record<string, any> = {};
41 if (!Array.isArray(unset)) unset = [unset];
42 for (const key of unset) {
pineafan0bc04162022-07-25 17:22:26 +010043 uo[key] = null;
pineafan6702cef2022-06-13 17:52:37 +010044 }
Skyler Grey75ea9172022-08-06 10:22:23 +010045 const out = { $set: {}, $unset: {} };
46 if (set) out.$set = set;
47 if (unset.length) out.$unset = uo;
pineafan0bc04162022-07-25 17:22:26 +010048 await this.guilds.updateOne({ id: guild }, out, { upsert: true });
pineafan6702cef2022-06-13 17:52:37 +010049 }
50
pineafan63fc5e22022-08-04 22:04:10 +010051 // eslint-disable-next-line @typescript-eslint/no-explicit-any
pineafan6702cef2022-06-13 17:52:37 +010052 async append(guild: string, key: string, value: any) {
TheCodedProff8ef7942023-03-03 15:32:32 -050053 // console.log("Guild append")
pineafan6702cef2022-06-13 17:52:37 +010054 if (Array.isArray(value)) {
Skyler Grey75ea9172022-08-06 10:22:23 +010055 await this.guilds.updateOne(
56 { id: guild },
57 {
58 $addToSet: { [key]: { $each: value } }
59 },
60 { upsert: true }
61 );
pineafan6702cef2022-06-13 17:52:37 +010062 } else {
Skyler Grey75ea9172022-08-06 10:22:23 +010063 await this.guilds.updateOne(
64 { id: guild },
65 {
66 $addToSet: { [key]: value }
67 },
68 { upsert: true }
69 );
pineafan6702cef2022-06-13 17:52:37 +010070 }
71 }
72
Skyler Grey75ea9172022-08-06 10:22:23 +010073 async remove(
74 guild: string,
75 key: string,
Skyler Greyc634e2b2022-08-06 17:50:48 +010076 // eslint-disable-next-line @typescript-eslint/no-explicit-any
Skyler Grey75ea9172022-08-06 10:22:23 +010077 value: any,
78 innerKey?: string | null
79 ) {
TheCodedProff8ef7942023-03-03 15:32:32 -050080 // console.log("Guild remove")
pineafan02ba0232022-07-24 22:16:15 +010081 if (innerKey) {
Skyler Grey75ea9172022-08-06 10:22:23 +010082 await this.guilds.updateOne(
83 { id: guild },
84 {
85 $pull: { [key]: { [innerKey]: { $eq: value } } }
86 },
87 { upsert: true }
88 );
pineafan0bc04162022-07-25 17:22:26 +010089 } else if (Array.isArray(value)) {
Skyler Grey75ea9172022-08-06 10:22:23 +010090 await this.guilds.updateOne(
91 { id: guild },
92 {
93 $pullAll: { [key]: value }
94 },
95 { upsert: true }
96 );
pineafan6702cef2022-06-13 17:52:37 +010097 } else {
Skyler Grey75ea9172022-08-06 10:22:23 +010098 await this.guilds.updateOne(
99 { id: guild },
100 {
101 $pullAll: { [key]: [value] }
102 },
103 { upsert: true }
104 );
pineafan6702cef2022-06-13 17:52:37 +0100105 }
pineafan6fb3e072022-05-20 19:27:23 +0100106 }
pineafane23c4ec2022-07-27 21:56:27 +0100107
108 async delete(guild: string) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500109 // console.log("Guild delete")
pineafane23c4ec2022-07-27 21:56:27 +0100110 await this.guilds.deleteOne({ id: guild });
111 }
pineafan6fb3e072022-05-20 19:27:23 +0100112}
113
TheCodedProfcfe8e9a2023-02-26 17:28:09 -0500114interface TranscriptEmbed {
115 title?: string;
116 description?: string;
117 fields?: {
118 name: string;
119 value: string;
120 inline: boolean;
121 }[];
122 footer?: {
123 text: string;
124 iconURL?: string;
125 };
TheCodedProffaae5332023-03-01 18:16:05 -0500126 color?: number;
127 timestamp?: string;
128 author?: {
129 name: string;
130 iconURL?: string;
131 url?: string;
132 };
TheCodedProfcfe8e9a2023-02-26 17:28:09 -0500133}
134
135interface TranscriptComponent {
136 type: number;
137 style?: ButtonStyle;
138 label?: string;
139 description?: string;
140 placeholder?: string;
141 emojiURL?: string;
142}
143
144interface TranscriptAuthor {
145 username: string;
146 discriminator: number;
147 nickname?: string;
148 id: string;
149 iconURL?: string;
150 topRole: {
151 color: number;
152 badgeURL?: string;
TheCodedProf088b1b22023-02-28 17:31:11 -0500153 };
154 bot: boolean;
TheCodedProfcfe8e9a2023-02-26 17:28:09 -0500155}
156
157interface TranscriptAttachment {
158 url: string;
159 filename: string;
160 size: number;
161 log?: string;
162}
163
164interface TranscriptMessage {
165 id: string;
166 author: TranscriptAuthor;
167 content?: string;
168 embeds?: TranscriptEmbed[];
169 components?: TranscriptComponent[][];
170 editedTimestamp?: number;
171 createdTimestamp: number;
172 flags?: string[];
173 attachments?: TranscriptAttachment[];
174 stickerURLs?: string[];
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500175 referencedMessage?: string | [string, string, string]; // the message id, the channel id, the guild id
TheCodedProfcfe8e9a2023-02-26 17:28:09 -0500176}
177
178interface TranscriptSchema {
179 code: string;
180 for: TranscriptAuthor;
181 type: "ticket" | "purge"
182 guild: string;
183 channel: string;
184 messages: TranscriptMessage[];
185 createdTimestamp: number;
186 createdBy: TranscriptAuthor;
187}
188
TheCodedProf003160f2023-03-04 17:09:40 -0500189interface findDocSchema { channelID:string, messageID: string; transcript: string }
190
TheCodedProfcfe8e9a2023-02-26 17:28:09 -0500191export class Transcript {
192 transcripts: Collection<TranscriptSchema>;
TheCodedProf003160f2023-03-04 17:09:40 -0500193 messageToTranscript: Collection<findDocSchema>;
TheCodedProfcfe8e9a2023-02-26 17:28:09 -0500194
195 constructor() {
196 this.transcripts = database.collection<TranscriptSchema>("transcripts");
TheCodedProf003160f2023-03-04 17:09:40 -0500197 this.messageToTranscript = database.collection<findDocSchema>("messageToTranscript");
198 }
199
200 async upload(data: findDocSchema) {
201 // console.log("Transcript upload")
202 await this.messageToTranscript.insertOne(data);
TheCodedProfcfe8e9a2023-02-26 17:28:09 -0500203 }
204
205 async create(transcript: Omit<TranscriptSchema, "code">) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500206 // console.log("Transcript create")
TheCodedProfcfe8e9a2023-02-26 17:28:09 -0500207 let code;
208 do {
TheCodedProf088b1b22023-02-28 17:31:11 -0500209 code = crypto.randomBytes(64).toString("base64").replace(/=/g, "").replace(/\//g, "_").replace(/\+/g, "-");
TheCodedProfcfe8e9a2023-02-26 17:28:09 -0500210 } while (await this.transcripts.findOne({ code: code }));
TheCodedProf75c51be2023-03-03 17:18:18 -0500211 const key = crypto.randomBytes(32**2).toString("base64").replace(/=/g, "").replace(/\//g, "_").replace(/\+/g, "-").substring(0, 32);
212 const iv = getIV().toString("base64").replace(/=/g, "").replace(/\//g, "_").replace(/\+/g, "-");
213 for(const message of transcript.messages) {
214 if(message.content) {
215 const encCipher = crypto.createCipheriv("AES-256-CBC", key, iv);
216 message.content = encCipher.update(message.content, "utf8", "base64") + encCipher.final("base64");
217 }
218 }
TheCodedProfcfe8e9a2023-02-26 17:28:09 -0500219
TheCodedProffaae5332023-03-01 18:16:05 -0500220 const doc = await this.transcripts.insertOne(Object.assign(transcript, { code: code }), collectionOptions);
TheCodedProf003160f2023-03-04 17:09:40 -0500221 if(doc.acknowledged) {
222 client.database.eventScheduler.schedule("deleteTranscript", (Date.now() + 1000 * 60 * 60 * 24 * 7).toString(), { guild: transcript.guild, code: code, iv: iv, key: key });
223 return [code, key, iv];
224 }
TheCodedProf75c51be2023-03-03 17:18:18 -0500225 else return [null, null, null];
TheCodedProfcfe8e9a2023-02-26 17:28:09 -0500226 }
227
TheCodedProf003160f2023-03-04 17:09:40 -0500228 async delete(code: string) {
229 // console.log("Transcript delete")
230 await this.transcripts.deleteOne({ code: code });
TheCodedProf75c51be2023-03-03 17:18:18 -0500231 }
232
233 async deleteAll(guild: string) {
234 // console.log("Transcript delete")
235 const filteredDocs = await this.transcripts.find({ guild: guild }).toArray();
236 for (const doc of filteredDocs) {
237 await this.transcripts.deleteOne({ code: doc.code });
238 }
TheCodedProfcfe8e9a2023-02-26 17:28:09 -0500239 }
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500240
TheCodedProf003160f2023-03-04 17:09:40 -0500241 async readEncrypted(code: string) {
242 // console.log("Transcript read")
243 let doc: TranscriptSchema | null = await this.transcripts.findOne({ code: code });
244 let findDoc: findDocSchema | null = null;
245 if(!doc) findDoc = (await this.messageToTranscript.findOne({ transcript: code }));
246 if(findDoc) {
247 const message = await ((client.channels.cache.get(findDoc.channelID)) as Discord.TextBasedChannel | null)?.messages.fetch(findDoc.messageID);
248 if(!message) return null;
249 const attachment = message.attachments.first();
250 if(!attachment) return null;
251 const transcript = (await fetch(attachment.url)).body;
252 if(!transcript) return null;
253 const reader = transcript.getReader();
254 let data: Uint8Array | null = null;
255 let allPacketsReceived = false;
256 while (!allPacketsReceived) {
257 const { value, done } = await reader.read();
258 if (done) {allPacketsReceived = true; continue;}
259 if(!data) {
260 data = value;
261 } else {
262 data = new Uint8Array(Buffer.concat([data, value]));
263 }
264 }
265 if(!data) return null;
266 doc = JSON.parse(Buffer.from(data).toString());
267 }
268 if(!doc) return null;
269 return doc;
270 }
271
272 async read(code: string, key: string, iv: string) {
273 // console.log("Transcript read")
274 let doc: TranscriptSchema | null = await this.transcripts.findOne({ code: code });
275 let findDoc: findDocSchema | null = null;
276 if(!doc) findDoc = (await this.messageToTranscript.findOne({ transcript: code }));
277 if(findDoc) {
278 const message = await ((client.channels.cache.get(findDoc.channelID)) as Discord.TextBasedChannel | null)?.messages.fetch(findDoc.messageID);
279 if(!message) return null;
280 const attachment = message.attachments.first();
281 if(!attachment) return null;
282 const transcript = (await fetch(attachment.url)).body;
283 if(!transcript) return null;
284 const reader = transcript.getReader();
285 let data: Uint8Array | null = null;
286 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, no-constant-condition
287 while(true) {
288 const { value, done } = await reader.read();
289 if (done) break;
290 if(!data) {
291 data = value;
292 } else {
293 data = new Uint8Array(Buffer.concat([data, value]));
294 }
295 }
296 if(!data) return null;
297 doc = JSON.parse(Buffer.from(data).toString());
298 }
299 if(!doc) return null;
300 for(const message of doc.messages) {
301 if(message.content) {
302 const decCipher = crypto.createDecipheriv("AES-256-CBC", key, iv);
303 message.content = decCipher.update(message.content, "base64", "utf8") + decCipher.final("utf8");
304 }
305 }
306 return doc;
307 }
308
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500309 async createTranscript(messages: Message[], interaction: MessageComponentInteraction | CommandInteraction, member: GuildMember) {
310 const interactionMember = await interaction.guild?.members.fetch(interaction.user.id)
311 const newOut: Omit<TranscriptSchema, "code"> = {
312 type: "ticket",
313 for: {
314 username: member!.user.username,
315 discriminator: parseInt(member!.user.discriminator),
316 id: member!.user.id,
317 topRole: {
318 color: member!.roles.highest.color
TheCodedProf088b1b22023-02-28 17:31:11 -0500319 },
320 iconURL: member!.user.displayAvatarURL({ forceStatic: true}),
321 bot: member!.user.bot
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500322 },
323 guild: interaction.guild!.id,
324 channel: interaction.channel!.id,
325 messages: [],
326 createdTimestamp: Date.now(),
327 createdBy: {
328 username: interaction.user.username,
329 discriminator: parseInt(interaction.user.discriminator),
330 id: interaction.user.id,
331 topRole: {
332 color: interactionMember?.roles.highest.color ?? 0x000000
TheCodedProf088b1b22023-02-28 17:31:11 -0500333 },
334 iconURL: interaction.user.displayAvatarURL({ forceStatic: true}),
335 bot: interaction.user.bot
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500336 }
337 }
TheCodedProf088b1b22023-02-28 17:31:11 -0500338 if(member.nickname) newOut.for.nickname = member.nickname;
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500339 if(interactionMember?.roles.icon) newOut.createdBy.topRole.badgeURL = interactionMember.roles.icon.iconURL()!;
340 messages.reverse().forEach((message) => {
341 const msg: TranscriptMessage = {
342 id: message.id,
343 author: {
344 username: message.author.username,
345 discriminator: parseInt(message.author.discriminator),
346 id: message.author.id,
347 topRole: {
348 color: message.member!.roles.highest.color
TheCodedProf088b1b22023-02-28 17:31:11 -0500349 },
TheCodedProffaae5332023-03-01 18:16:05 -0500350 iconURL: message.member!.user.displayAvatarURL({ forceStatic: true}),
TheCodedProf088b1b22023-02-28 17:31:11 -0500351 bot: message.author.bot
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500352 },
353 createdTimestamp: message.createdTimestamp
354 };
TheCodedProf088b1b22023-02-28 17:31:11 -0500355 if(message.member?.nickname) msg.author.nickname = message.member.nickname;
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500356 if (message.member!.roles.icon) msg.author.topRole.badgeURL = message.member!.roles.icon.iconURL()!;
357 if (message.content) msg.content = message.content;
358 if (message.embeds.length > 0) msg.embeds = message.embeds.map(embed => {
359 const obj: TranscriptEmbed = {};
360 if (embed.title) obj.title = embed.title;
361 if (embed.description) obj.description = embed.description;
362 if (embed.fields.length > 0) obj.fields = embed.fields.map(field => {
363 return {
364 name: field.name,
365 value: field.value,
366 inline: field.inline ?? false
367 }
368 });
TheCodedProffaae5332023-03-01 18:16:05 -0500369 if (embed.color) obj.color = embed.color;
370 if (embed.timestamp) obj.timestamp = embed.timestamp
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500371 if (embed.footer) obj.footer = {
372 text: embed.footer.text,
373 };
374 if (embed.footer?.iconURL) obj.footer!.iconURL = embed.footer.iconURL;
TheCodedProffaae5332023-03-01 18:16:05 -0500375 if (embed.author) obj.author = {
376 name: embed.author.name
377 };
378 if (embed.author?.iconURL) obj.author!.iconURL = embed.author.iconURL;
379 if (embed.author?.url) obj.author!.url = embed.author.url;
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500380 return obj;
381 });
382 if (message.components.length > 0) msg.components = message.components.map(component => component.components.map(child => {
383 const obj: TranscriptComponent = {
384 type: child.type
385 }
386 if (child.type === ComponentType.Button) {
387 obj.style = child.style;
388 obj.label = child.label ?? "";
389 } else if (child.type > 2) {
390 obj.placeholder = child.placeholder ?? "";
391 }
392 return obj
393 }));
394 if (message.editedTimestamp) msg.editedTimestamp = message.editedTimestamp;
395 msg.flags = message.flags.toArray();
396
397 if (message.stickers.size > 0) msg.stickerURLs = message.stickers.map(sticker => sticker.url);
398 if (message.reference) msg.referencedMessage = [message.reference.guildId ?? "", message.reference.channelId, message.reference.messageId ?? ""];
399 newOut.messages.push(msg);
400 });
401 return newOut;
402 }
403
404 toHumanReadable(transcript: Omit<TranscriptSchema, "code">): string {
405 let out = "";
406 for (const message of transcript.messages) {
407 if (message.referencedMessage) {
408 if (Array.isArray(message.referencedMessage)) {
409 out += `> [Crosspost From] ${message.referencedMessage[0]} in ${message.referencedMessage[1]} in ${message.referencedMessage[2]}\n`;
410 }
411 else out += `> [Reply To] ${message.referencedMessage}\n`;
412 }
TheCodedProff8ef7942023-03-03 15:32:32 -0500413 out += `${message.author.nickname ?? message.author.username}#${message.author.discriminator} (${message.author.id}) (${message.id})`;
414 out += ` [${new Date(message.createdTimestamp).toISOString()}]`;
415 if (message.editedTimestamp) out += ` [Edited: ${new Date(message.editedTimestamp).toISOString()}]`;
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500416 out += "\n";
417 if (message.content) out += `[Content]\n${message.content}\n\n`;
418 if (message.embeds) {
419 for (const embed of message.embeds) {
420 out += `[Embed]\n`;
421 if (embed.title) out += `| Title: ${embed.title}\n`;
422 if (embed.description) out += `| Description: ${embed.description}\n`;
423 if (embed.fields) {
424 for (const field of embed.fields) {
425 out += `| Field: ${field.name} - ${field.value}\n`;
426 }
427 }
428 if (embed.footer) {
429 out += `|Footer: ${embed.footer.text}\n`;
430 }
431 out += "\n";
432 }
433 }
434 if (message.components) {
435 for (const component of message.components) {
436 out += `[Component]\n`;
437 for (const button of component) {
438 out += `| Button: ${button.label ?? button.description}\n`;
439 }
440 out += "\n";
441 }
442 }
443 if (message.attachments) {
444 for (const attachment of message.attachments) {
445 out += `[Attachment] ${attachment.filename} (${attachment.size} bytes) ${attachment.url}\n`;
446 }
447 }
TheCodedProf088b1b22023-02-28 17:31:11 -0500448 out += "\n\n"
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500449 }
450 return out
451 }
TheCodedProfcfe8e9a2023-02-26 17:28:09 -0500452}
453
pineafan4edb7762022-06-26 19:21:04 +0100454export class History {
455 histories: Collection<HistorySchema>;
pineafan4edb7762022-06-26 19:21:04 +0100456
pineafan3a02ea32022-08-11 21:35:04 +0100457 constructor() {
pineafan4edb7762022-06-26 19:21:04 +0100458 this.histories = database.collection<HistorySchema>("history");
pineafan4edb7762022-06-26 19:21:04 +0100459 }
460
Skyler Grey75ea9172022-08-06 10:22:23 +0100461 async create(
462 type: string,
463 guild: string,
464 user: Discord.User,
465 moderator: Discord.User | null,
466 reason: string | null,
pineafan3a02ea32022-08-11 21:35:04 +0100467 before?: string | null,
468 after?: string | null,
469 amount?: string | null
Skyler Grey75ea9172022-08-06 10:22:23 +0100470 ) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500471 // console.log("History create");
pineafan4edb7762022-06-26 19:21:04 +0100472 await this.histories.insertOne({
473 type: type,
474 guild: guild,
475 user: user.id,
pineafan3a02ea32022-08-11 21:35:04 +0100476 moderator: moderator ? moderator.id : null,
pineafan4edb7762022-06-26 19:21:04 +0100477 reason: reason,
478 occurredAt: new Date(),
pineafan3a02ea32022-08-11 21:35:04 +0100479 before: before ?? null,
480 after: after ?? null,
481 amount: amount ?? null
TheCodedProffaae5332023-03-01 18:16:05 -0500482 }, collectionOptions);
pineafan4edb7762022-06-26 19:21:04 +0100483 }
484
485 async read(guild: string, user: string, year: number) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500486 // console.log("History read");
Skyler Grey75ea9172022-08-06 10:22:23 +0100487 const entry = (await this.histories
488 .find({
489 guild: guild,
490 user: user,
491 occurredAt: {
492 $gte: new Date(year - 1, 11, 31, 23, 59, 59),
493 $lt: new Date(year + 1, 0, 1, 0, 0, 0)
494 }
495 })
496 .toArray()) as HistorySchema[];
pineafan4edb7762022-06-26 19:21:04 +0100497 return entry;
498 }
pineafane23c4ec2022-07-27 21:56:27 +0100499
500 async delete(guild: string) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500501 // console.log("History delete");
pineafane23c4ec2022-07-27 21:56:27 +0100502 await this.histories.deleteMany({ guild: guild });
503 }
pineafan4edb7762022-06-26 19:21:04 +0100504}
505
TheCodedProfb5e9d552023-01-29 15:43:26 -0500506interface ScanCacheSchema {
507 addedAt: Date;
508 hash: string;
509 data: boolean;
510 tags: string[];
511}
512
513export class ScanCache {
514 scanCache: Collection<ScanCacheSchema>;
515
516 constructor() {
517 this.scanCache = database.collection<ScanCacheSchema>("scanCache");
518 }
519
520 async read(hash: string) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500521 // console.log("ScanCache read");
TheCodedProfb5e9d552023-01-29 15:43:26 -0500522 return await this.scanCache.findOne({ hash: hash });
523 }
524
525 async write(hash: string, data: boolean, tags?: string[]) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500526 // console.log("ScanCache write");
TheCodedProffaae5332023-03-01 18:16:05 -0500527 await this.scanCache.insertOne({ hash: hash, data: data, tags: tags ?? [], addedAt: new Date() }, collectionOptions);
TheCodedProfb5e9d552023-01-29 15:43:26 -0500528 }
529
530 async cleanup() {
TheCodedProff8ef7942023-03-03 15:32:32 -0500531 // console.log("ScanCache cleanup");
TheCodedProfb5e9d552023-01-29 15:43:26 -0500532 await this.scanCache.deleteMany({ addedAt: { $lt: new Date(Date.now() - (1000 * 60 * 60 * 24 * 31)) }, hash: { $not$text: "http"} });
533 }
534}
535
PineaFan538d3752023-01-12 21:48:23 +0000536export class PerformanceTest {
537 performanceData: Collection<PerformanceDataSchema>;
538
539 constructor() {
540 this.performanceData = database.collection<PerformanceDataSchema>("performance");
541 }
542
543 async record(data: PerformanceDataSchema) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500544 // console.log("PerformanceTest record");
PineaFan538d3752023-01-12 21:48:23 +0000545 data.timestamp = new Date();
TheCodedProffaae5332023-03-01 18:16:05 -0500546 await this.performanceData.insertOne(data, collectionOptions);
PineaFan538d3752023-01-12 21:48:23 +0000547 }
548 async read() {
TheCodedProff8ef7942023-03-03 15:32:32 -0500549 // console.log("PerformanceTest read");
PineaFan538d3752023-01-12 21:48:23 +0000550 return await this.performanceData.find({}).toArray();
551 }
552}
553
554export interface PerformanceDataSchema {
555 timestamp?: Date;
556 discord: number;
557 databaseRead: number;
558 resources: {
559 cpu: number;
560 memory: number;
561 temperature: number;
562 }
563}
564
pineafan4edb7762022-06-26 19:21:04 +0100565export class ModNotes {
566 modNotes: Collection<ModNoteSchema>;
pineafan4edb7762022-06-26 19:21:04 +0100567
pineafan3a02ea32022-08-11 21:35:04 +0100568 constructor() {
pineafan4edb7762022-06-26 19:21:04 +0100569 this.modNotes = database.collection<ModNoteSchema>("modNotes");
pineafan4edb7762022-06-26 19:21:04 +0100570 }
571
572 async create(guild: string, user: string, note: string | null) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500573 // console.log("ModNotes create");
Skyler Grey11236ba2022-08-08 21:13:33 +0100574 await this.modNotes.updateOne({ guild: guild, user: user }, { $set: { note: note } }, { upsert: true });
pineafan4edb7762022-06-26 19:21:04 +0100575 }
576
577 async read(guild: string, user: string) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500578 // console.log("ModNotes read");
pineafan63fc5e22022-08-04 22:04:10 +0100579 const entry = await this.modNotes.findOne({ guild: guild, user: user });
pineafan4edb7762022-06-26 19:21:04 +0100580 return entry?.note ?? null;
581 }
TheCodedProf267563a2023-01-21 17:00:57 -0500582
583 async delete(guild: string) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500584 // console.log("ModNotes delete");
TheCodedProf267563a2023-01-21 17:00:57 -0500585 await this.modNotes.deleteMany({ guild: guild });
586 }
pineafan4edb7762022-06-26 19:21:04 +0100587}
588
pineafan73a7c4a2022-07-24 10:38:04 +0100589export class Premium {
590 premium: Collection<PremiumSchema>;
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500591 cache: Map<string, [boolean, string, number, boolean, Date]>; // Date indicates the time one hour after it was created
592 cacheTimeout = 1000 * 60 * 60; // 1 hour
pineafan4edb7762022-06-26 19:21:04 +0100593
pineafan3a02ea32022-08-11 21:35:04 +0100594 constructor() {
pineafan73a7c4a2022-07-24 10:38:04 +0100595 this.premium = database.collection<PremiumSchema>("premium");
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500596 this.cache = new Map<string, [boolean, string, number, boolean, Date]>();
pineafan4edb7762022-06-26 19:21:04 +0100597 }
598
TheCodedProf633866f2023-02-03 17:06:00 -0500599 async updateUser(user: string, level: number) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500600 // console.log("Premium updateUser");
TheCodedProf633866f2023-02-03 17:06:00 -0500601 if(!(await this.userExists(user))) await this.createUser(user, level);
602 await this.premium.updateOne({ user: user }, { $set: { level: level } }, { upsert: true });
603 }
604
605 async userExists(user: string): Promise<boolean> {
TheCodedProff8ef7942023-03-03 15:32:32 -0500606 // console.log("Premium userExists");
TheCodedProf633866f2023-02-03 17:06:00 -0500607 const entry = await this.premium.findOne({ user: user });
608 return entry ? true : false;
609 }
TheCodedProf633866f2023-02-03 17:06:00 -0500610 async createUser(user: string, level: number) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500611 // console.log("Premium createUser");
TheCodedProffaae5332023-03-01 18:16:05 -0500612 await this.premium.insertOne({ user: user, appliesTo: [], level: level }, collectionOptions);
TheCodedProf633866f2023-02-03 17:06:00 -0500613 }
614
TheCodedProfaa3fe992023-02-25 21:53:09 -0500615 async hasPremium(guild: string): Promise<[boolean, string, number, boolean] | null> {
TheCodedProff8ef7942023-03-03 15:32:32 -0500616 // console.log("Premium hasPremium");
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500617 // [Has premium, user giving premium, level, is mod: if given automatically]
618 const cached = this.cache.get(guild);
619 if (cached && cached[4].getTime() < Date.now()) return [cached[0], cached[1], cached[2], cached[3]];
TheCodedProf94ff6de2023-02-22 17:47:26 -0500620 const entries = await this.premium.find({}).toArray();
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500621 const members = (await client.guilds.fetch(guild)).members.cache
TheCodedProf94ff6de2023-02-22 17:47:26 -0500622 for(const {user} of entries) {
623 const member = members.get(user);
TheCodedProfaa3fe992023-02-25 21:53:09 -0500624 if(member) { //TODO: Notify user if they've given premium to a server that has since gotten premium via a mod.
TheCodedProf94ff6de2023-02-22 17:47:26 -0500625 const modPerms = //TODO: Create list in config for perms
626 member.permissions.has("Administrator") ||
627 member.permissions.has("ManageChannels") ||
628 member.permissions.has("ManageRoles") ||
629 member.permissions.has("ManageEmojisAndStickers") ||
630 member.permissions.has("ManageWebhooks") ||
631 member.permissions.has("ManageGuild") ||
632 member.permissions.has("KickMembers") ||
633 member.permissions.has("BanMembers") ||
634 member.permissions.has("ManageEvents") ||
635 member.permissions.has("ManageMessages") ||
636 member.permissions.has("ManageThreads")
637 const entry = entries.find(e => e.user === member.id);
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500638 if(entry && (entry.level === 3) && modPerms) {
639 this.cache.set(guild, [true, member.id, entry.level, true, new Date(Date.now() + this.cacheTimeout)]);
640 return [true, member.id, entry.level, true];
641 }
TheCodedProf94ff6de2023-02-22 17:47:26 -0500642 }
643 }
Skyler Grey75ea9172022-08-06 10:22:23 +0100644 const entry = await this.premium.findOne({
TheCodedProf94ff6de2023-02-22 17:47:26 -0500645 appliesTo: {
646 $elemMatch: {
647 $eq: guild
648 }
649 }
Skyler Grey75ea9172022-08-06 10:22:23 +0100650 });
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500651 this.cache.set(guild, [entry ? true : false, entry?.user ?? "", entry?.level ?? 0, false, new Date(Date.now() + this.cacheTimeout)]);
TheCodedProfaa3fe992023-02-25 21:53:09 -0500652 return entry ? [true, entry.user, entry.level, false] : null;
TheCodedProf267563a2023-01-21 17:00:57 -0500653 }
654
TheCodedProf633866f2023-02-03 17:06:00 -0500655 async fetchUser(user: string): Promise<PremiumSchema | null> {
TheCodedProff8ef7942023-03-03 15:32:32 -0500656 // console.log("Premium fetchUser");
TheCodedProf267563a2023-01-21 17:00:57 -0500657 const entry = await this.premium.findOne({ user: user });
TheCodedProf633866f2023-02-03 17:06:00 -0500658 if (!entry) return null;
659 return entry;
660 }
661
TheCodedProf94ff6de2023-02-22 17:47:26 -0500662 async checkAllPremium(member?: GuildMember) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500663 // console.log("Premium checkAllPremium");
TheCodedProf633866f2023-02-03 17:06:00 -0500664 const entries = await this.premium.find({}).toArray();
TheCodedProf94ff6de2023-02-22 17:47:26 -0500665 if(member) {
666 const entry = entries.find(e => e.user === member.id);
667 if(entry) {
668 const expiresAt = entry.expiresAt;
669 if(expiresAt) expiresAt < Date.now() ? await this.premium.deleteOne({user: member.id}) : null;
670 }
671 const roles = member.roles;
672 let level = 0;
673 if (roles.cache.has("1066468879309750313")) {
TheCodedProf633866f2023-02-03 17:06:00 -0500674 level = 99;
TheCodedProf94ff6de2023-02-22 17:47:26 -0500675 } else if (roles.cache.has("1066465491713003520")) {
TheCodedProf633866f2023-02-03 17:06:00 -0500676 level = 1;
TheCodedProf94ff6de2023-02-22 17:47:26 -0500677 } else if (roles.cache.has("1066439526496604194")) {
TheCodedProf633866f2023-02-03 17:06:00 -0500678 level = 2;
TheCodedProf94ff6de2023-02-22 17:47:26 -0500679 } else if (roles.cache.has("1066464134322978912")) {
TheCodedProf633866f2023-02-03 17:06:00 -0500680 level = 3;
681 }
TheCodedProf94ff6de2023-02-22 17:47:26 -0500682 await this.updateUser(member.id, level);
TheCodedProf633866f2023-02-03 17:06:00 -0500683 if (level > 0) {
TheCodedProf94ff6de2023-02-22 17:47:26 -0500684 await this.premium.updateOne({ user: member.id }, {$unset: { expiresAt: ""}})
TheCodedProf633866f2023-02-03 17:06:00 -0500685 } else {
TheCodedProf94ff6de2023-02-22 17:47:26 -0500686 await this.premium.updateOne({ user: member.id }, {$set: { expiresAt: (Date.now() + (1000*60*60*24*3)) }})
687 }
688 } else {
689 const members = await (await client.guilds.fetch('684492926528651336')).members.fetch();
690 for(const {roles, id} of members.values()) {
691 const entry = entries.find(e => e.user === id);
692 if(entry) {
693 const expiresAt = entry.expiresAt;
694 if(expiresAt) expiresAt < Date.now() ? await this.premium.deleteOne({user: id}) : null;
695 }
696 let level: number = 0;
697 if (roles.cache.has("1066468879309750313")) {
698 level = 99;
699 } else if (roles.cache.has("1066465491713003520")) {
700 level = 1;
701 } else if (roles.cache.has("1066439526496604194")) {
702 level = 2;
703 } else if (roles.cache.has("1066464134322978912")) {
704 level = 3;
705 }
706 await this.updateUser(id, level);
707 if (level > 0) {
708 await this.premium.updateOne({ user: id }, {$unset: { expiresAt: ""}})
709 } else {
710 await this.premium.updateOne({ user: id }, {$set: { expiresAt: (Date.now() + (1000*60*60*24*3)) }})
711 }
TheCodedProf633866f2023-02-03 17:06:00 -0500712 }
713 }
TheCodedProf267563a2023-01-21 17:00:57 -0500714 }
715
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500716 async addPremium(user: string, guild: string) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500717 // console.log("Premium addPremium");
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500718 const { level } = (await this.fetchUser(user))!;
719 this.cache.set(guild, [true, user, level, false, new Date(Date.now() + this.cacheTimeout)]);
TheCodedProf267563a2023-01-21 17:00:57 -0500720 return this.premium.updateOne({ user: user }, { $addToSet: { appliesTo: guild } }, { upsert: true });
pineafan4edb7762022-06-26 19:21:04 +0100721 }
TheCodedProffc420b72023-01-24 17:14:38 -0500722
723 removePremium(user: string, guild: string) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500724 // console.log("Premium removePremium");
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500725 this.cache.set(guild, [false, "", 0, false, new Date(Date.now() + this.cacheTimeout)]);
TheCodedProffc420b72023-01-24 17:14:38 -0500726 return this.premium.updateOne({ user: user }, { $pull: { appliesTo: guild } });
727 }
pineafan4edb7762022-06-26 19:21:04 +0100728}
729
pineafan6fb3e072022-05-20 19:27:23 +0100730export interface GuildConfig {
Skyler Grey75ea9172022-08-06 10:22:23 +0100731 id: string;
732 version: number;
PineaFan100df682023-01-02 13:26:08 +0000733 singleEventNotifications: Record<string, boolean>;
pineafan6fb3e072022-05-20 19:27:23 +0100734 filters: {
735 images: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100736 NSFW: boolean;
737 size: boolean;
738 };
739 malware: boolean;
pineafan6fb3e072022-05-20 19:27:23 +0100740 wordFilter: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100741 enabled: boolean;
pineafan6fb3e072022-05-20 19:27:23 +0100742 words: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100743 strict: string[];
744 loose: string[];
745 };
pineafan6fb3e072022-05-20 19:27:23 +0100746 allowed: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100747 users: string[];
748 roles: string[];
749 channels: string[];
750 };
751 };
pineafan6fb3e072022-05-20 19:27:23 +0100752 invite: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100753 enabled: boolean;
PineaFan538d3752023-01-12 21:48:23 +0000754 allowed: {
755 channels: string[];
756 roles: string[];
757 users: string[];
758 };
Skyler Grey75ea9172022-08-06 10:22:23 +0100759 };
pineafan6fb3e072022-05-20 19:27:23 +0100760 pings: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100761 mass: number;
762 everyone: boolean;
763 roles: boolean;
pineafan6fb3e072022-05-20 19:27:23 +0100764 allowed: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100765 roles: string[];
766 rolesToMention: string[];
767 users: string[];
768 channels: string[];
769 };
770 };
TheCodedProfad0b8202023-02-14 14:27:09 -0500771 clean: {
772 channels: string[];
773 allowed: {
TheCodedProff8ef7942023-03-03 15:32:32 -0500774 users: string[];
TheCodedProfad0b8202023-02-14 14:27:09 -0500775 roles: string[];
776 }
777 }
Skyler Grey75ea9172022-08-06 10:22:23 +0100778 };
TheCodedProfbaee2c12023-02-18 16:11:06 -0500779 autoPublish: {
780 enabled: boolean;
781 channels: string[];
782 }
pineafan6fb3e072022-05-20 19:27:23 +0100783 welcome: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100784 enabled: boolean;
Skyler Grey75ea9172022-08-06 10:22:23 +0100785 role: string | null;
786 ping: string | null;
787 channel: string | null;
788 message: string | null;
789 };
790 stats: Record<string, { name: string; enabled: boolean }>;
pineafan6fb3e072022-05-20 19:27:23 +0100791 logging: {
792 logs: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100793 enabled: boolean;
794 channel: string | null;
Skyler Greyad002172022-08-16 18:48:26 +0100795 toLog: string;
Skyler Grey75ea9172022-08-06 10:22:23 +0100796 };
pineafan6fb3e072022-05-20 19:27:23 +0100797 staff: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100798 channel: string | null;
799 };
pineafan73a7c4a2022-07-24 10:38:04 +0100800 attachments: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100801 channel: string | null;
802 saved: Record<string, string>;
803 };
804 };
pineafan6fb3e072022-05-20 19:27:23 +0100805 verify: {
PineaFandf4996f2023-01-01 14:20:06 +0000806 enabled: boolean;
Skyler Grey75ea9172022-08-06 10:22:23 +0100807 role: string | null;
808 };
pineafan6fb3e072022-05-20 19:27:23 +0100809 tickets: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100810 enabled: boolean;
811 category: string | null;
Skyler Greyad002172022-08-16 18:48:26 +0100812 types: string;
813 customTypes: string[] | null;
Skyler Grey75ea9172022-08-06 10:22:23 +0100814 useCustom: boolean;
815 supportRole: string | null;
816 maxTickets: number;
817 };
pineafan6fb3e072022-05-20 19:27:23 +0100818 moderation: {
819 mute: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100820 timeout: boolean;
821 role: string | null;
822 text: string | null;
823 link: string | null;
824 };
pineafan6fb3e072022-05-20 19:27:23 +0100825 kick: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100826 text: string | null;
827 link: string | null;
828 };
pineafan6fb3e072022-05-20 19:27:23 +0100829 ban: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100830 text: string | null;
831 link: string | null;
832 };
pineafan6fb3e072022-05-20 19:27:23 +0100833 softban: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100834 text: string | null;
835 link: string | null;
836 };
pineafan6fb3e072022-05-20 19:27:23 +0100837 warn: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100838 text: string | null;
839 link: string | null;
840 };
pineafan6fb3e072022-05-20 19:27:23 +0100841 role: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100842 role: string | null;
TheCodedProfd9636e82023-01-17 22:13:06 -0500843 text: null;
844 link: null;
Skyler Grey75ea9172022-08-06 10:22:23 +0100845 };
PineaFane6ba7882023-01-18 20:41:16 +0000846 nick: {
847 text: string | null;
848 link: string | null;
849 }
Skyler Grey75ea9172022-08-06 10:22:23 +0100850 };
pineafan6fb3e072022-05-20 19:27:23 +0100851 tracks: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100852 name: string;
853 retainPrevious: boolean;
854 nullable: boolean;
855 track: string[];
856 manageableBy: string[];
857 }[];
pineafan6fb3e072022-05-20 19:27:23 +0100858 roleMenu: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100859 enabled: boolean;
860 allowWebUI: boolean;
pineafan6fb3e072022-05-20 19:27:23 +0100861 options: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100862 name: string;
863 description: string;
864 min: number;
865 max: number;
pineafan6fb3e072022-05-20 19:27:23 +0100866 options: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100867 name: string;
868 description: string | null;
869 role: string;
870 }[];
871 }[];
872 };
873 tags: Record<string, string>;
pineafan63fc5e22022-08-04 22:04:10 +0100874}
pineafan4edb7762022-06-26 19:21:04 +0100875
876export interface HistorySchema {
Skyler Grey75ea9172022-08-06 10:22:23 +0100877 type: string;
878 guild: string;
879 user: string;
880 moderator: string | null;
pineafan3a02ea32022-08-11 21:35:04 +0100881 reason: string | null;
Skyler Grey75ea9172022-08-06 10:22:23 +0100882 occurredAt: Date;
883 before: string | null;
884 after: string | null;
885 amount: string | null;
pineafan4edb7762022-06-26 19:21:04 +0100886}
887
888export interface ModNoteSchema {
Skyler Grey75ea9172022-08-06 10:22:23 +0100889 guild: string;
890 user: string;
pineafan3a02ea32022-08-11 21:35:04 +0100891 note: string | null;
pineafan4edb7762022-06-26 19:21:04 +0100892}
893
pineafan73a7c4a2022-07-24 10:38:04 +0100894export interface PremiumSchema {
Skyler Grey75ea9172022-08-06 10:22:23 +0100895 user: string;
896 level: number;
Skyler Grey75ea9172022-08-06 10:22:23 +0100897 appliesTo: string[];
TheCodedProf633866f2023-02-03 17:06:00 -0500898 expiresAt?: number;
Skyler Grey75ea9172022-08-06 10:22:23 +0100899}