From 6b7dd0e5d94c3043dcea6fab6cf2213dc4a2030e Mon Sep 17 00:00:00 2001 From: Mario Date: Thu, 7 Jan 2021 23:13:22 +0100 Subject: [PATCH 1/3] Better logs --- .gitignore | 1 + src/config.ts => setup/config.ts.example | 0 setup/setup.cmd | 9 +- setup/setup.sh | 23 ++++ src/commands/reply.ts | 3 + src/database/IDatabase.ts | 5 +- src/database/mongo/Schemas.ts | 9 ++ src/database/mongo/mongo.ts | 28 ++++- src/database/sql/sql.ts | 25 +++- src/events/channelDelete.ts | 153 +++++++++++++++++++---- src/events/messageCreate.ts | 24 ++-- src/lib/types/Database.ts | 11 +- 12 files changed, 242 insertions(+), 49 deletions(-) rename src/config.ts => setup/config.ts.example (100%) create mode 100644 setup/setup.sh diff --git a/.gitignore b/.gitignore index 67dfc1e8..85910a1b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ prod/ commandSchema.txt .env *.db +src/config.ts \ No newline at end of file diff --git a/src/config.ts b/setup/config.ts.example similarity index 100% rename from src/config.ts rename to setup/config.ts.example diff --git a/setup/setup.cmd b/setup/setup.cmd index c1192b70..1325ebd3 100644 --- a/setup/setup.cmd +++ b/setup/setup.cmd @@ -4,8 +4,8 @@ title ModMail Setup echo Welcome to ThePhoDit's ModMail setup. You will be asked to introduce several information and then you will be asked to run some commands. echo Please, introduce your Discord Bot Token: -set /p token="" -echo BOT_TOKEN=%token% >> ../.env +set /p bot_token="" +echo BOT_TOKEN=%bot_token% >> ../.env echo Please, indicate if you want to use MongoDB (type: MONGO) or SQLite (type: SQL) as your DB. If you are going to use a site such as Repl.it or Heroku to host your bot, you MUST select MongoDB. set /p db="" @@ -17,4 +17,7 @@ if %db%==MONGO ( echo MONGO_URI=%mongo_uri% >> ../.env ) -echo Done. Installing dependencies... Please, fill in the configuration file as specified in the documentation and run the command "npm run start" once the installation has finished. \ No newline at end of file +copy config.ts.example ../src/config.ts + +echo Done. Installing dependencies... Please, fill in the configuration file as specified in the documentation and run the command "npm run start" once the installation has finished. +npm install \ No newline at end of file diff --git a/setup/setup.sh b/setup/setup.sh new file mode 100644 index 00000000..617eac62 --- /dev/null +++ b/setup/setup.sh @@ -0,0 +1,23 @@ +#!/bin/bash +title "ModMail Setup" +echo "Welcome to ThePhoDit's ModMail setup. You will be asked to introduce several information and then you will be asked to run some commands." + +echo "Please, introduce your Discord Bot Token:" +read BOT_TOKEN + +echo "Please, indicate if you want to use MongoDB (type: MONGO) or SQLite (type: SQL) as your DB. If you are going to use a site such as Repl.it or Heroku to host your bot, you MUST select MongoDB." +read DB + +if [ "$DB" == "MONGO" ]; then + echo "Please, provide your MongoDB connection URI." + read MONGO_URI +fi + +echo "BOT_TOKEN=$BOT_TOKEN +DB=$DB +MONGO_URI=$MONGO_URI" > ../.env + +cp config.ts.example ../src/config.ts + +echo "Done. Installing dependencies... Please, fill in the configuration file as specified in the documentation and run the command \"npm run start\" once the installation has finished." +npm install \ No newline at end of file diff --git a/src/commands/reply.ts b/src/commands/reply.ts index ab257a5e..4bfa2c31 100644 --- a/src/commands/reply.ts +++ b/src/commands/reply.ts @@ -25,6 +25,9 @@ export default new Command('reply', async (caller, cmd, userDB) => { caller.utils.discord.createMessage(cmd.channel.id, { embed: channelEmbed.code }, false, files); caller.utils.discord.createMessage(userDB!.user, { embed: userEmbed.code }, true, files); + + // Add log to the DB. + caller.db.addMessage(cmd.msg.author.id, 'ADMIN', cmd.args.join(' '), userDB!.channel, files.length > 1 ? cmd.msg.attachments.map((a) => a.url) : undefined); }, { level: 'HELPER', diff --git a/src/database/IDatabase.ts b/src/database/IDatabase.ts index 405fb2dc..b0f2c00f 100644 --- a/src/database/IDatabase.ts +++ b/src/database/IDatabase.ts @@ -1,13 +1,14 @@ -import { SnippetDB, UserDB } from '../lib/types/Database'; +import { MessageLog, SnippetDB, UserDB } from '../lib/types/Database'; export interface IDatabase { getUser(id: string, channel: boolean): Promise; addUser(id: string): Promise; boundChannel(userID: string, channelID: string): Promise; getSnippet(name: string): Promise; - closeChannel(id: string): void; + closeChannel(id: string): Promise; updateBlacklist(userID: string, action: 'add' | 'remove'): void; createSnippet(name: string, creatorID: string, content: string): void; deleteSnippet(name: string): void; getSnippets(): Promise; + addMessage(userID: string, location: 'USER' | 'ADMIN' | 'OOT', content: string, channelID: string): Promise; } \ No newline at end of file diff --git a/src/database/mongo/Schemas.ts b/src/database/mongo/Schemas.ts index 78f71468..476ab1b9 100644 --- a/src/database/mongo/Schemas.ts +++ b/src/database/mongo/Schemas.ts @@ -21,6 +21,15 @@ const user = new Schema({ type: Number, required: true, default: 0 + }, + logs: { + type: [{ + userID: String, + location: String, + content: String, + images: [String] + }], + default: [] } }); diff --git a/src/database/mongo/mongo.ts b/src/database/mongo/mongo.ts index 75b865ac..64342bcf 100644 --- a/src/database/mongo/mongo.ts +++ b/src/database/mongo/mongo.ts @@ -1,4 +1,4 @@ -import { UserDB, SnippetDB } from '../../lib/types/Database'; +import { UserDB, SnippetDB, MessageLog } from '../../lib/types/Database'; import { IDatabase } from '../IDatabase'; import { connect, set } from 'mongoose'; import { User, Snippet } from './Schemas'; @@ -50,8 +50,8 @@ export default class Mongo implements IDatabase { return data ? data as SnippetDB : null; } - closeChannel(id: string): void { - User.findOneAndUpdate( + async closeChannel(id: string): Promise { + const user = await User.findOneAndUpdate( { channel: id }, @@ -59,12 +59,15 @@ export default class Mongo implements IDatabase { channel: '0', $inc: { threads: 1 - } - }).exec(); + }, + logs: [] + }).lean(); + + return (user as UserDB).logs; } updateBlacklist(userID: string, action: 'add' | 'remove'): void { - User.findOneAndUpdate( + User.update( { user: userID }, @@ -90,4 +93,17 @@ export default class Mongo implements IDatabase { async getSnippets(): Promise { return await Snippet.find({}).lean() as SnippetDB[]; } + + async addMessage(userID: string, location: 'USER' | 'ADMIN' | 'OOT', content: string, channelID: string, images: string[] | undefined = undefined): Promise { + User.findOneAndUpdate( + { + channel: channelID + }, + { + $push: { + logs: { userID, location, content, images } + } + } + ).exec(); + } } \ No newline at end of file diff --git a/src/database/sql/sql.ts b/src/database/sql/sql.ts index 90943e38..fe984475 100644 --- a/src/database/sql/sql.ts +++ b/src/database/sql/sql.ts @@ -1,4 +1,4 @@ -import { UserDB, SnippetDB } from '../../lib/types/Database'; +import {UserDB, SnippetDB, MessageLog} from '../../lib/types/Database'; import { IDatabase } from '../IDatabase'; export default class SQL implements IDatabase { @@ -9,9 +9,16 @@ export default class SQL implements IDatabase { private constructor() { // eslint-disable-next-line @typescript-eslint/no-var-requires this.DB = require('better-sqlite3')('modmail.db'); - this.DB.prepare('CREATE TABLE IF NOT EXISTS users (user TEXT NOT NULL PRIMARY KEY, channel TEXT NOT NULL DEFAULT \'0\', threads INTEGER NOT NULL DEFAULT 0, blacklisted INTEGER NOT NULL DEFAULT 0)').run(); + this.DB.prepare('CREATE TABLE IF NOT EXISTS users (user TEXT NOT NULL PRIMARY KEY, channel TEXT NOT NULL DEFAULT \'0\', threads INTEGER NOT NULL DEFAULT 0, blacklisted INTEGER NOT NULL DEFAULT 0, logs TEXT NOT NULL DEFAULT \'[]\')').run(); this.DB.prepare('CREATE INDEX IF NOT EXISTS channel_index ON users (channel)').run(); this.DB.prepare('CREATE TABLE IF NOT EXISTS snippets (name TEXT NOT NULL PRIMARY KEY, creator TEXT NOT NULL, content TEXT NOT NULL)').run(); + // Changes that have been applied after bot creation. + try { + this.DB.prepare('ALTER TABLE users ADD COLUMN logs TEXT DEFAULT \'[]\'').run(); + } + catch (e) { + console.log(e); + } } static getDatabase(): SQL { @@ -43,8 +50,10 @@ export default class SQL implements IDatabase { return data ? data : null; } - closeChannel(id: string): void { - this.DB.prepare('UPDATE users SET channel = \'0\', threads = threads + 1 WHERE channel = ?').run(id); + async closeChannel(id: string): Promise { + const data = await this.DB.prepare('SELECT logs FROM users WHERE channel = ?').get(id); + this.DB.prepare('UPDATE users SET channel = \'0\', threads = threads + 1, logs = \'[]\' WHERE channel = ?').run(id); + return JSON.parse(data.logs); } updateBlacklist(userID: string, action: 'add' | 'remove'): void { @@ -62,4 +71,12 @@ export default class SQL implements IDatabase { async getSnippets(): Promise { return await this.DB.prepare('SELECT * FROM snippets').all() as SnippetDB[]; } + + async addMessage(userID: string, location: 'USER' | 'ADMIN' | 'OOT', content: string, channelID: string, images: string[] | undefined = undefined): Promise { + const result = await this.DB.prepare('SELECT logs FROM users WHERE channel = ?').get(channelID); + const array = JSON.parse(result.logs); + array.push({userID, location, content, images }); + this.DB.prepare('UPDATE users SET logs = ? WHERE channel = ?').run(JSON.stringify(array), channelID); + + } } \ No newline at end of file diff --git a/src/events/channelDelete.ts b/src/events/channelDelete.ts index 2b643b6a..f08adc0d 100644 --- a/src/events/channelDelete.ts +++ b/src/events/channelDelete.ts @@ -1,9 +1,7 @@ import Caller from '../lib/structures/Caller'; -import { Channel, TextChannel } from 'eris'; +import { TextChannel } from 'eris'; import { UserDB } from '../lib/types/Database'; -import { COLORS } from '../Constants'; - -export default async (caller: Caller, channel: Channel): Promise => { +export default async (caller: Caller, channel: TextChannel): Promise => { const category = caller.bot.getChannel(caller.category); if (!category || category.type !== 4) return; @@ -15,38 +13,141 @@ export default async (caller: Caller, channel: Channel): Promise => { if (userDB.channel === '0') return; const messages: string[] = []; - for (const msg of (channel as TextChannel).messages.values()) { - if (!msg.content && msg.embeds.length === 0) continue; - let location: string; - // Location of the message. - if (msg.embeds.length > 0 && - (msg.embeds[0].color === parseInt(COLORS.RED.replace('#', ''), 16) || - msg.embeds[0].color === parseInt(COLORS.BLUE.replace('#', ''), 16))) location = 'DM'; - else if (msg.embeds.length > 0) location = 'SERVER'; - else location = 'SERVER - Out Of Thread'; + const messagesArray = await caller.db.closeChannel(channel.id); + for (const msg of messagesArray) { // Message author - const author = msg.embeds.length > 0 ? msg.embeds[0].author?.name || `${msg.author.username}#${msg.author.discriminator}` : `${msg.author.username}#${msg.author.discriminator}`; - const content = msg.embeds.length > 0 && msg.embeds[0].description ? msg.embeds[0].description : msg.content; - const files: string[] = []; - if (msg.attachments.length > 0) for (const file of msg.attachments) files.push(file.url); - messages.push(`${location} | ${author} | ${content} | ${files.join(' ')}`); - } + const author = caller.bot.users.get(msg.userID) || await caller.utils.discord.fetchUser(msg.userID); - caller.db.closeChannel(channel.id); + messages.push(` +
+

${author ? `${author.username}#${author.discriminator}` : msg.userID} - ${msg.location === 'OOT' ? 'SERVER - Out Of Thread' : msg.location}

+

${msg.content}

+ ${msg.images ? ` +
+ ${msg.images.map((f, v) => `Image ${v}`).join(' ')} +
` : ''} +
+
+`); + } if (!caller.logsChannel) return; await caller.utils.discord.createMessage(caller.logsChannel, `A thread from ${(channel as TextChannel).name} has been closed.`, false, { name: `${(channel as TextChannel).name}-${Date.now()}.html`, file: Buffer.from(` - ModMail Logs - - + ModMail Logs + + + + + -

Thread Logs



-

Message Location | Author | Content | Files


-

${messages.join('
')}

+ +
+ +
+ +

Thread Logs

+
+ + + +
+ +
+
+

Log from ${channel.guild.name}

+
+
+ ${messages.join('')} +
`)}); diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index f30e1855..4912b5df 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -40,8 +40,8 @@ export default async (caller: Caller, msg: Message): Promise => { .setTimestamp(); if (files.length > 0) guildEmbed.addField('Files', `This message contains ${files.length} file${files.length > 1 ? 's' : ''}`); - caller.utils.discord.createMessage(channel.id, {embed: guildEmbed.code}, false, files); - msg.addReaction('✅'); + caller.utils.discord.createMessage(channel.id, { embed: guildEmbed.code }, false, files); + msg.addReaction('✅').catch(() => false); } // Not opened else { @@ -51,7 +51,7 @@ export default async (caller: Caller, msg: Message): Promise => { }); if (!channel) return caller.utils.discord.createMessage(msg.author.id, 'An error has occurred - 2.', true); - caller.db.boundChannel(msg.author.id, channel.id); + await caller.db.boundChannel(msg.author.id, channel.id); userDB = await caller.db.getUser(msg.author.id) as UserDB; // Send message to the new channel, then to the user. @@ -75,10 +75,17 @@ export default async (caller: Caller, msg: Message): Promise => { caller.utils.discord.createMessage(msg.author.id, { embed: userOpenEmbed.code }, true); (channel as TextChannel).createMessage({ content: config.role_ping ? `<@&${config.role_ping}>` : '', embed: guildOpenEmbed.code }, files); } + // Add log to the DB. + caller.db.addMessage(msg.author.id, 'USER', msg.content, userDB.channel, files.length > 0 ? msg.attachments.map((a) => a.url) : undefined); } // Out of DMs section. const prefix = config.bot_prefix || '/'; + if (msg.author.bot) return; + + userDB = await caller.db.getUser(msg.channel.id, true); + if (userDB && !(msg.content.startsWith(`${prefix}reply`) || msg.content.startsWith(`${prefix}r`))) + caller.db.addMessage(msg.author.id, 'OOT', msg.content, userDB.channel); if (!msg.content.startsWith(prefix)) return; @@ -90,12 +97,11 @@ export default async (caller: Caller, msg: Message): Promise => { command = command.slice(prefix.length); const cmd = caller.commands.get(command.toLowerCase()) || caller.commands.get(caller.aliases.get(command.toLowerCase()) as string); - userDB = await caller.db.getUser(msg.channel.id, true); - // If no command is found, try to look for a snippet. const snippet = await caller.db.getSnippet(command); if (!cmd && userDB && snippet && category.channels.has(msg.channel.id)) { - if (!((config.bot_helpers as string[]).some(r => msg.member!.roles.includes(r)))) return caller.utils.discord.createMessage(msg.channel.id, 'Invalid permissions.'); + if (!((config.bot_helpers as string[]).some(r => msg.member!.roles.includes(r)))) + return caller.utils.discord.createMessage(msg.channel.id, 'Invalid permissions.'); const userEmbed = new MessageEmbed() .setAuthor(`${msg.author.username}#${msg.author.discriminator}`, msg.author.dynamicAvatarURL()) .setColor(COLORS.RED) @@ -109,11 +115,15 @@ export default async (caller: Caller, msg: Message): Promise => { caller.utils.discord.createMessage(msg.channel.id, { embed: channelEmbed.code }); caller.utils.discord.createMessage(userDB.user, { embed: userEmbed.code }, true); + + // Add log to the DB. + caller.db.addMessage(msg.author.id, 'ADMIN', snippet.content, userDB.channel); return; } else if (!cmd) return; - if (!((config[`bot_${cmd.options.level.toLowerCase()}s` as keyof typeof config] as string[]).some(r => msg.member!.roles.includes(r)))) return caller.utils.discord.createMessage(msg.channel.id, 'Invalid permissions.'); + if (!((config[`bot_${cmd.options.level.toLowerCase()}s` as keyof typeof config] as string[]).some(r => msg.member!.roles.includes(r)))) + return caller.utils.discord.createMessage(msg.channel.id, 'Invalid permissions.'); if (cmd.options.threadOnly && (!userDB || !category.channels.has(msg.channel.id))) return; const channel = msg.channel as TextChannel; diff --git a/src/lib/types/Database.ts b/src/lib/types/Database.ts index 73d1d9bb..3db2623c 100644 --- a/src/lib/types/Database.ts +++ b/src/lib/types/Database.ts @@ -3,6 +3,7 @@ interface UserDB { channel: string; threads: number; blacklisted: 0 | 1; + logs: MessageLog[]; } interface SnippetDB { @@ -11,7 +12,15 @@ interface SnippetDB { content: string; } +interface MessageLog { + userID: string; + location: 'USER' | ' ADMIN' | 'OOT'; + content: string; + images?: string[]; +} + export { UserDB, - SnippetDB + SnippetDB, + MessageLog }; \ No newline at end of file From dcd537706f86d01d1e6b4945e6f5abe29125842b Mon Sep 17 00:00:00 2001 From: Mario Date: Fri, 8 Jan 2021 11:51:13 +0100 Subject: [PATCH 2/3] Mongo change --- src/database/mongo/mongo.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/database/mongo/mongo.ts b/src/database/mongo/mongo.ts index 64342bcf..f457a0cf 100644 --- a/src/database/mongo/mongo.ts +++ b/src/database/mongo/mongo.ts @@ -12,7 +12,9 @@ export default class Mongo implements IDatabase { static db: Mongo; private constructor() { - this.DB = connect(process.env.MONGO_URI!); + this.DB = connect(process.env.MONGO_URI!, { + useUnifiedTopology: true + }); } static getDatabase(): Mongo { From cb19072ff1191b477c18fd157b972fad744fd540 Mon Sep 17 00:00:00 2001 From: Mario Date: Fri, 8 Jan 2021 12:07:05 +0100 Subject: [PATCH 3/3] Change in setup for windows --- setup/setup.cmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/setup.cmd b/setup/setup.cmd index 1325ebd3..e2ee4618 100644 --- a/setup/setup.cmd +++ b/setup/setup.cmd @@ -17,7 +17,7 @@ if %db%==MONGO ( echo MONGO_URI=%mongo_uri% >> ../.env ) -copy config.ts.example ../src/config.ts +copy config.ts.example ..\src\config.ts echo Done. Installing dependencies... Please, fill in the configuration file as specified in the documentation and run the command "npm run start" once the installation has finished. npm install \ No newline at end of file