diff --git a/package.json b/package.json
index 63510b7..629774e 100644
--- a/package.json
+++ b/package.json
@@ -38,6 +38,7 @@
"keyv": "^4.0.3",
"leeks.js": "^0.2.2",
"leekslazylogger-fastify": "^0.1.1",
+ "mustache": "^4.2.0",
"node-emoji": "^1.10.0",
"node-fetch": "^2.6.1",
"semver": "^7.3.4",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2652f22..6347df5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -13,6 +13,7 @@ specifiers:
leeks.js: ^0.2.2
leekslazylogger-fastify: ^0.1.1
mariadb: ^2.5.2
+ mustache: ^4.2.0
mysql2: ^2.2.5
node-emoji: ^1.10.0
node-fetch: ^2.6.1
@@ -38,6 +39,7 @@ dependencies:
keyv: 4.0.3
leeks.js: 0.2.2
leekslazylogger-fastify: 0.1.1
+ mustache: 4.2.0
node-emoji: 1.10.0
node-fetch: 2.6.1
semver: 7.3.4
@@ -1625,6 +1627,11 @@ packages:
/ms/2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+ /mustache/4.2.0:
+ resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==}
+ hasBin: true
+ dev: false
+
/mysql2/2.2.5:
resolution: {integrity: sha512-XRqPNxcZTpmFdXbJqb+/CtYVLCx14x1RTeNMD4954L331APu75IC74GDqnZMEt1kwaXy6TySo55rF2F3YJS78g==}
engines: {node: '>= 8.0'}
diff --git a/src/commands/blacklist.js b/src/commands/blacklist.js
index bff6938..dac17ef 100644
--- a/src/commands/blacklist.js
+++ b/src/commands/blacklist.js
@@ -82,7 +82,7 @@ module.exports = class BlacklistCommand extends Command {
const member_or_role = is_role ? 'role' : 'member';
const index = settings.blacklist.findIndex(element => element === id);
- const new_blacklist = [ ...settings.blacklist ];
+ const new_blacklist = [...settings.blacklist];
if (index === -1) {
new_blacklist.push(id);
diff --git a/src/settings.schema.json b/src/commands/extra/settings.schema.json
similarity index 98%
rename from src/settings.schema.json
rename to src/commands/extra/settings.schema.json
index 73c07f4..2af95d9 100644
--- a/src/settings.schema.json
+++ b/src/commands/extra/settings.schema.json
@@ -66,7 +66,6 @@
},
"required": [
"name",
- "name_format",
"opening_message",
"roles"
]
diff --git a/src/commands/extra/survey.template.html b/src/commands/extra/survey.template.html
new file mode 100644
index 0000000..a20ed5a
--- /dev/null
+++ b/src/commands/extra/survey.template.html
@@ -0,0 +1,67 @@
+
+
+
+
+ {{survey}} Survey Responses | Discord Tickets
+
+
+
+
+
+
+
+
+
+
+
+
+
{{survey}} survey responses
+
+
+
+
+
+
{{count.responses}}
+
Responses
+
+
+
+
+
{{count.users}}
+
Users
+
+
+
+
+
+
+
+
+ {{#columns}}
+ {{.}} |
+ {{/columns}}
+
+
+
+ {{#responses}}
+
+ {{#.}}
+ {{.}} |
+ {{/.}}
+
+ {{/responses}}
+
+
+
+ {{#columns}}
+ {{.}} |
+ {{/columns}}
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/commands/settings.js b/src/commands/settings.js
index b40c487..3333317 100644
--- a/src/commands/settings.js
+++ b/src/commands/settings.js
@@ -19,8 +19,7 @@ module.exports = class SettingsCommand extends Command {
permissions: ['MANAGE_GUILD']
});
- this.schema = require('../settings.schema.json');
-
+ this.schema = require('./extra/settings.schema.json');
this.v = new Validator();
}
@@ -33,7 +32,7 @@ module.exports = class SettingsCommand extends Command {
const settings = await message.guild.settings;
const i18n = this.client.i18n.getLocale(settings.locale);
- const attachments = [ ...message.attachments.values() ];
+ const attachments = [...message.attachments.values()];
if (attachments.length >= 1) {
@@ -207,7 +206,7 @@ module.exports = class SettingsCommand extends Command {
`Settings for ${message.guild.name}.json`
);
- message.channel.send({
+ return await message.channel.send({
files: [attachment]
});
}
diff --git a/src/commands/survey.js b/src/commands/survey.js
new file mode 100644
index 0000000..d6ffdd7
--- /dev/null
+++ b/src/commands/survey.js
@@ -0,0 +1,112 @@
+const Command = require('../modules/commands/command');
+// eslint-disable-next-line no-unused-vars
+const { MessageAttachment, MessageEmbed, Message } = require('discord.js');
+const fsp = require('fs').promises;
+const { path } = require('../utils/fs');
+const mustache = require('mustache');
+
+module.exports = class SurveyCommand extends Command {
+ constructor(client) {
+ const i18n = client.i18n.getLocale(client.config.locale);
+ super(client, {
+ internal: true,
+ name: i18n('commands.survey.name'),
+ description: i18n('commands.survey.description'),
+ aliases: [
+ i18n('commands.survey.aliases.surveys')
+ ],
+ process_args: false,
+ args: [
+ {
+ name: i18n('commands.survey.args.survey.name'),
+ description: i18n('commands.survey.args.survey.description'),
+ example: i18n('commands.survey.args.survey.example'),
+ required: false,
+ }
+ ],
+ staff_only: true
+ });
+ }
+
+ /**
+ * @param {Message} message
+ * @param {string} args
+ * @returns {Promise}
+ */
+ async execute(message, args) {
+ const settings = await message.guild.settings;
+ const i18n = this.client.i18n.getLocale(settings.locale);
+
+ const survey = await this.client.db.models.Survey.findOne({
+ where: {
+ name: args,
+ guild: message.guild.id
+ }
+ });
+
+ if (survey) {
+ const { rows: responses, count } = await this.client.db.models.SurveyResponse.findAndCountAll({
+ where: {
+ survey: survey.id
+ }
+ });
+
+ const users = new Set();
+
+
+ for (const i in responses) {
+ const ticket = await this.client.db.models.Ticket.findOne({
+ where: {
+ id: responses[i].ticket
+ }
+ });
+ users.add(ticket.creator);
+ const answers = responses[i].answers.map(a => this.client.cryptr.decrypt(a));
+ answers.unshift(ticket.number);
+ responses[i] = answers;
+ }
+
+ let template = await fsp.readFile(path('./src/commands/extra/survey.template.html'), {
+ encoding: 'utf8'
+ });
+
+ template = template.replace(/\n|\t/, '');
+
+ survey.questions.unshift('Ticket #');
+
+ const html = mustache.render(template, {
+ survey: survey.name.charAt(0).toUpperCase() + survey.name.slice(1),
+ count: {
+ responses: count,
+ users: users.size
+ },
+ columns: survey.questions,
+ responses
+ });
+
+ const attachment = new MessageAttachment(
+ Buffer.from(html),
+ `${survey.name}.html`
+ );
+
+ return await message.channel.send({
+ files: [attachment]
+ });
+ } else {
+ const surveys = await this.client.db.models.Survey.findAll({
+ where: {
+ guild: message.guild.id
+ }
+ });
+
+ const list = surveys.map(s => `❯ **\`${s.name}\`**`);
+ return await message.channel.send(
+ new MessageEmbed()
+ .setColor(settings.colour)
+ .setTitle(i18n('commands.survey.response.list.title'))
+ .setDescription(list.join('\n'))
+ .setFooter(settings.footer, message.guild.iconURL())
+ );
+ }
+ }
+};
\ No newline at end of file
diff --git a/src/locales/en-GB.json b/src/locales/en-GB.json
index d95c7e5..e170f52 100644
--- a/src/locales/en-GB.json
+++ b/src/locales/en-GB.json
@@ -359,6 +359,25 @@
}
}
},
+ "survey": {
+ "aliases": {
+ "surveys": "surveys"
+ },
+ "args": {
+ "survey": {
+ "description": "The name of the survey to view responses of",
+ "example": "support",
+ "name": "survey"
+ }
+ },
+ "description": "View survey responses",
+ "name": "survey",
+ "response": {
+ "list": {
+ "title": "📃 Surveys"
+ }
+ }
+ },
"tag": {
"aliases": {
"faq": "faq",
@@ -460,6 +479,16 @@
"released": {
"description": "%s has released this ticket.",
"title": "✅ Ticket released"
+ },
+ "survey": {
+ "complete": {
+ "description": "Thank you for your feedback.",
+ "title": "✅ Thank you"
+ },
+ "start": {
+ "description": "Hey, %s. Before this channel is deleted, would you mind completing a quick %d-question survey? React with ✅ to start, or ignore this message.",
+ "title": "❔ Feedback"
+ }
}
}
}
\ No newline at end of file
diff --git a/src/modules/tickets/manager.js b/src/modules/tickets/manager.js
index 18680ec..ca49516 100644
--- a/src/modules/tickets/manager.js
+++ b/src/modules/tickets/manager.js
@@ -51,16 +51,16 @@ module.exports = class TicketManager extends EventEmitter {
})) + 1;
const guild = this.client.guilds.cache.get(guild_id);
- const member = await guild.members.fetch(creator_id);
+ const creator = await guild.members.fetch(creator_id);
const name = cat_row.name_format
- .replace(/{+\s?(user)?name\s?}+/gi, member.displayName)
+ .replace(/{+\s?(user)?name\s?}+/gi, creator.displayName)
.replace(/{+\s?num(ber)?\s?}+/gi, number);
const t_channel = await guild.channels.create(name, {
type: 'text',
- topic: `${member}${topic.length > 0 ? ` | ${topic}` : ''}`,
+ topic: `${creator}${topic.length > 0 ? ` | ${topic}` : ''}`,
parent: category_id,
- reason: `${member.user.tag} requested a new ticket channel`
+ reason: `${creator.user.tag} requested a new ticket channel`
});
t_channel.updateOverwrite(creator_id, {
@@ -68,7 +68,7 @@ module.exports = class TicketManager extends EventEmitter {
READ_MESSAGE_HISTORY: true,
SEND_MESSAGES: true,
ATTACH_FILES: true
- }, `Ticket channel created by ${member.user.tag}`);
+ }, `Ticket channel created by ${creator.user.tag}`);
const t_row = await this.client.db.models.Ticket.create({
id: t_channel.id,
@@ -102,17 +102,17 @@ module.exports = class TicketManager extends EventEmitter {
}
const description = cat_row.opening_message
- .replace(/{+\s?(user)?name\s?}+/gi, member.displayName)
- .replace(/{+\s?(tag|ping|mention)?\s?}+/gi, member.user.toString());
+ .replace(/{+\s?(user)?name\s?}+/gi, creator.displayName)
+ .replace(/{+\s?(tag|ping|mention)?\s?}+/gi, creator.user.toString());
const embed = new MessageEmbed()
.setColor(settings.colour)
- .setAuthor(member.user.username, member.user.displayAvatarURL())
+ .setAuthor(creator.user.username, creator.user.displayAvatarURL())
.setDescription(description)
.setFooter(settings.footer, guild.iconURL());
if (topic) embed.addField(i18n('ticket.opening_message.fields.topic'), topic);
- const sent = await t_channel.send(member.user.toString(), embed);
+ const sent = await t_channel.send(creator.user.toString(), embed);
await sent.pin({ reason: 'Ticket opening message' });
await t_row.update({
@@ -158,11 +158,11 @@ module.exports = class TicketManager extends EventEmitter {
await t_row.update({
topic: this.client.cryptr.encrypt(topic)
});
- await t_channel.setTopic(`${member} | ${topic}`, { reason: 'User updated ticket topic' });
+ await t_channel.setTopic(`${creator} | ${topic}`, { reason: 'User updated ticket topic' });
await sent.edit(
new MessageEmbed()
.setColor(settings.colour)
- .setAuthor(member.user.username, member.user.displayAvatarURL())
+ .setAuthor(creator.user.username, creator.user.displayAvatarURL())
.setDescription(description)
.addField(i18n('ticket.opening_message.fields.topic'), topic)
.setFooter(settings.footer, guild.iconURL())
@@ -196,7 +196,7 @@ module.exports = class TicketManager extends EventEmitter {
}
})();
- this.client.log.info(`${member.user.tag} created a new ticket in "${guild.name}"`);
+ this.client.log.info(`${creator.user.tag} created a new ticket in "${guild.name}"`);
this.emit('create', t_row.id, creator_id);
@@ -222,32 +222,38 @@ module.exports = class TicketManager extends EventEmitter {
const i18n = this.client.i18n.getLocale(settings.locale);
const channel = await this.client.channels.fetch(t_row.id);
- if (closer_id) {
- const member = await guild.members.fetch(closer_id);
+ const close = async () => {
+ const pinned = await channel.messages.fetchPinned();
+ await t_row.update({
+ open: false,
+ closed_by: closer_id || null,
+ closed_reason: reason ? this.client.cryptr.encrypt(reason) : null,
+ pinned_messages: [...pinned.keys()]
+ });
- await this.archives.updateMember(ticket_id, member);
+ if (closer_id) {
+ const closer = await guild.members.fetch(closer_id);
+
+ await this.archives.updateMember(ticket_id, closer);
- if (channel) {
const description = reason
- ? i18n('ticket.closed_by_member_with_reason.description', member.user.toString(), reason)
- : i18n('ticket.closed_by_member.description', member.user.toString());
+ ? i18n('ticket.closed_by_member_with_reason.description', closer.user.toString(), reason)
+ : i18n('ticket.closed_by_member.description', closer.user.toString());
await channel.send(
new MessageEmbed()
.setColor(settings.success_colour)
- .setAuthor(member.user.username, member.user.displayAvatarURL())
+ .setAuthor(closer.user.username, closer.user.displayAvatarURL())
.setTitle(i18n('ticket.closed.title'))
.setDescription(description)
.setFooter(settings.footer, guild.iconURL())
);
setTimeout(async () => {
- await channel.delete(`Ticket channel closed by ${member.user.tag}${reason ? `: "${reason}"` : ''}`);
+ await channel.delete(`Ticket channel closed by ${closer.user.tag}${reason ? `: "${reason}"` : ''}`);
}, 5000);
- }
- this.client.log.info(`${member.user.tag} closed a ticket (${ticket_id})${reason ? `: "${reason}"` : ''}`);
- } else {
- if (channel) {
+ this.client.log.info(`${closer.user.tag} closed a ticket (${ticket_id})${reason ? `: "${reason}"` : ''}`);
+ } else {
const description = reason
? i18n('ticket.closed_with_reason.description')
: i18n('ticket.closed.description');
@@ -262,20 +268,100 @@ module.exports = class TicketManager extends EventEmitter {
setTimeout(async () => {
await channel.delete(`Ticket channel closed${reason ? `: "${reason}"` : ''}`);
}, 5000);
+
+ this.client.log.info(`A ticket was closed (${ticket_id})${reason ? `: "${reason}"` : ''}`);
}
+ };
- this.client.log.info(`A ticket was closed (${ticket_id})${reason ? `: "${reason}"` : ''}`);
+ if (channel) {
+ const creator = await guild.members.fetch(t_row.creator);
+
+ const cat_row = await this.client.db.models.Category.findOne({
+ where: {
+ id: t_row.category
+ }
+ });
+
+ if (creator && cat_row.survey) {
+ const survey = await this.client.db.models.Survey.findOne({
+ where: {
+ guild: t_row.guild,
+ name: cat_row.survey
+ }
+ });
+
+ if (survey) {
+ const r_collector_message = await channel.send(
+ creator.toString(),
+ new MessageEmbed()
+ .setColor(settings.colour)
+ .setTitle(i18n('ticket.survey.start.title'))
+ .setDescription(i18n('ticket.survey.start.description', creator.toString(), survey.questions.length))
+ .setFooter(i18n('collector_expires_in', 60))
+ );
+
+ await r_collector_message.react('✅');
+
+ const collector_filter = (reaction, user) => {
+ return user.id === creator.user.id && reaction.emoji.name === '✅';
+ };
+
+ const r_collector = r_collector_message.createReactionCollector(collector_filter, {
+ time: 60000
+ });
+
+ r_collector.on('collect', async () => {
+ r_collector.stop();
+ const filter = message => message.author.id === creator.id;
+ let answers = [];
+ let number = 1;
+ for (const question of survey.questions) {
+ await channel.send(
+ new MessageEmbed()
+ .setColor(settings.colour)
+ .setTitle(`${number++}/${survey.questions.length}`)
+ .setDescription(question)
+ .setFooter(i18n('collector_expires_in', 60))
+ );
+
+ try {
+ const collected = await channel.awaitMessages(filter, { max: 1, time: 60000, errors: ['time'] });
+ answers.push(collected.first().content);
+ } catch (collected) {
+ return await close();
+ }
+ }
+
+ await channel.send(
+ new MessageEmbed()
+ .setColor(settings.success_colour)
+ .setTitle(i18n('ticket.survey.complete.title'))
+ .setDescription(i18n('ticket.survey.complete.description'))
+ .setFooter(settings.footer, guild.iconURL())
+ );
+
+ answers = answers.map(a => this.client.cryptr.encrypt(a));
+ await this.client.db.models.SurveyResponse.create({
+ answers,
+ survey: survey.id,
+ ticket: t_row.id
+ });
+
+ await close();
+
+ });
+
+ r_collector.on('end', async (collected) => {
+ if (collected.size === 0) {
+ await close();
+ }
+ });
+ }
+ } else {
+ await close();
+ }
}
- const pinned = await channel.messages.fetchPinned();
-
- await t_row.update({
- open: false,
- closed_by: closer_id || null,
- closed_reason: reason ? this.client.cryptr.encrypt(reason) : null,
- pinned_messages: [...pinned.keys()]
- });
-
this.emit('close', ticket_id);
return t_row;
}