This commit is contained in:
Isaac 2022-07-15 23:19:42 +01:00
parent 145514a86b
commit 97623f3203
15 changed files with 193 additions and 57 deletions

View File

@ -109,7 +109,7 @@
"max-len": [ "max-len": [
"warn", "warn",
{ {
"code": 150, "code": 180,
"ignoreRegExpLiterals": true, "ignoreRegExpLiterals": true,
"ignoreStrings": true, "ignoreStrings": true,
"ignoreTemplateLiterals": true, "ignoreTemplateLiterals": true,

View File

@ -1,7 +1,7 @@
CONFIG_PATH=./user/config.yml CONFIG_PATH=./user/config.yml
DISCORD_SECRET= DISCORD_SECRET=
DISCORD_TOKEN= DISCORD_TOKEN=
DB_ENCRYPTION_KEY= ENCRYPTION_KEY=
DB_CONNECTION_URL="" DB_CONNECTION_URL=""
HTTP_BIND=3000 HTTP_BIND=3000
HTTP_EXTERNAL= HTTP_EXTERNAL=

View File

@ -33,25 +33,29 @@
"@eartharoid/dbf": "^0.0.1", "@eartharoid/dbf": "^0.0.1",
"@eartharoid/dtf": "^2.0.1", "@eartharoid/dtf": "^2.0.1",
"@fastify/cookie": "^6.0.0", "@fastify/cookie": "^6.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.13.0", "@prisma/client": "^3.15.2",
"discord.js": "^13.6.0", "discord.js": "^13.8.1",
"dotenv": "^16.0.0", "dotenv": "^16.0.1",
"fastify": "^3.29.0", "fastify": "^4.2.1",
"figlet": "^1.5.2", "figlet": "^1.5.2",
"keyv": "^4.3.2",
"leeks.js": "^0.2.4", "leeks.js": "^0.2.4",
"leekslazylogger": "^4.1.7", "leekslazylogger": "^4.1.7",
"ms": "^2.1.3",
"node-dir": "^0.1.17", "node-dir": "^0.1.17",
"node-fetch": "2", "node-fetch": "^2.6.7",
"semver": "^7.3.7", "semver": "^7.3.7",
"terminal-link": "^2.1.1", "terminal-link": "^2.1.1",
"yaml": "^1.10.2" "yaml": "^1.10.2"
}, },
"devDependencies": { "devDependencies": {
"all-contributors-cli": "^6.20.0", "all-contributors-cli": "^6.20.0",
"eslint": "^8.14.0", "eslint": "^8.19.0",
"eslint-plugin-unused-imports": "^2.0.0", "eslint-plugin-unused-imports": "^2.0.0",
"prisma": "^3.13.0" "nodemon": "^2.0.19",
"prisma": "^3.15.2"
} }
} }

View File

