Settings, encryption, logging

This commit is contained in:
Isaac 2022-07-16 22:18:50 +01:00
parent 97623f3203
commit 79462e83e6
10 changed files with 221 additions and 10 deletions

View File

@ -32,11 +32,14 @@
"dependencies": { "dependencies": {
"@eartharoid/dbf": "^0.0.1", "@eartharoid/dbf": "^0.0.1",
"@eartharoid/dtf": "^2.0.1", "@eartharoid/dtf": "^2.0.1",
"@eartharoid/i18n": "^1.0.4",
"@fastify/cookie": "^6.0.0", "@fastify/cookie": "^6.0.0",
"@fastify/cors": "^8.0.0", "@fastify/cors": "^8.0.0",
"@fastify/jwt": "^5.0.1", "@fastify/jwt": "^5.0.1",
"@fastify/oauth2": "^5.0.0", "@fastify/oauth2": "^5.0.0",
"@prisma/client": "^3.15.2", "@prisma/client": "^4.0.0",
"cryptr": "^6.0.3",
"deep-object-diff": "^1.1.7",
"discord.js": "^13.8.1", "discord.js": "^13.8.1",
"dotenv": "^16.0.1", "dotenv": "^16.0.1",
"fastify": "^4.2.1", "fastify": "^4.2.1",
@ -56,6 +59,6 @@
"eslint": "^8.19.0", "eslint": "^8.19.0",
"eslint-plugin-unused-imports": "^2.0.0", "eslint-plugin-unused-imports": "^2.0.0",
"nodemon": "^2.0.19", "nodemon": "^2.0.19",
"prisma": "^3.15.2" "prisma": "^4.0.0"
} }
} }

View File

