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": [
"warn",
{
"code": 150,
"code": 180,
"ignoreRegExpLiterals": true,
"ignoreStrings": true,
"ignoreTemplateLiterals": true,

View File

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

View File

@ -33,25 +33,29 @@
"@eartharoid/dbf": "^0.0.1",
"@eartharoid/dtf": "^2.0.1",
"@fastify/cookie": "^6.0.0",
"@fastify/cors": "^8.0.0",
"@fastify/jwt": "^5.0.1",
"@fastify/oauth2": "^5.0.0",
"@prisma/client": "^3.13.0",
"discord.js": "^13.6.0",
"dotenv": "^16.0.0",
"fastify": "^3.29.0",
"@prisma/client": "^3.15.2",
"discord.js": "^13.8.1",
"dotenv": "^16.0.1",
"fastify": "^4.2.1",
"figlet": "^1.5.2",
"keyv": "^4.3.2",
"leeks.js": "^0.2.4",
"leekslazylogger": "^4.1.7",
"ms": "^2.1.3",
"node-dir": "^0.1.17",
"node-fetch": "2",
"node-fetch": "^2.6.7",
"semver": "^7.3.7",
"terminal-link": "^2.1.1",
"yaml": "^1.10.2"
},
"devDependencies": {
"all-contributors-cli": "^6.20.0",
"eslint": "^8.14.0",
"eslint": "^8.19.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())
errorColour String @default("RED")
feedback Feedback[]
footer String? @default("Discord Tickets by eartharoid")
id String @id @db.VarChar(19)
logChannel String? @db.VarChar(19)
primaryColour String @default("#009999")

View File

@ -2,7 +2,7 @@ const { randomBytes } = require('crypto');
const { short } = require('leeks.js');
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') +
'&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 { Intents } = require('discord.js');
const { PrismaClient } = require('@prisma/client');
const Keyv = require('keyv');
module.exports = class Client extends FrameworkClient {
constructor() {
@ -16,6 +17,7 @@ module.exports = class Client extends FrameworkClient {
async login(token) {
this.prisma = new PrismaClient();
// this.prisma.$use((params, next) => {})
this.keyv = new Keyv();
return super.login(token);
}

View File

@ -7,6 +7,13 @@ const { readFiles } = require('node-dir');
module.exports = client => {
// cors plugins
fastify.register(require('@fastify/cors'), {
credentials: true,
methods: ['DELETE', 'GET', 'PATCH', 'PUT', 'POST'],
origin: true,
});
// oauth2 plugin
fastify.register(oauth, {
callbackUri: `${process.env.HTTP_EXTERNAL}/auth/callback`,
@ -32,14 +39,22 @@ module.exports = client => {
signed: false,
},
// secret: randomBytes(16).toString('hex'),
secret: process.env.DB_ENCRYPTION_KEY,
secret: process.env.ENCRYPTION_KEY,
});
// auth
fastify.decorate('authenticate', async (req, res) => {
try {
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) {
res.send(err);
}
@ -50,14 +65,21 @@ module.exports = client => {
const userId = req.user.payload.id;
const guildId = req.params.guild;
const guild = client.guilds.cache.get(guildId);
const guildMember = await guild.members.fetch(userId);
const isAdmin = guildMember.permissions.has('MANAGE_GUILD');
if (!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',
message: 'User is not authorised for this action',
statusCode: 401,
});
}
const guildMember = await guild.members.fetch(userId);
const isAdmin = guildMember?.permissions.has('MANAGE_GUILD');
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
fastify.listen(process.env.HTTP_BIND, (err, addr) => {
fastify.listen({ port: process.env.HTTP_BIND }, (err, addr) => {
if (err) client.log.error.http(err);
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);
}
if (process.env.DB_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).'));
if (process.env.ENCRYPTION_KEY === undefined) {
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);
}

View File

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

View File

@ -1,6 +1,6 @@
module.exports.get = fastify => ({
handler: async (req, res) => {
/** @type {import('../../../../../../client')} */
/** @type {import('client')} */
const client = res.context.config.client;
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 => ({
handler: async (req, res) => {
/** @type {import('../../../../../../client')} */
/** @type {import('client')} */
const client = res.context.config.client;
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 => ({
handler: async (req, res) => {
/** @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],
});
/* eslint-disable no-underscore-dangle */
const ms = require('ms');
module.exports.get = fastify => ({
handler: async (req, res) => {
/** @type {import('../../../../../client')} */
/** @type {import("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 } }) ??
await client.prisma.guild.create({ data: { id: req.params.guild } });
res.send(settings);
if (!cached) {
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,
},
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 },
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],
});

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
.filter(async guild => {
const member = await guild.members.fetch(req.user.payload.id);
if (!member) return false;
return member.permissions.has('MANAGE_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;
},
});