@ -116,6 +116,7 @@ model Guild {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
errorColour String @default("RED") errorColour String @default("RED")
feedback Feedback[] feedback Feedback[]
footer String? @default("Discord Tickets by eartharoid")
id String @id @db.VarChar(19) id String @id @db.VarChar(19)
logChannel String? @db.VarChar(19) logChannel String? @db.VarChar(19)
primaryColour String @default("#009999") primaryColour String @default("#009999")

View File

@ -2,7 +2,7 @@ const { randomBytes } = require('crypto');
const { short } = require('leeks.js'); const { short } = require('leeks.js');
console.log(short( console.log(short(
'Set the "DB_ENCRYPTION_KEY" environment variable to: \n&1&!f' + 'Set the "ENCRYPTION_KEY" environment variable to: \n&1&!f' +
randomBytes(24).toString('hex') + randomBytes(24).toString('hex') +
'&r\n\n&0&!e WARNING &r &e&lIf you lose the encryption key, most of the data in the database will become unreadable, requiring a new key and a full reset.', '&r\n\n&0&!e WARNING &r &e&lIf you lose the encryption key, most of the data in the database will become unreadable, requiring a new key and a full reset.',
)); ));

View File

@ -1,6 +1,7 @@
const { Client: FrameworkClient }= require('@eartharoid/dbf'); 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');
module.exports = class Client extends FrameworkClient { module.exports = class Client extends FrameworkClient {
constructor() { constructor() {
@ -16,6 +17,7 @@ module.exports = class Client extends FrameworkClient {
async login(token) { async login(token) {
this.prisma = new PrismaClient(); this.prisma = new PrismaClient();
// this.prisma.$use((params, next) => {}) // this.prisma.$use((params, next) => {})
this.keyv = new Keyv();
return super.login(token); return super.login(token);
} }

View File

@ -3,10 +3,17 @@ const oauth = require('@fastify/oauth2');
// const { randomBytes } = require('crypto'); // const { randomBytes } = require('crypto');
const { short } = require('leeks.js'); const { short } = require('leeks.js');
const { join } = require('path'); const { join } = require('path');
const { readFiles } = require('node-dir'); const { readFiles } = require('node-dir');
module.exports = client => { module.exports = client => {
// cors plugins
fastify.register(require('@fastify/cors'), {
credentials: true,
methods: ['DELETE', 'GET', 'PATCH', 'PUT', 'POST'],
origin: true,
});
// oauth2 plugin // oauth2 plugin
fastify.register(oauth, { fastify.register(oauth, {
callbackUri: `${process.env.HTTP_EXTERNAL}/auth/callback`, callbackUri: `${process.env.HTTP_EXTERNAL}/auth/callback`,
@ -32,14 +39,22 @@ module.exports = client => {
signed: false, signed: false,
}, },
// secret: randomBytes(16).toString('hex'), // secret: randomBytes(16).toString('hex'),
secret: process.env.DB_ENCRYPTION_KEY, secret: process.env.ENCRYPTION_KEY,
}); });
// auth // auth
fastify.decorate('authenticate', async (req, res) => { fastify.decorate('authenticate', async (req, res) => {
try { try {
const data = await req.jwtVerify(); const data = await req.jwtVerify();
if (data.payload.expiresAt < Date.now()) res.redirect('/auth/login'); // if (data.payload.expiresAt < Date.now()) res.redirect('/auth/login');
if (data.payload.expiresAt < Date.now()) {
return res.code(401).send({
error: 'Unauthorised',
message: 'You are not authenticated.',
statusCode: 401,
});
}
} catch (err) { } catch (err) {
res.send(err); res.send(err);
} }
@ -50,14 +65,21 @@ module.exports = client => {
const userId = req.user.payload.id; const userId = req.user.payload.id;
const guildId = req.params.guild; const guildId = req.params.guild;
const guild = client.guilds.cache.get(guildId); const guild = client.guilds.cache.get(guildId);
const guildMember = await guild.members.fetch(userId); if (!guild) {
const isAdmin = guildMember.permissions.has('MANAGE_GUILD'); return res.code(404).send({
error: 'Not Found',
message: 'The requested resource could not be found.',
statusCode: 404,
if (!isAdmin) { });
return res.code(401).send({ }
error: 'Unauthorised', const guildMember = await guild.members.fetch(userId);
message: 'User is not authorised for this action', const isAdmin = guildMember?.permissions.has('MANAGE_GUILD');
statusCode: 401, if (!guildMember || !isAdmin) {
return res.code(403).send({
error: 'Forbidden',
message: 'You are not permitted for this action.',
statusCode: 403,
}); });
} }
@ -119,7 +141,7 @@ module.exports = client => {
} }
// start server // start server
fastify.listen(process.env.HTTP_BIND, (err, addr) => { fastify.listen({ port: process.env.HTTP_BIND }, (err, addr) => {
if (err) client.log.error.http(err); if (err) client.log.error.http(err);
else client.log.success.http(`Listening at ${addr}`); else client.log.success.http(`Listening at ${addr}`);
}); });

View File

@ -41,8 +41,8 @@ if (!semver.satisfies(process.versions.node, pkg.engines.node)) {
process.exit(1); process.exit(1);
} }
if (process.env.DB_ENCRYPTION_KEY === undefined) { if (process.env.ENCRYPTION_KEY === undefined) {
console.log('\x07' + colours.redBright('Error: The "DB_ENCRYPTION_KEY" environment variable is not set.\nRun "npm run keygen" to generate a key, or set it to "false" to disable encryption (not recommended).')); console.log('\x07' + colours.redBright('Error: The "ENCRYPTION_KEY" environment variable is not set.\nRun "npm run keygen" to generate a key, or set it to "false" to disable encryption (not recommended).'));
process.exit(1); process.exit(1);
} }

View File

@ -11,6 +11,7 @@ module.exports = class extends Listener {
} }
run() { run() {
process.title = this.client.user.tag + ' [Discord Tickets]';
this.client.log.success('Connected to Discord as "%s"', this.client.user.tag); this.client.log.success('Connected to Discord as "%s"', this.client.user.tag);
} }
}; };

View File

@ -1,6 +1,6 @@
module.exports.get = fastify => ({ module.exports.get = fastify => ({
handler: async (req, res) => { handler: async (req, res) => {
/** @type {import('../../../../../../client')} */ /** @type {import('client')} */
const client = res.context.config.client; const client = res.context.config.client;
const categories = await client.prisma.guild.findUnique({ where: { id: req.params.guild } }).categories(); const categories = await client.prisma.guild.findUnique({ where: { id: req.params.guild } }).categories();
@ -12,7 +12,7 @@ module.exports.get = fastify => ({
module.exports.post = fastify => ({ module.exports.post = fastify => ({
handler: async (req, res) => { handler: async (req, res) => {
/** @type {import('../../../../../../client')} */ /** @type {import('client')} */
const client = res.context.config.client; const client = res.context.config.client;
const user = await client.users.fetch(req.user.payload.id); const user = await client.users.fetch(req.user.payload.id);

View File

@ -0,0 +1,13 @@
module.exports.get = fastify => ({
handler: async (req, res) => {
/** @type {import('client')} */
const client = res.context.config.client;
const id = req.params.guild;
const guild = client.guilds.cache.get(id) ?? {};
const { query } = req.query;
if (!query) return {};
const data = query.split(/\./g).reduce((acc, part) => acc && acc[part], guild);
return data;
},
onRequest: [fastify.authenticate, fastify.isAdmin],
});

View File

@ -1,40 +1,53 @@
module.exports.delete = fastify => ({ /* eslint-disable no-underscore-dangle */
handler: async (req, res) => { const ms = require('ms');
/** @type {import('../../../../../client')} */
const client = res.context.config.client;
await client.prisma.guild.delete({ where: { id: req.params.guild } });
const settings = await client.prisma.guild.create({ data: { id: req.params.guild } });
res.send(settings);
},
onRequest: [fastify.authenticate, fastify.isAdmin],
});
module.exports.get = fastify => ({ module.exports.get = fastify => ({
handler: async (req, res) => { handler: async (req, res) => {
/** @type {import('../../../../../client')} */ /** @type {import("client")} */
const client = res.context.config.client; const client = res.context.config.client;
const id = req.params.guild;
const cacheKey = `cache/stats/guild:${id}`;
let cached = await client.keyv.get(cacheKey);
const settings = await client.prisma.guild.findUnique({ where: { id: req.params.guild } }) ?? if (!cached) {
await client.prisma.guild.create({ data: { id: req.params.guild } }); const guild = client.guilds.cache.get(id);
const settings = await client.prisma.guild.findUnique({ where: { id } }) ??
await client.prisma.guild.create({ data: { id } });
const categories = await client.prisma.category.findMany({
select: {
_count: { select: { tickets: true } },
id: true,
name: true,
},
where: { guildId: id },
});
const tickets = await client.prisma.ticket.findMany({
select: {
createdAt: true,
firstResponseAt: true,
},
where: { guildId: id },
});
cached = {
createdAt: settings.createdAt,
id: guild.id,
logo: guild.iconURL(),
name: guild.name,
stats: {
avgResponseTime: ms(tickets.reduce((total, ticket) => total + (ticket.firstResponseAt - ticket.createdAt), 0) ?? 1 / tickets.length),
categories: categories.map(c => ({
id: c.id,
name: c.name,
tickets: c._count.tickets,
})),
tags: await client.prisma.tag.count({ where: { guildId: id } }),
tickets: tickets.length,
},
};
await client.keyv.set(cacheKey, cached, ms('5m'));
}
res.send(settings); return cached;
},
onRequest: [fastify.authenticate, fastify.isAdmin],
});
module.exports.patch = fastify => ({
handler: async (req, res) => {
/** @type {import('../../../../../client')} */
const client = res.context.config.client;
const settings = await client.prisma.guild.update({
data: req.body,
where: { id: req.params.guild },
});
res.send(settings);
}, },
onRequest: [fastify.authenticate, fastify.isAdmin], onRequest: [fastify.authenticate, fastify.isAdmin],
}); });

View File

@ -0,0 +1,40 @@
module.exports.delete = fastify => ({
handler: async (req, res) => {
/** @type {import('client')} */
const client = res.context.config.client;
const id = req.params.guild;
await client.prisma.guild.delete({ where: { id } });
const settings = await client.prisma.guild.create({ data: { id } });
return settings;
},
onRequest: [fastify.authenticate, fastify.isAdmin],
});
module.exports.get = fastify => ({
handler: async (req, res) => {
/** @type {import('client')} */
const client = res.context.config.client;
const id = req.params.guild;
const settings = await client.prisma.guild.findUnique({ where: { id } }) ??
await client.prisma.guild.create({ data: { id } });
return settings;
},
onRequest: [fastify.authenticate, fastify.isAdmin],
});
module.exports.patch = fastify => ({
handler: async (req, res) => {
/** @type {import('client')} */
const client = res.context.config.client;
const id = req.params.guild;
const settings = await client.prisma.guild.update({
data: req.body,
where: { id },
});
return settings;
},
onRequest: [fastify.authenticate, fastify.isAdmin],
});

View File

@ -4,6 +4,7 @@ module.exports.get = fastify => ({
const guilds = client.guilds.cache const guilds = client.guilds.cache
.filter(async guild => { .filter(async guild => {
const member = await guild.members.fetch(req.user.payload.id); const member = await guild.members.fetch(req.user.payload.id);
if (!member) return false;
return member.permissions.has('MANAGE_GUILD'); return member.permissions.has('MANAGE_GUILD');
}) })
.map(guild => ({ .map(guild => ({

39
src/routes/api/client.js Normal file
View File

@ -0,0 +1,39 @@
const ms = require('ms');
module.exports.get = () => ({
handler: async (req, res) => {
/** @type {import("client")} */
const client = res.context.config.client;
const cacheKey = 'cache/stats/client';
let cached = await client.keyv.get(cacheKey);
if (!cached) {
const tickets = await client.prisma.ticket.findMany({
select: {
createdAt: true,
firstResponseAt: true,
},
});
const users = await client.prisma.user.findMany({ select: { messageCount: true } });
cached = {
avatar: client.user.avatarURL(),
discriminator: client.user.discriminator,
id: client.user.id,
stats: {
activatedUsers: users.length,
archivedMessages: users.reduce((total, user) => total + user.messageCount, 0), // don't count archivedMessage table rows, they get deleted
avgResponseTime: ms(tickets.reduce((total, ticket) => total + (ticket.firstResponseAt - ticket.createdAt), 0) ?? 1 / tickets.length),
categories: await client.prisma.category.count(),
guilds: client.guilds.cache.size,
members: client.guilds.cache.reduce((t, g) => t + g.memberCount, 0),
tags: await client.prisma.tag.count(),
tickets: tickets.length,
},
username: client.user.username,
};
await client.keyv.set(cacheKey, cached, ms('5m'));
}
return cached;
},
});