blob: 278aa343381773868edae5eb9ecf3d2a4eba294d [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);
TheCodedProf78b90332023-03-04 14:02:21 -050012const 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 +010013await mongoClient.connect();
TheCodedProf8d577fa2023-03-01 13:06:40 -050014const database = mongoClient.db();
pineafan6fb3e072022-05-20 19:27:23 +010015
TheCodedProf78b90332023-03-04 14:02:21 -050016const collectionOptions = { authdb: config.mongoOptions.authSource, w: "majority" };
TheCodedProf75c51be2023-03-03 17:18:18 -050017const getIV = () => crypto.randomBytes(16);
TheCodedProffaae5332023-03-01 18:16:05 -050018
Samuel Shuertcc63dee2023-03-03 18:54:29 -050019
pineafan4edb7762022-06-26 19:21:04 +010020export class Guilds {
pineafan6fb3e072022-05-20 19:27:23 +010021 guilds: Collection<GuildConfig>;
TheCodedProff8ef7942023-03-03 15:32:32 -050022 defaultData: GuildConfig;
pineafan63fc5e22022-08-04 22:04:10 +010023
24 constructor() {
pineafan4edb7762022-06-26 19:21:04 +010025 this.guilds = database.collection<GuildConfig>("guilds");
TheCodedProff8ef7942023-03-03 15:32:32 -050026 this.defaultData = defaultData;
pineafan63fc5e22022-08-04 22:04:10 +010027 }
28
Skyler Greyad002172022-08-16 18:48:26 +010029 async read(guild: string): Promise<GuildConfig> {
TheCodedProff8ef7942023-03-03 15:32:32 -050030 // console.log("Guild read")
pineafan63fc5e22022-08-04 22:04:10 +010031 const entry = await this.guilds.findOne({ id: guild });
TheCodedProff8ef7942023-03-03 15:32:32 -050032 const data = _.clone(this.defaultData!);
33 return _.merge(data, entry ?? {});
pineafan6fb3e072022-05-20 19:27:23 +010034 }
35
Skyler Grey11236ba2022-08-08 21:13:33 +010036 async write(guild: string, set: object | null, unset: string[] | string = []) {
TheCodedProff8ef7942023-03-03 15:32:32 -050037 // console.log("Guild write")
pineafan63fc5e22022-08-04 22:04:10 +010038 // eslint-disable-next-line @typescript-eslint/no-explicit-any
39 const uo: Record<string, any> = {};
40 if (!Array.isArray(unset)) unset = [unset];
41 for (const key of unset) {
pineafan0bc04162022-07-25 17:22:26 +010042 uo[key] = null;
pineafan6702cef2022-06-13 17:52:37 +010043 }
Skyler Grey75ea9172022-08-06 10:22:23 +010044 const out = { $set: {}, $unset: {} };
45 if (set) out.$set = set;
46 if (unset.length) out.$unset = uo;
pineafan0bc04162022-07-25 17:22:26 +010047 await this.guilds.updateOne({ id: guild }, out, { upsert: true });
pineafan6702cef2022-06-13 17:52:37 +010048 }
49
pineafan63fc5e22022-08-04 22:04:10 +010050 // eslint-disable-next-line @typescript-eslint/no-explicit-any
pineafan6702cef2022-06-13 17:52:37 +010051 async append(guild: string, key: string, value: any) {
TheCodedProff8ef7942023-03-03 15:32:32 -050052 // console.log("Guild append")
pineafan6702cef2022-06-13 17:52:37 +010053 if (Array.isArray(value)) {
Skyler Grey75ea9172022-08-06 10:22:23 +010054 await this.guilds.updateOne(
55 { id: guild },
56 {
57 $addToSet: { [key]: { $each: value } }
58 },
59 { upsert: true }
60 );
pineafan6702cef2022-06-13 17:52:37 +010061 } else {
Skyler Grey75ea9172022-08-06 10:22:23 +010062 await this.guilds.updateOne(
63 { id: guild },
64 {
65 $addToSet: { [key]: value }
66 },
67 { upsert: true }
68 );
pineafan6702cef2022-06-13 17:52:37 +010069 }
70 }
71
Skyler Grey75ea9172022-08-06 10:22:23 +010072 async remove(
73 guild: string,
74 key: string,
Skyler Greyc634e2b2022-08-06 17:50:48 +010075 // eslint-disable-next-line @typescript-eslint/no-explicit-any
Skyler Grey75ea9172022-08-06 10:22:23 +010076 value: any,
77 innerKey?: string | null
78 ) {
TheCodedProff8ef7942023-03-03 15:32:32 -050079 // console.log("Guild remove")
pineafan02ba0232022-07-24 22:16:15 +010080 if (innerKey) {
Skyler Grey75ea9172022-08-06 10:22:23 +010081 await this.guilds.updateOne(
82 { id: guild },
83 {
84 $pull: { [key]: { [innerKey]: { $eq: value } } }
85 },
86 { upsert: true }
87 );
pineafan0bc04162022-07-25 17:22:26 +010088 } else if (Array.isArray(value)) {
Skyler Grey75ea9172022-08-06 10:22:23 +010089 await this.guilds.updateOne(
90 { id: guild },
91 {
92 $pullAll: { [key]: value }
93 },
94 { upsert: true }
95 );
pineafan6702cef2022-06-13 17:52:37 +010096 } else {
Skyler Grey75ea9172022-08-06 10:22:23 +010097 await this.guilds.updateOne(
98 { id: guild },
99 {
100 $pullAll: { [key]: [value] }
101 },
102 { upsert: true }
103 );
pineafan6702cef2022-06-13 17:52:37 +0100104 }
pineafan6fb3e072022-05-20 19:27:23 +0100105 }
pineafane23c4ec2022-07-27 21:56:27 +0100106
107 async delete(guild: string) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500108 // console.log("Guild delete")
pineafane23c4ec2022-07-27 21:56:27 +0100109 await this.guilds.deleteOne({ id: guild });
110 }
pineafan6fb3e072022-05-20 19:27:23 +0100111}
112
TheCodedProfcfe8e9a2023-02-26 17:28:09 -0500113interface TranscriptEmbed {
114 title?: string;
115 description?: string;
116 fields?: {
117 name: string;
118 value: string;
119 inline: boolean;
120 }[];
121 footer?: {
122 text: string;
123 iconURL?: string;
124 };
TheCodedProffaae5332023-03-01 18:16:05 -0500125 color?: number;
126 timestamp?: string;
127 author?: {
128 name: string;
129 iconURL?: string;
130 url?: string;
131 };
TheCodedProfcfe8e9a2023-02-26 17:28:09 -0500132}
133
134interface TranscriptComponent {
135 type: number;
136 style?: ButtonStyle;
137 label?: string;
138 description?: string;
139 placeholder?: string;
140 emojiURL?: string;
141}
142
143interface TranscriptAuthor {
144 username: string;
145 discriminator: number;
146 nickname?: string;
147 id: string;
148 iconURL?: string;
149 topRole: {
150 color: number;
151 badgeURL?: string;
TheCodedProf088b1b22023-02-28 17:31:11 -0500152 };
153 bot: boolean;
TheCodedProfcfe8e9a2023-02-26 17:28:09 -0500154}
155
156interface TranscriptAttachment {
157 url: string;
158 filename: string;
159 size: number;
160 log?: string;
161}
162
163interface TranscriptMessage {
164 id: string;
165 author: TranscriptAuthor;
166 content?: string;
167 embeds?: TranscriptEmbed[];
168 components?: TranscriptComponent[][];
169 editedTimestamp?: number;
170 createdTimestamp: number;
171 flags?: string[];
172 attachments?: TranscriptAttachment[];
173 stickerURLs?: string[];
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500174 referencedMessage?: string | [string, string, string]; // the message id, the channel id, the guild id
TheCodedProfcfe8e9a2023-02-26 17:28:09 -0500175}
176
177interface TranscriptSchema {
178 code: string;
179 for: TranscriptAuthor;
180 type: "ticket" | "purge"
181 guild: string;
182 channel: string;
183 messages: TranscriptMessage[];
184 createdTimestamp: number;
185 createdBy: TranscriptAuthor;
186}
187
188export class Transcript {
189 transcripts: Collection<TranscriptSchema>;
190
191 constructor() {
192 this.transcripts = database.collection<TranscriptSchema>("transcripts");
193 }
194
195 async create(transcript: Omit<TranscriptSchema, "code">) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500196 // console.log("Transcript create")
TheCodedProfcfe8e9a2023-02-26 17:28:09 -0500197 let code;
198 do {
TheCodedProf088b1b22023-02-28 17:31:11 -0500199 code = crypto.randomBytes(64).toString("base64").replace(/=/g, "").replace(/\//g, "_").replace(/\+/g, "-");
TheCodedProfcfe8e9a2023-02-26 17:28:09 -0500200 } while (await this.transcripts.findOne({ code: code }));
TheCodedProf75c51be2023-03-03 17:18:18 -0500201 const key = crypto.randomBytes(32**2).toString("base64").replace(/=/g, "").replace(/\//g, "_").replace(/\+/g, "-").substring(0, 32);
202 const iv = getIV().toString("base64").replace(/=/g, "").replace(/\//g, "_").replace(/\+/g, "-");
203 for(const message of transcript.messages) {
204 if(message.content) {
205 const encCipher = crypto.createCipheriv("AES-256-CBC", key, iv);
206 message.content = encCipher.update(message.content, "utf8", "base64") + encCipher.final("base64");
207 }
208 }
TheCodedProfcfe8e9a2023-02-26 17:28:09 -0500209
TheCodedProffaae5332023-03-01 18:16:05 -0500210 const doc = await this.transcripts.insertOne(Object.assign(transcript, { code: code }), collectionOptions);
TheCodedProf75c51be2023-03-03 17:18:18 -0500211 if(doc.acknowledged) return [code, key, iv];
212 else return [null, null, null];
TheCodedProfcfe8e9a2023-02-26 17:28:09 -0500213 }
214
TheCodedProf75c51be2023-03-03 17:18:18 -0500215 async read(code: string, key: string, iv: string) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500216 // console.log("Transcript read")
TheCodedProf75c51be2023-03-03 17:18:18 -0500217 const doc = await this.transcripts.findOne({ code: code });
218 if(!doc) return null;
219 for(const message of doc.messages) {
220 if(message.content) {
221 const decCipher = crypto.createDecipheriv("AES-256-CBC", key, iv);
222 message.content = decCipher.update(message.content, "base64", "utf8") + decCipher.final("utf8");
223 }
224 }
225 return doc;
226 }
227
228 async deleteAll(guild: string) {
229 // console.log("Transcript delete")
230 const filteredDocs = await this.transcripts.find({ guild: guild }).toArray();
231 for (const doc of filteredDocs) {
232 await this.transcripts.deleteOne({ code: doc.code });
233 }
TheCodedProfcfe8e9a2023-02-26 17:28:09 -0500234 }
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500235
236 async createTranscript(messages: Message[], interaction: MessageComponentInteraction | CommandInteraction, member: GuildMember) {
237 const interactionMember = await interaction.guild?.members.fetch(interaction.user.id)
238 const newOut: Omit<TranscriptSchema, "code"> = {
239 type: "ticket",
240 for: {
241 username: member!.user.username,
242 discriminator: parseInt(member!.user.discriminator),
243 id: member!.user.id,
244 topRole: {
245 color: member!.roles.highest.color
TheCodedProf088b1b22023-02-28 17:31:11 -0500246 },
247 iconURL: member!.user.displayAvatarURL({ forceStatic: true}),
248 bot: member!.user.bot
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500249 },
250 guild: interaction.guild!.id,
251 channel: interaction.channel!.id,
252 messages: [],
253 createdTimestamp: Date.now(),
254 createdBy: {
255 username: interaction.user.username,
256 discriminator: parseInt(interaction.user.discriminator),
257 id: interaction.user.id,
258 topRole: {
259 color: interactionMember?.roles.highest.color ?? 0x000000
TheCodedProf088b1b22023-02-28 17:31:11 -0500260 },
261 iconURL: interaction.user.displayAvatarURL({ forceStatic: true}),
262 bot: interaction.user.bot
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500263 }
264 }
TheCodedProf088b1b22023-02-28 17:31:11 -0500265 if(member.nickname) newOut.for.nickname = member.nickname;
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500266 if(interactionMember?.roles.icon) newOut.createdBy.topRole.badgeURL = interactionMember.roles.icon.iconURL()!;
267 messages.reverse().forEach((message) => {
268 const msg: TranscriptMessage = {
269 id: message.id,
270 author: {
271 username: message.author.username,
272 discriminator: parseInt(message.author.discriminator),
273 id: message.author.id,
274 topRole: {
275 color: message.member!.roles.highest.color
TheCodedProf088b1b22023-02-28 17:31:11 -0500276 },
TheCodedProffaae5332023-03-01 18:16:05 -0500277 iconURL: message.member!.user.displayAvatarURL({ forceStatic: true}),
TheCodedProf088b1b22023-02-28 17:31:11 -0500278 bot: message.author.bot
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500279 },
280 createdTimestamp: message.createdTimestamp
281 };
TheCodedProf088b1b22023-02-28 17:31:11 -0500282 if(message.member?.nickname) msg.author.nickname = message.member.nickname;
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500283 if (message.member!.roles.icon) msg.author.topRole.badgeURL = message.member!.roles.icon.iconURL()!;
284 if (message.content) msg.content = message.content;
285 if (message.embeds.length > 0) msg.embeds = message.embeds.map(embed => {
286 const obj: TranscriptEmbed = {};
287 if (embed.title) obj.title = embed.title;
288 if (embed.description) obj.description = embed.description;
289 if (embed.fields.length > 0) obj.fields = embed.fields.map(field => {
290 return {
291 name: field.name,
292 value: field.value,
293 inline: field.inline ?? false
294 }
295 });
TheCodedProffaae5332023-03-01 18:16:05 -0500296 if (embed.color) obj.color = embed.color;
297 if (embed.timestamp) obj.timestamp = embed.timestamp
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500298 if (embed.footer) obj.footer = {
299 text: embed.footer.text,
300 };
301 if (embed.footer?.iconURL) obj.footer!.iconURL = embed.footer.iconURL;
TheCodedProffaae5332023-03-01 18:16:05 -0500302 if (embed.author) obj.author = {
303 name: embed.author.name
304 };
305 if (embed.author?.iconURL) obj.author!.iconURL = embed.author.iconURL;
306 if (embed.author?.url) obj.author!.url = embed.author.url;
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500307 return obj;
308 });
309 if (message.components.length > 0) msg.components = message.components.map(component => component.components.map(child => {
310 const obj: TranscriptComponent = {
311 type: child.type
312 }
313 if (child.type === ComponentType.Button) {
314 obj.style = child.style;
315 obj.label = child.label ?? "";
316 } else if (child.type > 2) {
317 obj.placeholder = child.placeholder ?? "";
318 }
319 return obj
320 }));
321 if (message.editedTimestamp) msg.editedTimestamp = message.editedTimestamp;
322 msg.flags = message.flags.toArray();
323
324 if (message.stickers.size > 0) msg.stickerURLs = message.stickers.map(sticker => sticker.url);
325 if (message.reference) msg.referencedMessage = [message.reference.guildId ?? "", message.reference.channelId, message.reference.messageId ?? ""];
326 newOut.messages.push(msg);
327 });
328 return newOut;
329 }
330
331 toHumanReadable(transcript: Omit<TranscriptSchema, "code">): string {
332 let out = "";
333 for (const message of transcript.messages) {
334 if (message.referencedMessage) {
335 if (Array.isArray(message.referencedMessage)) {
336 out += `> [Crosspost From] ${message.referencedMessage[0]} in ${message.referencedMessage[1]} in ${message.referencedMessage[2]}\n`;
337 }
338 else out += `> [Reply To] ${message.referencedMessage}\n`;
339 }
TheCodedProff8ef7942023-03-03 15:32:32 -0500340 out += `${message.author.nickname ?? message.author.username}#${message.author.discriminator} (${message.author.id}) (${message.id})`;
341 out += ` [${new Date(message.createdTimestamp).toISOString()}]`;
342 if (message.editedTimestamp) out += ` [Edited: ${new Date(message.editedTimestamp).toISOString()}]`;
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500343 out += "\n";
344 if (message.content) out += `[Content]\n${message.content}\n\n`;
345 if (message.embeds) {
346 for (const embed of message.embeds) {
347 out += `[Embed]\n`;
348 if (embed.title) out += `| Title: ${embed.title}\n`;
349 if (embed.description) out += `| Description: ${embed.description}\n`;
350 if (embed.fields) {
351 for (const field of embed.fields) {
352 out += `| Field: ${field.name} - ${field.value}\n`;
353 }
354 }
355 if (embed.footer) {
356 out += `|Footer: ${embed.footer.text}\n`;
357 }
358 out += "\n";
359 }
360 }
361 if (message.components) {
362 for (const component of message.components) {
363 out += `[Component]\n`;
364 for (const button of component) {
365 out += `| Button: ${button.label ?? button.description}\n`;
366 }
367 out += "\n";
368 }
369 }
370 if (message.attachments) {
371 for (const attachment of message.attachments) {
372 out += `[Attachment] ${attachment.filename} (${attachment.size} bytes) ${attachment.url}\n`;
373 }
374 }
TheCodedProf088b1b22023-02-28 17:31:11 -0500375 out += "\n\n"
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500376 }
377 return out
378 }
TheCodedProfcfe8e9a2023-02-26 17:28:09 -0500379}
380
pineafan4edb7762022-06-26 19:21:04 +0100381export class History {
382 histories: Collection<HistorySchema>;
pineafan4edb7762022-06-26 19:21:04 +0100383
pineafan3a02ea32022-08-11 21:35:04 +0100384 constructor() {
pineafan4edb7762022-06-26 19:21:04 +0100385 this.histories = database.collection<HistorySchema>("history");
pineafan4edb7762022-06-26 19:21:04 +0100386 }
387
Skyler Grey75ea9172022-08-06 10:22:23 +0100388 async create(
389 type: string,
390 guild: string,
391 user: Discord.User,
392 moderator: Discord.User | null,
393 reason: string | null,
pineafan3a02ea32022-08-11 21:35:04 +0100394 before?: string | null,
395 after?: string | null,
396 amount?: string | null
Skyler Grey75ea9172022-08-06 10:22:23 +0100397 ) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500398 // console.log("History create");
pineafan4edb7762022-06-26 19:21:04 +0100399 await this.histories.insertOne({
400 type: type,
401 guild: guild,
402 user: user.id,
pineafan3a02ea32022-08-11 21:35:04 +0100403 moderator: moderator ? moderator.id : null,
pineafan4edb7762022-06-26 19:21:04 +0100404 reason: reason,
405 occurredAt: new Date(),
pineafan3a02ea32022-08-11 21:35:04 +0100406 before: before ?? null,
407 after: after ?? null,
408 amount: amount ?? null
TheCodedProffaae5332023-03-01 18:16:05 -0500409 }, collectionOptions);
pineafan4edb7762022-06-26 19:21:04 +0100410 }
411
412 async read(guild: string, user: string, year: number) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500413 // console.log("History read");
Skyler Grey75ea9172022-08-06 10:22:23 +0100414 const entry = (await this.histories
415 .find({
416 guild: guild,
417 user: user,
418 occurredAt: {
419 $gte: new Date(year - 1, 11, 31, 23, 59, 59),
420 $lt: new Date(year + 1, 0, 1, 0, 0, 0)
421 }
422 })
423 .toArray()) as HistorySchema[];
pineafan4edb7762022-06-26 19:21:04 +0100424 return entry;
425 }
pineafane23c4ec2022-07-27 21:56:27 +0100426
427 async delete(guild: string) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500428 // console.log("History delete");
pineafane23c4ec2022-07-27 21:56:27 +0100429 await this.histories.deleteMany({ guild: guild });
430 }
pineafan4edb7762022-06-26 19:21:04 +0100431}
432
TheCodedProfb5e9d552023-01-29 15:43:26 -0500433interface ScanCacheSchema {
434 addedAt: Date;
435 hash: string;
436 data: boolean;
437 tags: string[];
438}
439
440export class ScanCache {
441 scanCache: Collection<ScanCacheSchema>;
442
443 constructor() {
444 this.scanCache = database.collection<ScanCacheSchema>("scanCache");
445 }
446
447 async read(hash: string) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500448 // console.log("ScanCache read");
TheCodedProfb5e9d552023-01-29 15:43:26 -0500449 return await this.scanCache.findOne({ hash: hash });
450 }
451
452 async write(hash: string, data: boolean, tags?: string[]) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500453 // console.log("ScanCache write");
TheCodedProffaae5332023-03-01 18:16:05 -0500454 await this.scanCache.insertOne({ hash: hash, data: data, tags: tags ?? [], addedAt: new Date() }, collectionOptions);
TheCodedProfb5e9d552023-01-29 15:43:26 -0500455 }
456
457 async cleanup() {
TheCodedProff8ef7942023-03-03 15:32:32 -0500458 // console.log("ScanCache cleanup");
TheCodedProfb5e9d552023-01-29 15:43:26 -0500459 await this.scanCache.deleteMany({ addedAt: { $lt: new Date(Date.now() - (1000 * 60 * 60 * 24 * 31)) }, hash: { $not$text: "http"} });
460 }
461}
462
PineaFan538d3752023-01-12 21:48:23 +0000463export class PerformanceTest {
464 performanceData: Collection<PerformanceDataSchema>;
465
466 constructor() {
467 this.performanceData = database.collection<PerformanceDataSchema>("performance");
468 }
469
470 async record(data: PerformanceDataSchema) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500471 // console.log("PerformanceTest record");
PineaFan538d3752023-01-12 21:48:23 +0000472 data.timestamp = new Date();
TheCodedProffaae5332023-03-01 18:16:05 -0500473 await this.performanceData.insertOne(data, collectionOptions);
PineaFan538d3752023-01-12 21:48:23 +0000474 }
475 async read() {
TheCodedProff8ef7942023-03-03 15:32:32 -0500476 // console.log("PerformanceTest read");
PineaFan538d3752023-01-12 21:48:23 +0000477 return await this.performanceData.find({}).toArray();
478 }
479}
480
481export interface PerformanceDataSchema {
482 timestamp?: Date;
483 discord: number;
484 databaseRead: number;
485 resources: {
486 cpu: number;
487 memory: number;
488 temperature: number;
489 }
490}
491
pineafan4edb7762022-06-26 19:21:04 +0100492export class ModNotes {
493 modNotes: Collection<ModNoteSchema>;
pineafan4edb7762022-06-26 19:21:04 +0100494
pineafan3a02ea32022-08-11 21:35:04 +0100495 constructor() {
pineafan4edb7762022-06-26 19:21:04 +0100496 this.modNotes = database.collection<ModNoteSchema>("modNotes");
pineafan4edb7762022-06-26 19:21:04 +0100497 }
498
499 async create(guild: string, user: string, note: string | null) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500500 // console.log("ModNotes create");
Skyler Grey11236ba2022-08-08 21:13:33 +0100501 await this.modNotes.updateOne({ guild: guild, user: user }, { $set: { note: note } }, { upsert: true });
pineafan4edb7762022-06-26 19:21:04 +0100502 }
503
504 async read(guild: string, user: string) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500505 // console.log("ModNotes read");
pineafan63fc5e22022-08-04 22:04:10 +0100506 const entry = await this.modNotes.findOne({ guild: guild, user: user });
pineafan4edb7762022-06-26 19:21:04 +0100507 return entry?.note ?? null;
508 }
TheCodedProf267563a2023-01-21 17:00:57 -0500509
510 async delete(guild: string) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500511 // console.log("ModNotes delete");
TheCodedProf267563a2023-01-21 17:00:57 -0500512 await this.modNotes.deleteMany({ guild: guild });
513 }
pineafan4edb7762022-06-26 19:21:04 +0100514}
515
pineafan73a7c4a2022-07-24 10:38:04 +0100516export class Premium {
517 premium: Collection<PremiumSchema>;
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500518 cache: Map<string, [boolean, string, number, boolean, Date]>; // Date indicates the time one hour after it was created
519 cacheTimeout = 1000 * 60 * 60; // 1 hour
pineafan4edb7762022-06-26 19:21:04 +0100520
pineafan3a02ea32022-08-11 21:35:04 +0100521 constructor() {
pineafan73a7c4a2022-07-24 10:38:04 +0100522 this.premium = database.collection<PremiumSchema>("premium");
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500523 this.cache = new Map<string, [boolean, string, number, boolean, Date]>();
pineafan4edb7762022-06-26 19:21:04 +0100524 }
525
TheCodedProf633866f2023-02-03 17:06:00 -0500526 async updateUser(user: string, level: number) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500527 // console.log("Premium updateUser");
TheCodedProf633866f2023-02-03 17:06:00 -0500528 if(!(await this.userExists(user))) await this.createUser(user, level);
529 await this.premium.updateOne({ user: user }, { $set: { level: level } }, { upsert: true });
530 }
531
532 async userExists(user: string): Promise<boolean> {
TheCodedProff8ef7942023-03-03 15:32:32 -0500533 // console.log("Premium userExists");
TheCodedProf633866f2023-02-03 17:06:00 -0500534 const entry = await this.premium.findOne({ user: user });
535 return entry ? true : false;
536 }
TheCodedProf633866f2023-02-03 17:06:00 -0500537 async createUser(user: string, level: number) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500538 // console.log("Premium createUser");
TheCodedProffaae5332023-03-01 18:16:05 -0500539 await this.premium.insertOne({ user: user, appliesTo: [], level: level }, collectionOptions);
TheCodedProf633866f2023-02-03 17:06:00 -0500540 }
541
TheCodedProfaa3fe992023-02-25 21:53:09 -0500542 async hasPremium(guild: string): Promise<[boolean, string, number, boolean] | null> {
TheCodedProff8ef7942023-03-03 15:32:32 -0500543 // console.log("Premium hasPremium");
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500544 // [Has premium, user giving premium, level, is mod: if given automatically]
545 const cached = this.cache.get(guild);
546 if (cached && cached[4].getTime() < Date.now()) return [cached[0], cached[1], cached[2], cached[3]];
TheCodedProf94ff6de2023-02-22 17:47:26 -0500547 const entries = await this.premium.find({}).toArray();
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500548 const members = (await client.guilds.fetch(guild)).members.cache
TheCodedProf94ff6de2023-02-22 17:47:26 -0500549 for(const {user} of entries) {
550 const member = members.get(user);
TheCodedProfaa3fe992023-02-25 21:53:09 -0500551 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 -0500552 const modPerms = //TODO: Create list in config for perms
553 member.permissions.has("Administrator") ||
554 member.permissions.has("ManageChannels") ||
555 member.permissions.has("ManageRoles") ||
556 member.permissions.has("ManageEmojisAndStickers") ||
557 member.permissions.has("ManageWebhooks") ||
558 member.permissions.has("ManageGuild") ||
559 member.permissions.has("KickMembers") ||
560 member.permissions.has("BanMembers") ||
561 member.permissions.has("ManageEvents") ||
562 member.permissions.has("ManageMessages") ||
563 member.permissions.has("ManageThreads")
564 const entry = entries.find(e => e.user === member.id);
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500565 if(entry && (entry.level === 3) && modPerms) {
566 this.cache.set(guild, [true, member.id, entry.level, true, new Date(Date.now() + this.cacheTimeout)]);
567 return [true, member.id, entry.level, true];
568 }
TheCodedProf94ff6de2023-02-22 17:47:26 -0500569 }
570 }
Skyler Grey75ea9172022-08-06 10:22:23 +0100571 const entry = await this.premium.findOne({
TheCodedProf94ff6de2023-02-22 17:47:26 -0500572 appliesTo: {
573 $elemMatch: {
574 $eq: guild
575 }
576 }
Skyler Grey75ea9172022-08-06 10:22:23 +0100577 });
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500578 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 -0500579 return entry ? [true, entry.user, entry.level, false] : null;
TheCodedProf267563a2023-01-21 17:00:57 -0500580 }
581
TheCodedProf633866f2023-02-03 17:06:00 -0500582 async fetchUser(user: string): Promise<PremiumSchema | null> {
TheCodedProff8ef7942023-03-03 15:32:32 -0500583 // console.log("Premium fetchUser");
TheCodedProf267563a2023-01-21 17:00:57 -0500584 const entry = await this.premium.findOne({ user: user });
TheCodedProf633866f2023-02-03 17:06:00 -0500585 if (!entry) return null;
586 return entry;
587 }
588
TheCodedProf94ff6de2023-02-22 17:47:26 -0500589 async checkAllPremium(member?: GuildMember) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500590 // console.log("Premium checkAllPremium");
TheCodedProf633866f2023-02-03 17:06:00 -0500591 const entries = await this.premium.find({}).toArray();
TheCodedProf94ff6de2023-02-22 17:47:26 -0500592 if(member) {
593 const entry = entries.find(e => e.user === member.id);
594 if(entry) {
595 const expiresAt = entry.expiresAt;
596 if(expiresAt) expiresAt < Date.now() ? await this.premium.deleteOne({user: member.id}) : null;
597 }
598 const roles = member.roles;
599 let level = 0;
600 if (roles.cache.has("1066468879309750313")) {
TheCodedProf633866f2023-02-03 17:06:00 -0500601 level = 99;
TheCodedProf94ff6de2023-02-22 17:47:26 -0500602 } else if (roles.cache.has("1066465491713003520")) {
TheCodedProf633866f2023-02-03 17:06:00 -0500603 level = 1;
TheCodedProf94ff6de2023-02-22 17:47:26 -0500604 } else if (roles.cache.has("1066439526496604194")) {
TheCodedProf633866f2023-02-03 17:06:00 -0500605 level = 2;
TheCodedProf94ff6de2023-02-22 17:47:26 -0500606 } else if (roles.cache.has("1066464134322978912")) {
TheCodedProf633866f2023-02-03 17:06:00 -0500607 level = 3;
608 }
TheCodedProf94ff6de2023-02-22 17:47:26 -0500609 await this.updateUser(member.id, level);
TheCodedProf633866f2023-02-03 17:06:00 -0500610 if (level > 0) {
TheCodedProf94ff6de2023-02-22 17:47:26 -0500611 await this.premium.updateOne({ user: member.id }, {$unset: { expiresAt: ""}})
TheCodedProf633866f2023-02-03 17:06:00 -0500612 } else {
TheCodedProf94ff6de2023-02-22 17:47:26 -0500613 await this.premium.updateOne({ user: member.id }, {$set: { expiresAt: (Date.now() + (1000*60*60*24*3)) }})
614 }
615 } else {
616 const members = await (await client.guilds.fetch('684492926528651336')).members.fetch();
617 for(const {roles, id} of members.values()) {
618 const entry = entries.find(e => e.user === id);
619 if(entry) {
620 const expiresAt = entry.expiresAt;
621 if(expiresAt) expiresAt < Date.now() ? await this.premium.deleteOne({user: id}) : null;
622 }
623 let level: number = 0;
624 if (roles.cache.has("1066468879309750313")) {
625 level = 99;
626 } else if (roles.cache.has("1066465491713003520")) {
627 level = 1;
628 } else if (roles.cache.has("1066439526496604194")) {
629 level = 2;
630 } else if (roles.cache.has("1066464134322978912")) {
631 level = 3;
632 }
633 await this.updateUser(id, level);
634 if (level > 0) {
635 await this.premium.updateOne({ user: id }, {$unset: { expiresAt: ""}})
636 } else {
637 await this.premium.updateOne({ user: id }, {$set: { expiresAt: (Date.now() + (1000*60*60*24*3)) }})
638 }
TheCodedProf633866f2023-02-03 17:06:00 -0500639 }
640 }
TheCodedProf267563a2023-01-21 17:00:57 -0500641 }
642
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500643 async addPremium(user: string, guild: string) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500644 // console.log("Premium addPremium");
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500645 const { level } = (await this.fetchUser(user))!;
646 this.cache.set(guild, [true, user, level, false, new Date(Date.now() + this.cacheTimeout)]);
TheCodedProf267563a2023-01-21 17:00:57 -0500647 return this.premium.updateOne({ user: user }, { $addToSet: { appliesTo: guild } }, { upsert: true });
pineafan4edb7762022-06-26 19:21:04 +0100648 }
TheCodedProffc420b72023-01-24 17:14:38 -0500649
650 removePremium(user: string, guild: string) {
TheCodedProff8ef7942023-03-03 15:32:32 -0500651 // console.log("Premium removePremium");
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500652 this.cache.set(guild, [false, "", 0, false, new Date(Date.now() + this.cacheTimeout)]);
TheCodedProffc420b72023-01-24 17:14:38 -0500653 return this.premium.updateOne({ user: user }, { $pull: { appliesTo: guild } });
654 }
pineafan4edb7762022-06-26 19:21:04 +0100655}
656
pineafan6fb3e072022-05-20 19:27:23 +0100657export interface GuildConfig {
Skyler Grey75ea9172022-08-06 10:22:23 +0100658 id: string;
659 version: number;
PineaFan100df682023-01-02 13:26:08 +0000660 singleEventNotifications: Record<string, boolean>;
pineafan6fb3e072022-05-20 19:27:23 +0100661 filters: {
662 images: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100663 NSFW: boolean;
664 size: boolean;
665 };
666 malware: boolean;
pineafan6fb3e072022-05-20 19:27:23 +0100667 wordFilter: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100668 enabled: boolean;
pineafan6fb3e072022-05-20 19:27:23 +0100669 words: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100670 strict: string[];
671 loose: string[];
672 };
pineafan6fb3e072022-05-20 19:27:23 +0100673 allowed: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100674 users: string[];
675 roles: string[];
676 channels: string[];
677 };
678 };
pineafan6fb3e072022-05-20 19:27:23 +0100679 invite: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100680 enabled: boolean;
PineaFan538d3752023-01-12 21:48:23 +0000681 allowed: {
682 channels: string[];
683 roles: string[];
684 users: string[];
685 };
Skyler Grey75ea9172022-08-06 10:22:23 +0100686 };
pineafan6fb3e072022-05-20 19:27:23 +0100687 pings: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100688 mass: number;
689 everyone: boolean;
690 roles: boolean;
pineafan6fb3e072022-05-20 19:27:23 +0100691 allowed: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100692 roles: string[];
693 rolesToMention: string[];
694 users: string[];
695 channels: string[];
696 };
697 };
TheCodedProfad0b8202023-02-14 14:27:09 -0500698 clean: {
699 channels: string[];
700 allowed: {
TheCodedProff8ef7942023-03-03 15:32:32 -0500701 users: string[];
TheCodedProfad0b8202023-02-14 14:27:09 -0500702 roles: string[];
703 }
704 }
Skyler Grey75ea9172022-08-06 10:22:23 +0100705 };
TheCodedProfbaee2c12023-02-18 16:11:06 -0500706 autoPublish: {
707 enabled: boolean;
708 channels: string[];
709 }
pineafan6fb3e072022-05-20 19:27:23 +0100710 welcome: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100711 enabled: boolean;
Skyler Grey75ea9172022-08-06 10:22:23 +0100712 role: string | null;
713 ping: string | null;
714 channel: string | null;
715 message: string | null;
716 };
717 stats: Record<string, { name: string; enabled: boolean }>;
pineafan6fb3e072022-05-20 19:27:23 +0100718 logging: {
719 logs: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100720 enabled: boolean;
721 channel: string | null;
Skyler Greyad002172022-08-16 18:48:26 +0100722 toLog: string;
Skyler Grey75ea9172022-08-06 10:22:23 +0100723 };
pineafan6fb3e072022-05-20 19:27:23 +0100724 staff: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100725 channel: string | null;
726 };
pineafan73a7c4a2022-07-24 10:38:04 +0100727 attachments: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100728 channel: string | null;
729 saved: Record<string, string>;
730 };
731 };
pineafan6fb3e072022-05-20 19:27:23 +0100732 verify: {
PineaFandf4996f2023-01-01 14:20:06 +0000733 enabled: boolean;
Skyler Grey75ea9172022-08-06 10:22:23 +0100734 role: string | null;
735 };
pineafan6fb3e072022-05-20 19:27:23 +0100736 tickets: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100737 enabled: boolean;
738 category: string | null;
Skyler Greyad002172022-08-16 18:48:26 +0100739 types: string;
740 customTypes: string[] | null;
Skyler Grey75ea9172022-08-06 10:22:23 +0100741 useCustom: boolean;
742 supportRole: string | null;
743 maxTickets: number;
744 };
pineafan6fb3e072022-05-20 19:27:23 +0100745 moderation: {
746 mute: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100747 timeout: boolean;
748 role: string | null;
749 text: string | null;
750 link: string | null;
751 };
pineafan6fb3e072022-05-20 19:27:23 +0100752 kick: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100753 text: string | null;
754 link: string | null;
755 };
pineafan6fb3e072022-05-20 19:27:23 +0100756 ban: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100757 text: string | null;
758 link: string | null;
759 };
pineafan6fb3e072022-05-20 19:27:23 +0100760 softban: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100761 text: string | null;
762 link: string | null;
763 };
pineafan6fb3e072022-05-20 19:27:23 +0100764 warn: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100765 text: string | null;
766 link: string | null;
767 };
pineafan6fb3e072022-05-20 19:27:23 +0100768 role: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100769 role: string | null;
TheCodedProfd9636e82023-01-17 22:13:06 -0500770 text: null;
771 link: null;
Skyler Grey75ea9172022-08-06 10:22:23 +0100772 };
PineaFane6ba7882023-01-18 20:41:16 +0000773 nick: {
774 text: string | null;
775 link: string | null;
776 }
Skyler Grey75ea9172022-08-06 10:22:23 +0100777 };
pineafan6fb3e072022-05-20 19:27:23 +0100778 tracks: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100779 name: string;
780 retainPrevious: boolean;
781 nullable: boolean;
782 track: string[];
783 manageableBy: string[];
784 }[];
pineafan6fb3e072022-05-20 19:27:23 +0100785 roleMenu: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100786 enabled: boolean;
787 allowWebUI: boolean;
pineafan6fb3e072022-05-20 19:27:23 +0100788 options: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100789 name: string;
790 description: string;
791 min: number;
792 max: number;
pineafan6fb3e072022-05-20 19:27:23 +0100793 options: {
Skyler Grey75ea9172022-08-06 10:22:23 +0100794 name: string;
795 description: string | null;
796 role: string;
797 }[];
798 }[];
799 };
800 tags: Record<string, string>;
pineafan63fc5e22022-08-04 22:04:10 +0100801}
pineafan4edb7762022-06-26 19:21:04 +0100802
803export interface HistorySchema {
Skyler Grey75ea9172022-08-06 10:22:23 +0100804 type: string;
805 guild: string;
806 user: string;
807 moderator: string | null;
pineafan3a02ea32022-08-11 21:35:04 +0100808 reason: string | null;
Skyler Grey75ea9172022-08-06 10:22:23 +0100809 occurredAt: Date;
810 before: string | null;
811 after: string | null;
812 amount: string | null;
pineafan4edb7762022-06-26 19:21:04 +0100813}
814
815export interface ModNoteSchema {
Skyler Grey75ea9172022-08-06 10:22:23 +0100816 guild: string;
817 user: string;
pineafan3a02ea32022-08-11 21:35:04 +0100818 note: string | null;
pineafan4edb7762022-06-26 19:21:04 +0100819}
820
pineafan73a7c4a2022-07-24 10:38:04 +0100821export interface PremiumSchema {
Skyler Grey75ea9172022-08-06 10:22:23 +0100822 user: string;
823 level: number;
Skyler Grey75ea9172022-08-06 10:22:23 +0100824 appliesTo: string[];
TheCodedProf633866f2023-02-03 17:06:00 -0500825 expiresAt?: number;
Skyler Grey75ea9172022-08-06 10:22:23 +0100826}