@ -109,7 +109,8 @@ model Feedback {
} }
model Guild { model Guild {
autoTag Json? autoClose Int?
autoTag Json @default("[]")
archive Boolean @default(true) archive Boolean @default(true)
blocklist Json @default("[]") blocklist Json @default("[]")
categories Category[] categories Category[]
@ -118,11 +119,14 @@ model Guild {
feedback Feedback[] feedback Feedback[]
footer String? @default("Discord Tickets by eartharoid") footer String? @default("Discord Tickets by eartharoid")
id String @id @db.VarChar(19) id String @id @db.VarChar(19)
locale String @default("en-GB")
logChannel String? @db.VarChar(19) logChannel String? @db.VarChar(19)
primaryColour String @default("#009999") primaryColour String @default("#009999")
staleAfter Int?
successColour String @default("GREEN") successColour String @default("GREEN")
tags Tag[] tags Tag[]
tickets Ticket[] tickets Ticket[]
workingHours Json @default("[null, null, null, null, null, null, null]")
@@map("guilds") @@map("guilds")
} }

View File

@ -2,9 +2,14 @@ const { Client: FrameworkClient }= require('@eartharoid/dbf');
const { Intents } = require('discord.js'); const { Intents } = require('discord.js');
const { PrismaClient } = require('@prisma/client'); const { PrismaClient } = require('@prisma/client');
const Keyv = require('keyv'); const Keyv = require('keyv');
const I18n = require('@eartharoid/i18n');
const fs = require('fs');
const { join } = require('path');
const YAML = require('yaml');
const middleware = require('./lib/prisma');
module.exports = class Client extends FrameworkClient { module.exports = class Client extends FrameworkClient {
constructor() { constructor(config, log) {
super({ super({
intents: [ intents: [
Intents.FLAGS.GUILDS, Intents.FLAGS.GUILDS,
@ -12,11 +17,26 @@ module.exports = class Client extends FrameworkClient {
Intents.FLAGS.GUILD_MESSAGES, Intents.FLAGS.GUILD_MESSAGES,
], ],
}); });
const locales = {};
fs.readdirSync(join(__dirname, 'i18n'))
.filter(file => file.endsWith('.yml'))
.forEach(file => {
const data = fs.readFileSync(join(__dirname, 'i18n/' + file), { encoding: 'utf8' });
const name = file.slice(0, file.length - 4);
locales[name] = YAML.parse(data);
});
/** @type {I18n} */
this.i18n = new I18n('en-GB', locales);
this.config = config;
this.log = log;
} }
async login(token) { async login(token) {
/** @type {PrismaClient} */
this.prisma = new PrismaClient(); this.prisma = new PrismaClient();
// this.prisma.$use((params, next) => {}) this.prisma.$use(middleware);
this.keyv = new Keyv(); this.keyv = new Keyv();
return super.login(token); return super.login(token);
} }

19
src/i18n/en-GB.yml Normal file
View File

@ -0,0 +1,19 @@
command:
log:
admin:
description:
joined: '{user} {verb} {targetType}'
target:
category: 'a category'
settings: 'the settings'
differences: 'Differences'
title:
joined: '{targetType} {verb}'
target:
category: 'Category'
settings: 'Settings'
verb:
create: 'created'
delete: 'deleted'
update: 'updated'
ticket:

View File

@ -72,9 +72,7 @@ process.on('unhandledRejection', error => {
log.error(error); log.error(error);
}); });
const client = new Client(); const client = new Client(config, log);
client.config = config;
client.log = log;
client.login().then(() => { client.login().then(() => {
http(client); http(client);
}); });

97
src/lib/logging.js Normal file
View File

@ -0,0 +1,97 @@
const { MessageEmbed } = require('discord.js');
/**
* @param {import("client")} client
* @param {string} guildId
* @returns {import("discord.js").TextChannel?}
*/
async function getLogChannel(client, guildId) {
const { logChannel: channelId } = await client.prisma.guild.findUnique({
select: { logChannel: true },
where: { id: guildId },
});
return channelId && client.channels.cache.get(channelId);
}
/**
* @param {import("client")} client
* @param {*} target
* @returns {string} target.type
* @returns {string} target.id
*/
async function getTargetName(client, target) {
if (target.type === 'settings') {
return client.guilds.cache.get(target.id).name;
} else {
const row = await client.prisma[target.type].findUnique({ where: { id: target.id } });
return row.name ?? target.id;
}
}
/**
* @param {import("client")} client
* @param {object} details
* @param {string} details.guildId
* @param {string} details.userId
* @param {string} details.action
*/
async function logAdminEvent(client, {
guildId, userId, action, target, diff,
}) {
const user = await client.users.fetch(userId);
client.log.info(`${user.tag} ${action}d ${target.type} ${target.id}`);
const settings = await client.prisma.guild.findUnique({
select: {
footer: true,
locale: true,
logChannel: true,
},
where: { id: guildId },
});
if (!settings.logChannel) return;
const getMessage = client.i18n.getLocale(settings.locale);
const i18nOptions = {
user: `<@${user.id}>`,
verb: getMessage(`log.admin.verb.${action}`),
};
const channel = client.channels.cache.get(settings.logChannel);
if (!channel) return;
const targetName = await getTargetName(client, target);
return await channel.send({
embeds: [
new MessageEmbed()
.setColor('ORANGE')
.setAuthor({
iconURL: user.avatarURL(),
name: user.username,
})
.setTitle(getMessage('log.admin.title.joined', {
...i18nOptions,
targetType: getMessage(`log.admin.title.target.${target.type}`),
verb: getMessage(`log.admin.verb.${action}`),
}))
.setDescription(getMessage('log.admin.description.joined', {
...i18nOptions,
targetType: getMessage(`log.admin.description.target.${target.type}`),
verb: getMessage(`log.admin.verb.${action}`),
}))
.addField(getMessage(`log.admin.title.target.${target.type}`), targetName),
// .setFooter({
// iconURL: client.guilds.cache.get(guildId).iconURL(),
// text: settings.footer,
// }),
...[
action === 'update' && diff &&
new MessageEmbed()
.setColor('ORANGE')
.setTitle(getMessage('log.admin.differences'))
.setDescription(`\`\`\`json\n${JSON.stringify(diff)}\n\`\`\``),
],
],
});
}
module.exports = {
getLogChannel,
logAdminEvent,
};

41
src/lib/prisma.js Normal file
View File

@ -0,0 +1,41 @@
const Cryptr = require('cryptr');
const cryptr = new Cryptr(process.env.ENCRYPTION_KEY);
const fields = [
'name',
'content',
'username',
'displayName',
'channelName',
'openingMessage',
'description',
'value',
'placeholder',
'closedReason',
'topic',
'comment',
'label',
'regex',
];
const shouldEncrypt = ['create', 'createMany', 'update', 'updateMany', 'upsert'];
const shouldDecrypt = ['findUnique', 'findFirst', 'findMany'];
module.exports = async (params, next) => {
if (params.args.data && shouldEncrypt.includes(params.action)) {
for (const field of fields) {
if (field in params.args.data) {
params.args.data[field] = cryptr.encrypt(params.args.data[field]);
}
}
}
const result = await next(params);
if (result && shouldDecrypt.includes(params.action)) {
for (const field of fields) {
if (field in result) {
result[field] = cryptr.decrypt(params.result[field]);
}
}
}
return result;
};

1
src/lib/strings.js Normal file
View File

@ -0,0 +1 @@
module.exports.capitalise = string => string.charAt(0).toUpperCase() + string.slice(1);

View File

@ -1,3 +1,6 @@
const { logAdminEvent } = require('../../../../../lib/logging.js');
const { updatedDiff } = require('deep-object-diff');
module.exports.delete = fastify => ({ module.exports.delete = fastify => ({
handler: async (req, res) => { handler: async (req, res) => {
/** @type {import('client')} */ /** @type {import('client')} */
@ -5,7 +8,15 @@ module.exports.delete = fastify => ({
const id = req.params.guild; const id = req.params.guild;
await client.prisma.guild.delete({ where: { id } }); await client.prisma.guild.delete({ where: { id } });
const settings = await client.prisma.guild.create({ data: { id } }); const settings = await client.prisma.guild.create({ data: { id } });
logAdminEvent(client, {
action: 'delete',
guildId: id,
target: {
id,
type: 'settings',
},
userId: req.user.payload.id,
});
return settings; return settings;
}, },
onRequest: [fastify.authenticate, fastify.isAdmin], onRequest: [fastify.authenticate, fastify.isAdmin],
@ -29,11 +40,21 @@ module.exports.patch = fastify => ({
/** @type {import('client')} */ /** @type {import('client')} */
const client = res.context.config.client; const client = res.context.config.client;
const id = req.params.guild; const id = req.params.guild;
const original = await client.prisma.guild.findUnique({ where: { id } });
const settings = await client.prisma.guild.update({ const settings = await client.prisma.guild.update({
data: req.body, data: req.body,
where: { id }, where: { id },
}); });
logAdminEvent(client, {
action: 'update',
diff: updatedDiff(original, settings),
guildId: id,
target: {
id,
type: 'settings',
},
userId: req.user.payload.id,
});
return settings; return settings;
}, },
onRequest: [fastify.authenticate, fastify.isAdmin], onRequest: [fastify.authenticate, fastify.isAdmin],

View File

@ -0,0 +1,7 @@
module.exports.get = () => ({
handler: async (req, res) => {
/** @type {import("client")} */
const client = res.context.config.client;
return client.i18n.locales;
},
});