diff --git a/package.json b/package.json index d92d681..7be000b 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,8 @@ "contributors:generate": "all-contributors generate", "keygen": "node scripts/keygen", "lint": "eslint src scripts --ext mjs --fix", + "start": "node .", + "studio": "npx prisma studio", "test": "echo \"There's nothing to test\" && exit 1" }, "repository": { @@ -37,7 +39,7 @@ "@fastify/cors": "^8.0.0", "@fastify/jwt": "^5.0.1", "@fastify/oauth2": "^5.0.0", - "@prisma/client": "^4.0.0", + "@prisma/client": "^4.1.0", "cryptr": "^6.0.3", "discord.js": "^14.0.2", "dotenv": "^16.0.1", @@ -50,7 +52,7 @@ "node-dir": "^0.1.17", "node-emoji": "^1.11.0", "object-diffy": "^1.0.4", - "prisma": "^4.0.0", + "prisma": "^4.1.0", "semver": "^7.3.7", "terminal-link": "^2.1.1", "yaml": "^1.10.2" diff --git a/prisma/schema.prisma b/prisma/schema.prisma index bb98b2f..12ffb19 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -114,6 +114,8 @@ model Guild { archive Boolean @default(true) blocklist Json @default("[]") categories Category[] + claimButton Boolean @default(false) + closeButton Boolean @default(false) createdAt DateTime @default(now()) errorColour String @default("Red") feedback Feedback[] diff --git a/src/i18n/en-GB.yml b/src/i18n/en-GB.yml index 8366a9d..dc5993a 100644 --- a/src/i18n/en-GB.yml +++ b/src/i18n/en-GB.yml @@ -1,4 +1,8 @@ -command: +buttons: + create: + emoji: '🎫' + text: 'Create a ticket' +commands: log: admin: changes: 'Changes' @@ -6,16 +10,21 @@ log: joined: '{user} {verb} {targetType}' target: category: 'a category' + panel: 'a panel' question: 'a question' settings: 'the settings' title: joined: '{targetType} {verb}' target: category: 'Category' + panel: 'Panel' question: 'Question' settings: 'Settings' verb: create: 'created' delete: 'deleted' update: 'updated' - ticket: \ No newline at end of file + tickets: +menus: + panel: + placeholder: 'Select a ticket category' \ No newline at end of file diff --git a/src/lib/logging.js b/src/lib/logging.js index 5165780..2464618 100644 --- a/src/lib/logging.js +++ b/src/lib/logging.js @@ -5,7 +5,7 @@ const { const { diff: getDiff } = require('object-diffy'); -const exists = thing => (typeof thing === 'string' && thing.length > 0) && thing !== null && thing !== undefined; +const exists = thing => (typeof thing === 'string' && thing.length > 0) && (thing !== null && thing !== undefined); const arrToObj = obj => { for (const key in obj) { @@ -26,7 +26,7 @@ function makeDiff({ for (const key in diff) { if (key === 'createdAt') continue; // object-diffy doesn't like dates const from = exists(diff[key].from) ? `- ${String(diff[key].from).replace(/\n/g, '\\n')}\n` : ''; - const to = exists(diff[key].to) ? `+ ${String(diff[key].to).replace(/\n/g, '\\n')}\n` : ''; + const to = exists(diff[key].to) ? `+ ${String(diff[key].to).replace(/\n/g, '\\n')}\n` : ''; fields.push({ inline: true, name: key, @@ -70,6 +70,10 @@ async function logAdminEvent(client, { where: { id: guildId }, }); if (!settings.logChannel) return; + const colour = action === 'create' + ? 'Green' : action === 'update' + ? 'Orange' : action === 'delete' + ? 'Red' : 'Default'; const getMessage = client.i18n.getLocale(settings.locale); const i18nOptions = { user: `<@${user.id}>`, @@ -79,7 +83,7 @@ async function logAdminEvent(client, { if (!channel) return; const embeds = [ new EmbedBuilder() - .setColor('Orange') + .setColor(colour) .setAuthor({ iconURL: user.avatarURL(), name: user.username, @@ -105,7 +109,7 @@ async function logAdminEvent(client, { if (diff && diff.original) { embeds.push( new EmbedBuilder() - .setColor('Orange') + .setColor(colour) .setTitle(getMessage('log.admin.changes')) .setFields(makeDiff(diff)), ); diff --git a/src/routes/api/admin/guilds/[guild]/panels.js b/src/routes/api/admin/guilds/[guild]/panels.js new file mode 100644 index 0000000..3e0a211 --- /dev/null +++ b/src/routes/api/admin/guilds/[guild]/panels.js @@ -0,0 +1,141 @@ +const { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle: { + Primary, + Secondary, + }, + ChannelType: { GuildText }, + EmbedBuilder, + SelectMenuBuilder, + SelectMenuOptionBuilder, +} = require('discord.js'); +const emoji = require('node-emoji'); +const { logAdminEvent } = require('../../../../../lib/logging'); + +module.exports.post = fastify => ({ + handler: async (req, res) => { + /** @type {import('client')} */ + const client = res.context.config.client; + const guild = client.guilds.cache.get(req.params.guild); + const data = req.body; + + const settings = await client.prisma.guild.findUnique({ + select: { + categories: true, + footer: true, + locale: true, + primaryColour: true, + }, + where: { id: guild.id }, + }); + const getMessage = client.i18n.getLocale(settings.locale); + const categories = settings.categories.filter(c => data.categories.includes(c.id)); + if (categories.length === 0) throw new Error('No categories'); + if (categories.length !== 1 && data.type === 'MESSAGE') throw new Error('Invalid number of categories for panel type'); + + let channel; + if (data.channel) { + channel = await client.channels.fetch(data.channel); + } else { + const allow = ['ViewChannel', 'ReadMessageHistory']; + if (data.type === 'MESSAGE') allow.push('SendMessages'); + channel = await guild.channels.create({ + name: 'create-a-ticket', + permissionOverwrites: [ + { + allow, + deny: ['AddReactions', 'AttachFiles'], + id: guild.roles.everyone, + }, + ], + position: 1, + rateLimitPerUser: 15, + reason: 'New ticket panel', + type: GuildText, + }); + } + + const embed = new EmbedBuilder() + .setColor(settings.primaryColour) + .setTitle(data.title) + .setFooter({ + iconURL: guild.iconURL(), + text: settings.footer, + }); + + if (data.description) embed.setDescription(data.description); + if (data.image) embed.setImage(data.image); + if (data.thumbnail) embed.setThumbnail(data.thumbnail); + + if (data.type === 'MESSAGE') { + await channel.send({ embeds: [embed] }); + } else { + const components = []; + + if (categories.length === 1) { + components.push( + new ButtonBuilder() + .setCustomId(JSON.stringify({ + action: 'createTicket', + target: categories[0].id, + })) + .setStyle(Primary) + .setLabel(getMessage('buttons.create.text')) + .setEmoji(getMessage('buttons.create.emoji')), + ); + } else { + if (data.type === 'BUTTON') { + components.push( + ...categories.map(category => + new ButtonBuilder() + .setCustomId(JSON.stringify({ + action: 'createTicket', + target: category.id, + })) + .setStyle(Secondary) + .setLabel(category.name) + .setEmoji(emoji.hasEmoji(category.emoji) ? emoji.get(category.emoji) : { id: category.emoji }), + ), + ); + } else { + components.push( + new SelectMenuBuilder() + .setCustomId('createTicket') + .setPlaceholder(getMessage('menus.panel.placeholder')) + .setOptions( + categories.map(category => + new SelectMenuOptionBuilder() + .setValue(String(category.id)) + .setLabel(category.name) + .setDescription(category.description) + .setEmoji(emoji.hasEmoji(category.emoji) ? emoji.get(category.emoji) : { id: category.emoji }), + ), + ), + ); + } + } + + await channel.send({ + components: [ + new ActionRowBuilder() + .setComponents(components), + ], + embeds: [embed], + }); + } + + logAdminEvent(client, { + action: 'create', + guildId: guild.id, + target: { + id: channel.toString(), + type: 'panel', + }, + userId: req.user.payload.id, + }); + + return true; + }, + onRequest: [fastify.authenticate, fastify.isAdmin], +}); \ No newline at end of file