blob: 49ed6f06b94b17271a1bd71da8498d32a40342c3 [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 });
TheCodedProff8ef7942023-03-03 15:32:32 -050033 const data = _.clone(this.defaultData!);
34 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
189export class Transcript {
190 transcripts: Collection<TranscriptSchema>;
191
192 constructor() {
193 this.transcripts = database.collection<TranscriptSchema>("transcripts");
194 }
195
196 async create(transcript: Omit<TranscriptSchema, "code">) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500197 // console.log("Transcript create")
TheCodedProfcfe8e9a2023-02-26 17:28:09 -0500198 let code;
199 do {
TheCodedProf088b1b22023-02-28 17:31:11 -0500200 code = crypto.randomBytes(64).toString("base64").replace(/=/g, "").replace(/\//g, "_").replace(/\+/g, "-");
TheCodedProfcfe8e9a2023-02-26 17:28:09 -0500201 } while (await this.transcripts.findOne({ code: code }));
TheCodedProf75c51be2023-03-03 17:18:18 -0500202 const key = crypto.randomBytes(32**2).toString("base64").replace(/=/g, "").replace(/\//g, "_").replace(/\+/g, "-").substring(0, 32);
203 const iv = getIV().toString("base64").replace(/=/g, "").replace(/\//g, "_").replace(/\+/g, "-");
204 for(const message of transcript.messages) {
205 if(message.content) {
206 const encCipher = crypto.createCipheriv("AES-256-CBC", key, iv);
207 message.content = encCipher.update(message.content, "utf8", "base64") + encCipher.final("base64");
208 }
209 }
TheCodedProfcfe8e9a2023-02-26 17:28:09 -0500210
TheCodedProffaae5332023-03-01 18:16:05 -0500211 const doc = await this.transcripts.insertOne(Object.assign(transcript, { code: code }), collectionOptions);
TheCodedProf75c51be2023-03-03 17:18:18 -0500212 if(doc.acknowledged) return [code, key, iv];
213 else return [null, null, null];
TheCodedProfcfe8e9a2023-02-26 17:28:09 -0500214 }
215
TheCodedProf75c51be2023-03-03 17:18:18 -0500216 async read(code: string, key: string, iv: string) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500217 // console.log("Transcript read")
TheCodedProf75c51be2023-03-03 17:18:18 -0500218 const doc = await this.transcripts.findOne({ code: code });
219 if(!doc) return null;
220 for(const message of doc.messages) {
221 if(message.content) {
222 const decCipher = crypto.createDecipheriv("AES-256-CBC", key, iv);
223 message.content = decCipher.update(message.content, "base64", "utf8") + decCipher.final("utf8");
224 }
225 }
226 return doc;
227 }
228
229 async deleteAll(guild: string) {
230 // console.log("Transcript delete")
231 const filteredDocs = await this.transcripts.find({ guild: guild }).toArray();
232 for (const doc of filteredDocs) {
233 await this.transcripts.deleteOne({ code: doc.code });
234 }
TheCodedProfcfe8e9a2023-02-26 17:28:09 -0500235 }
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500236
237 async createTranscript(messages: Message[], interaction: MessageComponentInteraction | CommandInteraction, member: GuildMember) {
238 const interactionMember = await interaction.guild?.members.fetch(interaction.user.id)
239 const newOut: Omit<TranscriptSchema, "code"> = {
240 type: "ticket",
241 for: {
242 username: member!.user.username,
243 discriminator: parseInt(member!.user.discriminator),
244 id: member!.user.id,
245 topRole: {
246 color: member!.roles.highest.color
TheCodedProf088b1b22023-02-28 17:31:11 -0500247 },
248 iconURL: member!.user.displayAvatarURL({ forceStatic: true}),
249 bot: member!.user.bot
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500250 },
251 guild: interaction.guild!.id,
252 channel: interaction.channel!.id,
253 messages: [],
254 createdTimestamp: Date.now(),
255 createdBy: {
256 username: interaction.user.username,
257 discriminator: parseInt(interaction.user.discriminator),
258 id: interaction.user.id,
259 topRole: {
260 color: interactionMember?.roles.highest.color ?? 0x000000
TheCodedProf088b1b22023-02-28 17:31:11 -0500261 },
262 iconURL: interaction.user.displayAvatarURL({ forceStatic: true}),
263 bot: interaction.user.bot
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500264 }
265 }
TheCodedProf088b1b22023-02-28 17:31:11 -0500266 if(member.nickname) newOut.for.nickname = member.nickname;
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500267 if(interactionMember?.roles.icon) newOut.createdBy.topRole.badgeURL = interactionMember.roles.icon.iconURL()!;
268 messages.reverse().forEach((message) => {
269 const msg: TranscriptMessage = {
270 id: message.id,
271 author: {
272 username: message.author.username,
273 discriminator: parseInt(message.author.discriminator),
274 id: message.author.id,
275 topRole: {
276 color: message.member!.roles.highest.color
TheCodedProf088b1b22023-02-28 17:31:11 -0500277 },
TheCodedProffaae5332023-03-01 18:16:05 -0500278 iconURL: message.member!.user.displayAvatarURL({ forceStatic: true}),
TheCodedProf088b1b22023-02-28 17:31:11 -0500279 bot: message.author.bot
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500280 },
281 createdTimestamp: message.createdTimestamp
282 };
TheCodedProf088b1b22023-02-28 17:31:11 -0500283 if(message.member?.nickname) msg.author.nickname = message.member.nickname;
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500284 if (message.member!.roles.icon) msg.author.topRole.badgeURL = message.member!.roles.icon.iconURL()!;
285 if (message.content) msg.content = message.content;
286 if (message.embeds.length > 0) msg.embeds = message.embeds.map(embed => {
287 const obj: TranscriptEmbed = {};
288 if (embed.title) obj.title = embed.title;
289 if (embed.description) obj.description = embed.description;
290 if (embed.fields.length > 0) obj.fields = embed.fields.map(field => {
291 return {
292 name: field.name,
293 value: field.value,
294 inline: field.inline ?? false
295 }
296 });
TheCodedProffaae5332023-03-01 18:16:05 -0500297 if (embed.color) obj.color = embed.color;
298 if (embed.timestamp) obj.timestamp = embed.timestamp
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500299 if (embed.footer) obj.footer = {
300 text: embed.footer.text,
301 };
302 if (embed.footer?.iconURL) obj.footer!.iconURL = embed.footer.iconURL;
TheCodedProffaae5332023-03-01 18:16:05 -0500303 if (embed.author) obj.author = {
304 name: embed.author.name
305 };
306 if (embed.author?.iconURL) obj.author!.iconURL = embed.author.iconURL;
307 if (embed.author?.url) obj.author!.url = embed.author.url;
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500308 return obj;
309 });
310 if (message.components.length > 0) msg.components = message.components.map(component => component.components.map(child => {
311 const obj: TranscriptComponent = {
312 type: child.type
313 }
314 if (child.type === ComponentType.Button) {
315 obj.style = child.style;
316 obj.label = child.label ?? "";
317 } else if (child.type > 2) {
318 obj.placeholder = child.placeholder ?? "";
319 }
320 return obj
321 }));
322 if (message.editedTimestamp) msg.editedTimestamp = message.editedTimestamp;
323 msg.flags = message.flags.toArray();
324
325 if (message.stickers.size > 0) msg.stickerURLs = message.stickers.map(sticker => sticker.url);
326 if (message.reference) msg.referencedMessage = [message.reference.guildId ?? "", message.reference.channelId, message.reference.messageId ?? ""];
327 newOut.messages.push(msg);
328 });
329 return newOut;
330 }
331
332 toHumanReadable(transcript: Omit<TranscriptSchema, "code">): string {
333 let out = "";
334 for (const message of transcript.messages) {
335 if (message.referencedMessage) {
336 if (Array.isArray(message.referencedMessage)) {
337 out += `> [Crosspost From] ${message.referencedMessage[0]} in ${message.referencedMessage[1]} in ${message.referencedMessage[2]}\n`;
338 }
339 else out += `> [Reply To] ${message.referencedMessage}\n`;
340 }
TheCodedProff8ef7942023-03-03 15:32:32 -0500341 out += `${message.author.nickname ?? message.author.username}#${message.author.discriminator} (${message.author.id}) (${message.id})`;
342 out += ` [${new Date(message.createdTimestamp).toISOString()}]`;
343 if (message.editedTimestamp) out += ` [Edited: ${new Date(message.editedTimestamp).toISOString()}]`;
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500344 out += "\n";
345 if (message.content) out += `[Content]\n${message.content}\n\n`;
346 if (message.embeds) {
347 for (const embed of message.embeds) {
348 out += `[Embed]\n`;
349 if (embed.title) out += `| Title: ${embed.title}\n`;
350 if (embed.description) out += `| Description: ${embed.description}\n`;
351 if (embed.fields) {
352 for (const field of embed.fields) {
353 out += `| Field: ${field.name} - ${field.value}\n`;
354 }
355 }
356 if (embed.footer) {
357 out += `|Footer: ${embed.footer.text}\n`;
358 }
359 out += "\n";
360 }
361 }
362 if (message.components) {
363 for (const component of message.components) {
364 out += `[Component]\n`;
365 for (const button of component) {
366 out += `| Button: ${button.label ?? button.description}\n`;
367 }
368 out += "\n";
369 }
370 }
371 if (message.attachments) {
372 for (const attachment of message.attachments) {
373 out += `[Attachment] ${attachment.filename} (${attachment.size} bytes) ${attachment.url}\n`;
374 }
375 }
TheCodedProf088b1b22023-02-28 17:31:11 -0500376 out += "\n\n"
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500377 }
378 return out
379 }
TheCodedProfcfe8e9a2023-02-26 17:28:09 -0500380}
381
pineafan4edb7762022-06-26 19:21:04 +0100382export class History {
383 histories: Collection<HistorySchema>;
pineafan4edb7762022-06-26 19:21:04 +0100384
pineafan3a02ea32022-08-11 21:35:04 +0100385 constructor() {
pineafan4edb7762022-06-26 19:21:04 +0100386 this.histories = database.collection<HistorySchema>("history");
pineafan4edb7762022-06-26 19:21:04 +0100387 }
388
Skyler Grey75ea9172022-08-06 10:22:23 +0100389 async create(
390 type: string,
391 guild: string,
392 user: Discord.User,
393 moderator: Discord.User | null,
394 reason: string | null,
pineafan3a02ea32022-08-11 21:35:04 +0100395 before?: string | null,
396 after?: string | null,
397 amount?: string | null
Skyler Grey75ea9172022-08-06 10:22:23 +0100398 ) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500399 // console.log("History create");
pineafan4edb7762022-06-26 19:21:04 +0100400 await this.histories.insertOne({
401 type: type,
402 guild: guild,
403 user: user.id,
pineafan3a02ea32022-08-11 21:35:04 +0100404 moderator: moderator ? moderator.id : null,
pineafan4edb7762022-06-26 19:21:04 +0100405 reason: reason,
406 occurredAt: new Date(),
pineafan3a02ea32022-08-11 21:35:04 +0100407 before: before ?? null,
408 after: after ?? null,
409 amount: amount ?? null
TheCodedProffaae5332023-03-01 18:16:05 -0500410 }, collectionOptions);
pineafan4edb7762022-06-26 19:21:04 +0100411 }
412
413 async read(guild: string, user: string, year: number) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500414 // console.log("History read");
Skyler Grey75ea9172022-08-06 10:22:23 +0100415 const entry = (await this.histories
416 .find({
417 guild: guild,
418 user: user,
419 occurredAt: {
420 $gte: new Date(year - 1, 11, 31, 23, 59, 59),
421 $lt: new Date(year + 1, 0, 1, 0, 0, 0)
422 }
423 })
424 .toArray()) as HistorySchema[];
pineafan4edb7762022-06-26 19:21:04 +0100425 return entry;
426 }
pineafane23c4ec2022-07-27 21:56:27 +0100427
428 async delete(guild: string) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500429 // console.log("History delete");
pineafane23c4ec2022-07-27 21:56:27 +0100430 await this.histories.deleteMany({ guild: guild });
431 }
pineafan4edb7762022-06-26 19:21:04 +0100432}
433
TheCodedProfb5e9d552023-01-29 15:43:26 -0500434interface ScanCacheSchema {
435 addedAt: Date;
436 hash: string;
437 data: boolean;
438 tags: string[];
439}
440
441export class ScanCache {
442 scanCache: Collection<ScanCacheSchema>;
443
444 constructor() {
445 this.scanCache = database.collection<ScanCacheSchema>("scanCache");
446 }
447
448 async read(hash: string) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500449 // console.log("ScanCache read");
TheCodedProfb5e9d552023-01-29 15:43:26 -0500450 return await this.scanCache.findOne({ hash: hash });
451 }
452
453 async write(hash: string, data: boolean, tags?: string[]) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500454 // console.log("ScanCache write");
TheCodedProffaae5332023-03-01 18:16:05 -0500455 await this.scanCache.insertOne({ hash: hash, data: data, tags: tags ?? [], addedAt: new Date() }, collectionOptions);
TheCodedProfb5e9d552023-01-29 15:43:26 -0500456 }
457
458 async cleanup() {
TheCodedProff8ef7942023-03-03 15:32:32 -0500459 // console.log("ScanCache cleanup");
TheCodedProfb5e9d552023-01-29 15:43:26 -0500460 await this.scanCache.deleteMany({ addedAt: { $lt: new Date(Date.now() - (1000 * 60 * 60 * 24 * 31)) }, hash: { $not$text: "http"} });
461 }
462}
463
PineaFan538d3752023-01-12 21:48:23 +0000464export class PerformanceTest {
465 performanceData: Collection<PerformanceDataSchema>;
466
467 constructor() {
468 this.performanceData = database.collection<PerformanceDataSchema>("performance");
469 }
470
471 async record(data: PerformanceDataSchema) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500472 // console.log("PerformanceTest record");
PineaFan538d3752023-01-12 21:48:23 +0000473 data.timestamp = new Date();
TheCodedProffaae5332023-03-01 18:16:05 -0500474 await this.performanceData.insertOne(data, collectionOptions);
PineaFan538d3752023-01-12 21:48:23 +0000475 }
476 async read() {
TheCodedProff8ef7942023-03-03 15:32:32 -0500477 // console.log("PerformanceTest read");
PineaFan538d3752023-01-12 21:48:23 +0000478 return await this.performanceData.find({}).toArray();
479 }
480}
481
482export interface PerformanceDataSchema {
483 timestamp?: Date;
484 discord: number;
485 databaseRead: number;
486 resources: {
487 cpu: number;
488 memory: number;
489 temperature: number;
490 }
491}
492
pineafan4edb7762022-06-26 19:21:04 +0100493export class ModNotes {
494 modNotes: Collection<ModNoteSchema>;
pineafan4edb7762022-06-26 19:21:04 +0100495
pineafan3a02ea32022-08-11 21:35:04 +0100496 constructor() {
pineafan4edb7762022-06-26 19:21:04 +0100497 this.modNotes = database.collection<ModNoteSchema>("modNotes");
pineafan4edb7762022-06-26 19:21:04 +0100498 }
499
500 async create(guild: string, user: string, note: string | null) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500501 // console.log("ModNotes create");
Skyler Grey11236ba2022-08-08 21:13:33 +0100502 await this.modNotes.updateOne({ guild: guild, user: user }, { $set: { note: note } }, { upsert: true });
pineafan4edb7762022-06-26 19:21:04 +0100503 }
504
505 async read(guild: string, user: string) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500506 // console.log("ModNotes read");
pineafan63fc5e22022-08-04 22:04:10 +0100507 const entry = await this.modNotes.findOne({ guild: guild, user: user });
pineafan4edb7762022-06-26 19:21:04 +0100508 return entry?.note ?? null;
509 }
TheCodedProf267563a2023-01-21 17:00:57 -0500510
511 async delete(guild: string) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500512 // console.log("ModNotes delete");
TheCodedProf267563a2023-01-21 17:00:57 -0500513 await this.modNotes.deleteMany({ guild: guild });
514 }
pineafan4edb7762022-06-26 19:21:04 +0100515}
516
pineafan73a7c4a2022-07-24 10:38:04 +0100517export class Premium {
518 premium: Collection<PremiumSchema>;
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500519 cache: Map<string, [boolean, string, number, boolean, Date]>; // Date indicates the time one hour after it was created
520 cacheTimeout = 1000 * 60 * 60; // 1 hour
pineafan4edb7762022-06-26 19:21:04 +0100521
pineafan3a02ea32022-08-11 21:35:04 +0100522 constructor() {
pineafan73a7c4a2022-07-24 10:38:04 +0100523 this.premium = database.collection<PremiumSchema>("premium");
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500524 this.cache = new Map<string, [boolean, string, number, boolean, Date]>();
pineafan4edb7762022-06-26 19:21:04 +0100525 }
526
TheCodedProf633866f2023-02-03 17:06:00 -0500527 async updateUser(user: string, level: number) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500528 // console.log("Premium updateUser");
TheCodedProf633866f2023-02-03 17:06:00 -0500529 if(!(await this.userExists(user))) await this.createUser(user, level);
530 await this.premium.updateOne({ user: user }, { $set: { level: level } }, { upsert: true });
531 }
532
533 async userExists(user: string): Promise<boolean> {
TheCodedProff8ef7942023-03-03 15:32:32 -0500534 // console.log("Premium userExists");
TheCodedProf633866f2023-02-03 17:06:00 -0500535 const entry = await this.premium.findOne({ user: user });
536 return entry ? true : false;
537 }
TheCodedProf633866f2023-02-03 17:06:00 -0500538 async createUser(user: string, level: number) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500539 // console.log("Premium createUser");
TheCodedProffaae5332023-03-01 18:16:05 -0500540 await this.premium.insertOne({ user: user, appliesTo: [], level: level }, collectionOptions);
TheCodedProf633866f2023-02-03 17:06:00 -0500541 }
542
TheCodedProfaa3fe992023-02-25 21:53:09 -0500543 async hasPremium(guild: string): Promise<[boolean, string, number, boolean] | null> {
TheCodedProff8ef7942023-03-03 15:32:32 -0500544 // console.log("Premium hasPremium");
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500545 // [Has premium, user giving premium, level, is mod: if given automatically]
546 const cached = this.cache.get(guild);
547 if (cached && cached[4].getTime() < Date.now()) return [cached[0], cached[1], cached[2], cached[3]];
TheCodedProf94ff6de2023-02-22 17:47:26 -0500548 const entries = await this.premium.find({}).toArray();
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500549 const members = (await client.guilds.fetch(guild)).members.cache
TheCodedProf94ff6de2023-02-22 17:47:26 -0500550 for(const {user} of entries) {
551 const member = members.get(user);
TheCodedProfaa3fe992023-02-25 21:53:09 -0500552 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 -0500553 const modPerms = //TODO: Create list in config for perms
554 member.permissions.has("Administrator") ||
555 member.permissions.has("ManageChannels") ||
556 member.permissions.has("ManageRoles") ||
557 member.permissions.has("ManageEmojisAndStickers") ||
558 member.permissions.has("ManageWebhooks") ||
559 member.permissions.has("ManageGuild") ||
560 member.permissions.has("KickMembers") ||
561 member.permissions.has("BanMembers") ||
562 member.permissions.has("ManageEvents") ||
563 member.permissions.has("ManageMessages") ||
564 member.permissions.has("ManageThreads")
565 const entry = entries.find(e => e.user === member.id);
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500566 if(entry && (entry.level === 3) && modPerms) {
567 this.cache.set(guild, [true, member.id, entry.level, true, new Date(Date.now() + this.cacheTimeout)]);
568 return [true, member.id, entry.level, true];
569 }
TheCodedProf94ff6de2023-02-22 17:47:26 -0500570 }
571 }
Skyler Grey75ea9172022-08-06 10:22:23 +0100572 const entry = await this.premium.findOne({
TheCodedProf94ff6de2023-02-22 17:47:26 -0500573 appliesTo: {
574 $elemMatch: {
575 $eq: guild
576 }
577 }
Skyler Grey75ea9172022-08-06 10:22:23 +0100578 });
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500579 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 -0500580 return entry ? [true, entry.user, entry.level, false] : null;
TheCodedProf267563a2023-01-21 17:00:57 -0500581 }
582
TheCodedProf633866f2023-02-03 17:06:00 -0500583 async fetchUser(user: string): Promise<PremiumSchema | null> {
TheCodedProff8ef7942023-03-03 15:32:32 -0500584 // console.log("Premium fetchUser");
TheCodedProf267563a2023-01-21 17:00:57 -0500585 const entry = await this.premium.findOne({ user: user });
TheCodedProf633866f2023-02-03 17:06:00 -0500586 if (!entry) return null;
587 return entry;
588 }
589
TheCodedProf94ff6de2023-02-22 17:47:26 -0500590 async checkAllPremium(member?: GuildMember) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500591 // console.log("Premium checkAllPremium");
TheCodedProf633866f2023-02-03 17:06:00 -0500592 const entries = await this.premium.find({}).toArray();
TheCodedProf94ff6de2023-02-22 17:47:26 -0500593 if(member) {
594 const entry = entries.find(e => e.user === member.id);
595 if(entry) {
596 const expiresAt = entry.expiresAt;
597 if(expiresAt) expiresAt < Date.now() ? await this.premium.deleteOne({user: member.id}) : null;
598 }
599 const roles = member.roles;
600 let level = 0;
601 if (roles.cache.has("1066468879309750313")) {
TheCodedProf633866f2023-02-03 17:06:00 -0500602 level = 99;
TheCodedProf94ff6de2023-02-22 17:47:26 -0500603 } else if (roles.cache.has("1066465491713003520")) {
TheCodedProf633866f2023-02-03 17:06:00 -0500604 level = 1;
TheCodedProf94ff6de2023-02-22 17:47:26 -0500605 } else if (roles.cache.has("1066439526496604194")) {
TheCodedProf633866f2023-02-03 17:06:00 -0500606 level = 2;
TheCodedProf94ff6de2023-02-22 17:47:26 -0500607 } else if (roles.cache.has("1066464134322978912")) {
TheCodedProf633866f2023-02-03 17:06:00 -0500608 level = 3;
609 }
TheCodedProf94ff6de2023-02-22 17:47:26 -0500610 await this.updateUser(member.id, level);
TheCodedProf633866f2023-02-03 17:06:00 -0500611 if (level > 0) {
TheCodedProf94ff6de2023-02-22 17:47:26 -0500612 await this.premium.updateOne({ user: member.id }, {$unset: { expiresAt: ""}})
TheCodedProf633866f2023-02-03 17:06:00 -0500613 } else {
TheCodedProf94ff6de2023-02-22 17:47:26 -0500614 await this.premium.updateOne({ user: member.id }, {$set: { expiresAt: (Date.now() + (1000*60*60*24*3)) }})
615 }
616 } else {
617 const members = await (await client.guilds.fetch('684492926528651336')).members.fetch();
618 for(const {roles, id} of members.values()) {
619 const entry = entries.find(e => e.user === id);
620 if(entry) {
621 const expiresAt = entry.expiresAt;
622 if(expiresAt) expiresAt < Date.now() ? await this.premium.deleteOne({user: id}) : null;
623 }
624 let level: number = 0;
625 if (roles.cache.has("1066468879309750313")) {
626 level = 99;
627 } else if (roles.cache.has("1066465491713003520")) {
628 level = 1;
629 } else if (roles.cache.has("1066439526496604194")) {
630 level = 2;
631 } else if (roles.cache.has("1066464134322978912")) {
632 level = 3;
633 }
634 await this.updateUser(id, level);
635 if (level > 0) {
636 await this.premium.updateOne({ user: id }, {$unset: { expiresAt: ""}})
637 } else {
638 await this.premium.updateOne({ user: id }, {$set: { expiresAt: (Date.now() + (1000*60*60*24*3)) }})
639 }
TheCodedProf633866f2023-02-03 17:06:00 -0500640 }
641 }
TheCodedProf267563a2023-01-21 17:00:57 -0500642 }
643
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500644 async addPremium(user: string, guild: string) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500645 // console.log("Premium addPremium");
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500646 const { level } = (await this.fetchUser(user))!;
647 this.cache.set(guild, [true, user, level, false, new Date(Date.now() + this.cacheTimeout)]);
TheCodedProf267563a2023-01-21 17:00:57 -0500648 return this.premium.updateOne({ user: user }, { $addToSet: { appliesTo: guild } }, { upsert: true });
pineafan4edb7762022-06-26 19:21:04 +0100649 }
TheCodedProffc420b72023-01-24 17:14:38 -0500650
651 removePremium(user: string, guild: string) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500652 // console.log("Premium removePremium");
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500653 this.cache.set(guild, [false, "", 0, false, new Date(Date.now() + this.cacheTimeout)]);
TheCodedProffc420b72023-01-24 17:14:38 -0500654 return this.premium.updateOne({ user: user }, { $pull: { appliesTo: guild } });
655 }
pineafan4edb7762022-06-26 19:21:04 +0100656}
657
pineafan6fb3e072022-05-20 19:27:23 +0100658export interface GuildConfig {
Skyler Grey75ea9172022-08-06 10:22:23 +0100659 id: string;
660 version: number;
PineaFan100df682023-01-02 13:26:08 +0000661 singleEventNotifications: Record<string, boolean>;
pineafan6fb3e072022-05-20 19:27:23 +0100662 filters: {
663 images: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100664 NSFW: boolean;
665 size: boolean;
666 };
667 malware: boolean;
pineafan6fb3e072022-05-20 19:27:23 +0100668 wordFilter: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100669 enabled: boolean;
pineafan6fb3e072022-05-20 19:27:23 +0100670 words: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100671 strict: string[];
672 loose: string[];
673 };
pineafan6fb3e072022-05-20 19:27:23 +0100674 allowed: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100675 users: string[];
676 roles: string[];
677 channels: string[];
678 };
679 };
pineafan6fb3e072022-05-20 19:27:23 +0100680 invite: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100681 enabled: boolean;
PineaFan538d3752023-01-12 21:48:23 +0000682 allowed: {
683 channels: string[];
684 roles: string[];
685 users: string[];
686 };
Skyler Grey75ea9172022-08-06 10:22:23 +0100687 };
pineafan6fb3e072022-05-20 19:27:23 +0100688 pings: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100689 mass: number;
690 everyone: boolean;
691 roles: boolean;
pineafan6fb3e072022-05-20 19:27:23 +0100692 allowed: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100693 roles: string[];
694 rolesToMention: string[];
695 users: string[];
696 channels: string[];
697 };
698 };
TheCodedProfad0b8202023-02-14 14:27:09 -0500699 clean: {
700 channels: string[];
701 allowed: {
TheCodedProff8ef7942023-03-03 15:32:32 -0500702 users: string[];
TheCodedProfad0b8202023-02-14 14:27:09 -0500703 roles: string[];
704 }
705 }
Skyler Grey75ea9172022-08-06 10:22:23 +0100706 };
TheCodedProfbaee2c12023-02-18 16:11:06 -0500707 autoPublish: {
708 enabled: boolean;
709 channels: string[];
710 }
pineafan6fb3e072022-05-20 19:27:23 +0100711 welcome: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100712 enabled: boolean;
Skyler Grey75ea9172022-08-06 10:22:23 +0100713 role: string | null;
714 ping: string | null;
715 channel: string | null;
716 message: string | null;
717 };
718 stats: Record<string, { name: string; enabled: boolean }>;
pineafan6fb3e072022-05-20 19:27:23 +0100719 logging: {
720 logs: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100721 enabled: boolean;
722 channel: string | null;
Skyler Greyad002172022-08-16 18:48:26 +0100723 toLog: string;
Skyler Grey75ea9172022-08-06 10:22:23 +0100724 };
pineafan6fb3e072022-05-20 19:27:23 +0100725 staff: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100726 channel: string | null;
727 };
pineafan73a7c4a2022-07-24 10:38:04 +0100728 attachments: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100729 channel: string | null;
730 saved: Record<string, string>;
731 };
732 };
pineafan6fb3e072022-05-20 19:27:23 +0100733 verify: {
PineaFandf4996f2023-01-01 14:20:06 +0000734 enabled: boolean;
Skyler Grey75ea9172022-08-06 10:22:23 +0100735 role: string | null;
736 };
pineafan6fb3e072022-05-20 19:27:23 +0100737 tickets: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100738 enabled: boolean;
739 category: string | null;
Skyler Greyad002172022-08-16 18:48:26 +0100740 types: string;
741 customTypes: string[] | null;
Skyler Grey75ea9172022-08-06 10:22:23 +0100742 useCustom: boolean;
743 supportRole: string | null;
744 maxTickets: number;
745 };
pineafan6fb3e072022-05-20 19:27:23 +0100746 moderation: {
747 mute: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100748 timeout: boolean;
749 role: string | null;
750 text: string | null;
751 link: string | null;
752 };
pineafan6fb3e072022-05-20 19:27:23 +0100753 kick: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100754 text: string | null;
755 link: string | null;
756 };
pineafan6fb3e072022-05-20 19:27:23 +0100757 ban: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100758 text: string | null;
759 link: string | null;
760 };
pineafan6fb3e072022-05-20 19:27:23 +0100761 softban: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100762 text: string | null;
763 link: string | null;
764 };
pineafan6fb3e072022-05-20 19:27:23 +0100765 warn: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100766 text: string | null;
767 link: string | null;
768 };
pineafan6fb3e072022-05-20 19:27:23 +0100769 role: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100770 role: string | null;
TheCodedProfd9636e82023-01-17 22:13:06 -0500771 text: null;
772 link: null;
Skyler Grey75ea9172022-08-06 10:22:23 +0100773 };
PineaFane6ba7882023-01-18 20:41:16 +0000774 nick: {
775 text: string | null;
776 link: string | null;
777 }
Skyler Grey75ea9172022-08-06 10:22:23 +0100778 };
pineafan6fb3e072022-05-20 19:27:23 +0100779 tracks: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100780 name: string;
781 retainPrevious: boolean;
782 nullable: boolean;
783 track: string[];
784 manageableBy: string[];
785 }[];
pineafan6fb3e072022-05-20 19:27:23 +0100786 roleMenu: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100787 enabled: boolean;
788 allowWebUI: boolean;
pineafan6fb3e072022-05-20 19:27:23 +0100789 options: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100790 name: string;
791 description: string;
792 min: number;
793 max: number;
pineafan6fb3e072022-05-20 19:27:23 +0100794 options: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100795 name: string;
796 description: string | null;
797 role: string;
798 }[];
799 }[];
800 };
801 tags: Record<string, string>;
pineafan63fc5e22022-08-04 22:04:10 +0100802}
pineafan4edb7762022-06-26 19:21:04 +0100803
804export interface HistorySchema {
Skyler Grey75ea9172022-08-06 10:22:23 +0100805 type: string;
806 guild: string;
807 user: string;
808 moderator: string | null;
pineafan3a02ea32022-08-11 21:35:04 +0100809 reason: string | null;
Skyler Grey75ea9172022-08-06 10:22:23 +0100810 occurredAt: Date;
811 before: string | null;
812 after: string | null;
813 amount: string | null;
pineafan4edb7762022-06-26 19:21:04 +0100814}
815
816export interface ModNoteSchema {
Skyler Grey75ea9172022-08-06 10:22:23 +0100817 guild: string;
818 user: string;
pineafan3a02ea32022-08-11 21:35:04 +0100819 note: string | null;
pineafan4edb7762022-06-26 19:21:04 +0100820}
821
pineafan73a7c4a2022-07-24 10:38:04 +0100822export interface PremiumSchema {
Skyler Grey75ea9172022-08-06 10:22:23 +0100823 user: string;
824 level: number;
Skyler Grey75ea9172022-08-06 10:22:23 +0100825 appliesTo: string[];
TheCodedProf633866f2023-02-03 17:06:00 -0500826 expiresAt?: number;
Skyler Grey75ea9172022-08-06 10:22:23 +0100827}