This commit is contained in:
Isaac 2021-05-22 00:04:18 +01:00
parent 777719d1b3
commit 32f43ac18f
No known key found for this signature in database
GPG Key ID: F6812DBC6719B4E3
58 changed files with 837 additions and 926 deletions

View File

@ -5,10 +5,73 @@ module.exports = {
'node': true
},
'extends': 'eslint:recommended',
'parserOptions': {
'ecmaVersion': 12
},
'parserOptions': { 'ecmaVersion': 12 },
'rules': {
'array-bracket-newline': [
'error',
'consistent'
],
'array-bracket-spacing': [
'error',
'never'
],
'array-element-newline': [
'error',
'consistent'
],
'arrow-body-style': [
'error',
'as-needed'
],
'arrow-parens': [
'error',
'as-needed'
],
'block-spacing': [
'error',
'always'
],
'brace-style': [
'error',
'1tbs'
],
'comma-dangle': [
'error',
'never'
],
'comma-spacing': [
'error',
{
'after': true,
'before': false
}
],
'comma-style': [
'error',
'last'
],
'computed-property-spacing': [
'error',
'never'
],
'curly': [
'error',
'multi-line', // 'multi'
'consistent'
],
'default-case-last': [
'error'
],
'dot-location': [
'error',
'property'
],
'dot-notation': [
'error'
],
'eqeqeq': [
'error'
],
'indent': [
'error',
'tab'
@ -17,6 +80,74 @@ module.exports = {
'off',
'windows'
],
'max-depth': [
'warn',
{ 'max': 5 }
],
'max-len': [
'warn',
{
'code': 150,
'ignoreRegExpLiterals': true,
'ignoreStrings': true,
'ignoreTemplateLiterals': true,
'ignoreTrailingComments': true,
'ignoreUrls': true
}
],
'max-lines': [
'warn'
],
'max-statements-per-line': [
'error'
],
'multiline-comment-style': [
'warn'
],
'no-console': [
'warn'
],
'no-return-assign': [
'error'
],
'no-template-curly-in-string': [
'warn'
],
'no-trailing-spaces': [
'error'
],
'no-underscore-dangle': [
'error'
],
'no-unneeded-ternary': [
'error'
],
'no-var': [
'error'
],
'no-whitespace-before-property': [
'error'
],
'object-curly-newline': [
'error',
{
'minProperties': 2,
'multiline': true
}
],
'object-curly-spacing': [
'error',
'always'
],
'object-property-newline': [
'error'
],
'operator-linebreak': [
'error'
],
'prefer-arrow-callback': [
'error'
],
'prefer-const': [
'error',
{
@ -32,5 +163,14 @@ module.exports = {
'error',
'always'
],
'sort-keys': [
'error',
'asc',
{ 'natural': true }
],
'space-in-parens': [
'error',
'never'
]
}
};

View File

@ -1,26 +1,17 @@
<img src='https://discordtickets.app/img/logo-small-circle.png' align='left' width='180px' height='180px' style='margin: 30px 40px 0 0'/>
<!-- <img align='left' width='0' height='192px' hspace='10'/> -->
<!-- omit in toc -->
# [Discord Tickets](https://discordtickets.app)
[![GitHub stars](https://img.shields.io/github/stars/discord-tickets/bot?style=flat-square)](https://github.com/discord-tickets/bot/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/discord-tickets/bot?style=flat-square)](https://github.com/discord-tickets/bot/stargazers)
[![License](https://img.shields.io/github/license/discord-tickets/bot?style=flat-square)](https://github.com/discord-tickets/bot/blob/master/LICENSE)
![Codacy grade](https://img.shields.io/codacy/grade/14e6851c85444424b75b8bc3f93e93db?logo=codacy&style=flat-square)
[![Discord](https://img.shields.io/discord/451745464480432129?label=discord&color=7289DA&style=flat-square)](https://discord.gg/pXc9vyC)
[![Codacy](https://img.shields.io/codacy/grade/b974eb5f984c40868e07d82c968bd02d?logo=codacy&style=flat-square)](https://www.codacy.com/gh/discord-tickets/bot/dashboard?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=discord-tickets/bot&amp;utm_campaign=Badge_Grade)
[![Discord](https://img.shields.io/discord/451745464480432129?label=discord&color=7289DA&style=flat-square)](https://go.eartharoid.me/discord)
[![Crowdin](https://badges.crowdin.net/discord-tickets/localized.svg)](https://i18n.discordtickets.app/project/discord-tickets)
<br>
![Discord Tickets](https://ik.imagekit.io/eartharoid/discord-tickets/wordmark/gradient_HTC7nqZr9.png)
An open-source ticket management bot for Discord - a free alternative to the premium and white-label plans of other popular ticketing bots.
<br><br>
⚠️
**The code on the master branch is for v3 which is not ready. Please go to the [v2 branch](https://github.com/discord-tickets/bot/tree/v2) for the current version of the code and [documentation](https://github.com/discord-tickets/bot/wiki)**.
⚠️
**The following information is about v3. Please read the README file on the [v2 branch](https://github.com/discord-tickets/bot/tree/v2) for information that is about the current version.**
## What is this?
Discord Tickets is a Discord bot for creating and managing "support ticket" channels. It is a free and open-source alternative to the popular paid "premium" and "white-label" ticketing bots, such as [Ticket Tool](https://tickettool.xyz/), [TicketsBot](https://ticketsbot.net/), [Tickety](https://tickety.net/), [Helper.gg](https://helper.gg/), [Helper](https://helper.wtf), and others.
@ -65,7 +56,7 @@ If you choose SQLite, which is the default as it is the easiest, you don't need
### Screenshots
> screenshot of a ticket channel
![The opening message of a ticket channel](https://i.imgur.com/2T5eSH0.png)
<!-- -->
> screenshot of a panel
@ -117,6 +108,6 @@ Does your community or company use Discord Tickets? Sponsor the project to get y
Discord Tickets is licensed under the [GPLv3 license](https://github.com/discord-tickets/bot/blob/master/LICENSE).
Discord Tickets is not related to Discord Inc.
This is not an official Discord product. It is not affiliated with nor endorsed by Discord Inc.
© 2021 Isaac Saunders

View File

@ -1,5 +0,0 @@
{
"include": [
"src/**/*.js"
]
}

View File

@ -1,7 +1,10 @@
/* eslint-disable no-console */
const link = require('terminal-link');
const leeks = require('leeks.js');
const { version, homepage } = require('../package.json');
const {
version, homepage
} = require('../package.json');
module.exports = () => {
console.log(leeks.colours.cyan(`

View File

@ -1,30 +1,32 @@
const Command = require('../modules/commands/command');
// eslint-disable-next-line no-unused-vars
const { MessageEmbed, Message } = require('discord.js');
const {
Message, // eslint-disable-line no-unused-vars
MessageEmbed
} = require('discord.js');
module.exports = class AddCommand extends Command {
constructor(client) {
const i18n = client.i18n.getLocale(client.config.locale);
super(client, {
internal: true,
name: i18n('commands.add.name'),
description: i18n('commands.add.description'),
aliases: [],
process_args: false,
args: [
{
name: i18n('commands.add.args.member.name'),
description: i18n('commands.add.args.member.description'),
example: i18n('commands.add.args.member.example'),
required: true,
name: i18n('commands.add.args.member.name'),
required: true
},
{
name: i18n('commands.add.args.ticket.name'),
description: i18n('commands.add.args.ticket.description'),
example: i18n('commands.add.args.ticket.example'),
required: false,
name: i18n('commands.add.args.ticket.name'),
required: false
}
]
],
description: i18n('commands.add.description'),
internal: true,
name: i18n('commands.add.name'),
process_args: false
});
}
@ -38,7 +40,7 @@ module.exports = class AddCommand extends Command {
const i18n = this.client.i18n.getLocale(settings.locale);
const ticket = message.mentions.channels.first() ?? message.channel;
const t_row = await this.client.tickets.resolve(ticket.id, message.guild.id);
const t_row = await this.client.tickets.resolve(ticket.id, message.guild.id);
if (!t_row) {
return await message.channel.send(
@ -93,10 +95,10 @@ module.exports = class AddCommand extends Command {
);
await ticket.updateOverwrite(member, {
VIEW_CHANNEL: true,
ATTACH_FILES: true,
READ_MESSAGE_HISTORY: true,
SEND_MESSAGES: true,
ATTACH_FILES: true
VIEW_CHANNEL: true
}, `${message.author.tag} added ${member.user.tag} to the ticket`);
await this.client.tickets.archives.updateMember(ticket.id, member);

View File

@ -1,27 +1,29 @@
const Command = require('../modules/commands/command');
// eslint-disable-next-line no-unused-vars
const { MessageEmbed, Message } = require('discord.js');
const {
Message, // eslint-disable-line no-unused-vars
MessageEmbed
} = require('discord.js');
module.exports = class BlacklistCommand extends Command {
constructor(client) {
const i18n = client.i18n.getLocale(client.config.locale);
super(client, {
internal: true,
name: i18n('commands.blacklist.name'),
description: i18n('commands.blacklist.description'),
aliases: [
i18n('commands.blacklist.aliases.unblacklist'),
i18n('commands.blacklist.aliases.unblacklist')
],
process_args: false,
args: [
{
name: i18n('commands.blacklist.args.member_or_role.name'),
description: i18n('commands.blacklist.args.member_or_role.description'),
example: i18n('commands.blacklist.args.member_or_role.example'),
required: false,
name: i18n('commands.blacklist.args.member_or_role.name'),
required: false
}
],
permissions: ['MANAGE_GUILD']
description: i18n('commands.blacklist.description'),
internal: true,
name: i18n('commands.blacklist.name'),
permissions: ['MANAGE_GUILD'],
process_args: false
});
}
@ -45,16 +47,19 @@ module.exports = class BlacklistCommand extends Command {
.setFooter(settings.footer, message.guild.iconURL())
);
}
const role = message.mentions.roles.first();
let id;
const input = args.trim().split(/\s/g)[0];
if (member) id = member.id;
else if (role) id = role.id;
else if (/\d{17,19}/.test(input)) id = input;
else if (settings.blacklist?.length === 0) {
if (member) {
id = member.id;
} else if (role) {
id = role.id;
} else if (/\d{17,19}/.test(input)) {
id = input;
} else if (settings.blacklist?.length === 0) {
return await message.channel.send(
new MessageEmbed()
.setColor(settings.colour)

View File

@ -1,6 +1,10 @@
const Command = require('../modules/commands/command');
// eslint-disable-next-line no-unused-vars
const { MessageEmbed, MessageMentions, Message } = require('discord.js');
const {
Message, // eslint-disable-line no-unused-vars
MessageEmbed,
MessageMentions
} = require('discord.js');
const { Op } = require('sequelize');
const toTime = require('to-time-monthsfork');
const { footer } = require('../utils/discord');
@ -9,43 +13,40 @@ module.exports = class CloseCommand extends Command {
constructor(client) {
const i18n = client.i18n.getLocale(client.config.locale);
super(client, {
internal: true,
name: i18n('commands.close.name'),
description: i18n('commands.close.description'),
aliases: [
i18n('commands.close.aliases.delete'),
i18n('commands.close.aliases.lock'),
i18n('commands.close.aliases.lock')
],
process_args: true,
args: [
{
name: i18n('commands.close.args.ticket.name'),
alias: i18n('commands.close.args.ticket.alias'),
description: i18n('commands.close.args.ticket.description'),
example: i18n('commands.close.args.ticket.example'),
name: i18n('commands.close.args.ticket.name'),
required: false,
// for arg parsing
alias: i18n('commands.close.args.ticket.alias'),
type: String
},
{
name: i18n('commands.close.args.reason.name'),
alias: i18n('commands.close.args.reason.alias'),
description: i18n('commands.close.args.reason.description'),
example: i18n('commands.close.args.reason.example'),
name: i18n('commands.close.args.reason.name'),
required: false,
// for arg parsing
alias: i18n('commands.close.args.reason.alias'),
type: String
},
{
name: i18n('commands.close.args.time.name'),
alias: i18n('commands.close.args.time.alias'),
description: i18n('commands.close.args.time.description'),
example: i18n('commands.close.args.time.example'),
name: i18n('commands.close.args.time.name'),
required: false,
// for arg parsing
alias: i18n('commands.close.args.time.alias'),
type: String
}
]
],
description: i18n('commands.close.description'),
internal: true,
name: i18n('commands.close.name'),
process_args: true
});
}
@ -79,10 +80,8 @@ module.exports = class CloseCommand extends Command {
const tickets = await this.client.db.models.Ticket.findAndCountAll({
where: {
last_message: {
[Op.lte]: new Date(Date.now() - period)
},
guild: message.guild.id
guild: message.guild.id,
last_message: { [Op.lte]: new Date(Date.now() - period) }
}
});
@ -105,13 +104,9 @@ module.exports = class CloseCommand extends Command {
await collector_message.react('✅');
const collector_filter = (reaction, user) => {
return user.id === message.author.id && reaction.emoji.name === '✅';
};
const collector_filter = (reaction, user) => user.id === message.author.id && reaction.emoji.name === '✅';
const collector = collector_message.createReactionCollector(collector_filter, {
time: 30000
});
const collector = collector_message.createReactionCollector(collector_filter, { time: 30000 });
collector.on('collect', async () => {
await collector_message.reactions.removeAll();
@ -130,7 +125,7 @@ module.exports = class CloseCommand extends Command {
});
collector.on('end', async (collected) => {
collector.on('end', async collected => {
if (collected.size === 0) {
await collector_message.reactions.removeAll();
await collector_message.edit(
@ -169,11 +164,7 @@ module.exports = class CloseCommand extends Command {
);
}
} else {
t_row = await this.client.db.models.Ticket.findOne({
where: {
id: message.channel.id
}
});
t_row = await this.client.db.models.Ticket.findOne({ where: { id: message.channel.id } });
if (!t_row) {
return await message.channel.send(
@ -196,13 +187,9 @@ module.exports = class CloseCommand extends Command {
await collector_message.react('✅');
const collector_filter = (reaction, user) => {
return user.id === message.author.id && reaction.emoji.name === '✅';
};
const collector_filter = (reaction, user) => user.id === message.author.id && reaction.emoji.name === '✅';
const collector = collector_message.createReactionCollector(collector_filter, {
time: 30000
});
const collector = collector_message.createReactionCollector(collector_filter, { time: 30000 });
collector.on('collect', async () => {
collector.stop();
@ -223,7 +210,7 @@ module.exports = class CloseCommand extends Command {
await this.client.tickets.close(t_row.id, message.author.id, message.guild.id, args[arg_reason]);
});
collector.on('end', async (collected) => {
collector.on('end', async collected => {
if (collected.size === 0) {
await collector_message.reactions.removeAll();
await collector_message.edit(
@ -244,7 +231,7 @@ module.exports = class CloseCommand extends Command {
}, 15000);
}
});
}
}
};

View File

@ -1,33 +1,35 @@
const Command = require('../modules/commands/command');
// eslint-disable-next-line no-unused-vars
const { MessageEmbed, Message } = require('discord.js');
const {
Message, // eslint-disable-line no-unused-vars
MessageEmbed
} = require('discord.js');
module.exports = class HelpCommand extends Command {
constructor(client) {
const i18n = client.i18n.getLocale(client.config.locale);
super(client, {
internal: true,
name: i18n('commands.help.name'),
description: i18n('commands.help.description'),
aliases: [
i18n('commands.help.aliases.command'),
i18n('commands.help.aliases.commands'),
i18n('commands.help.aliases.commands')
],
process_args: false,
args: [
{
name: i18n('commands.help.args.command.name'),
description: i18n('commands.help.args.command.description'),
example: i18n('commands.help.args.command.example'),
required: false,
name: i18n('commands.help.args.command.name'),
required: false
}
]
],
description: i18n('commands.help.description'),
internal: true,
name: i18n('commands.help.name'),
process_args: false
});
}
/**
* @param {Message} message
* @param {string} args
* @param {Message} message
* @param {string} args
* @returns {Promise<void|any>}
*/
async execute(message, args) {
@ -54,9 +56,7 @@ module.exports = class HelpCommand extends Command {
new MessageEmbed()
.setColor(settings.colour)
.setTitle(i18n('commands.help.response.list.title'))
.setDescription(i18n('commands.help.response.list.description', {
prefix: settings.command_prefix,
}))
.setDescription(i18n('commands.help.response.list.description', { prefix: settings.command_prefix }))
.addField(i18n('commands.help.response.list.fields.commands'), list.join('\n'))
.setFooter(settings.footer, message.guild.iconURL())
);

View File

@ -1,6 +1,8 @@
const Command = require('../modules/commands/command');
// eslint-disable-next-line no-unused-vars
const { MessageEmbed, Message } = require('discord.js');
const {
Message, // eslint-disable-line no-unused-vars
MessageEmbed
} = require('discord.js');
const { footer } = require('../utils/discord');
const { letters } = require('../utils/emoji');
const { wait } = require('../utils');
@ -9,23 +11,23 @@ module.exports = class NewCommand extends Command {
constructor(client) {
const i18n = client.i18n.getLocale(client.config.locale);
super(client, {
internal: true,
name: i18n('commands.new.name'),
description: i18n('commands.new.description'),
aliases: [
i18n('commands.new.aliases.create'),
i18n('commands.new.aliases.open'),
i18n('commands.new.aliases.ticket'),
i18n('commands.new.aliases.ticket')
],
process_args: false,
args: [
{
name: i18n('commands.new.args.topic.name'),
description: i18n('commands.new.args.topic.description'),
example: i18n('commands.new.args.topic.example'),
required: false,
name: i18n('commands.new.args.topic.name'),
required: false
}
]
],
description: i18n('commands.new.description'),
internal: true,
name: i18n('commands.new.name'),
process_args: false
});
}
@ -114,11 +116,7 @@ module.exports = class NewCommand extends Command {
}, 15000);
};
const categories = await this.client.db.models.Category.findAndCountAll({
where: {
guild: message.guild.id
}
});
const categories = await this.client.db.models.Category.findAndCountAll({ where: { guild: message.guild.id } });
if (categories.count === 0) {
return await message.channel.send(
@ -153,21 +151,21 @@ module.exports = class NewCommand extends Command {
return user.id === message.author.id && allowed.includes(reaction.emoji.name);
};
const collector = collector_message.createReactionCollector(collector_filter, {
time: 30000
});
const collector = collector_message.createReactionCollector(collector_filter, { time: 30000 });
collector.on('collect', async (reaction) => {
collector.on('collect', async reaction => {
collector.stop();
const index = letters_array.findIndex(value => value === reaction.emoji.name); // find where the letter is in the alphabet
if (index === -1) return setTimeout(async () => {
await collector_message.delete();
}, 15000);
if (index === -1) {
return setTimeout(async () => {
await collector_message.delete();
}, 15000);
}
await collector_message.reactions.removeAll();
create(categories.rows[index], collector_message); // create the ticket, passing the existing response message to be edited instead of creating a new one
});
collector.on('end', async (collected) => {
collector.on('end', async collected => {
if (collected.size === 0) {
await collector_message.reactions.removeAll();
await collector_message.edit(

View File

@ -1,59 +1,59 @@
const Command = require('../modules/commands/command');
// eslint-disable-next-line no-unused-vars
const { MessageEmbed, Message } = require('discord.js');
const { some, wait } = require('../utils');
const {
Message, // eslint-disable-line no-unused-vars
MessageEmbed
} = require('discord.js');
const {
some, wait
} = require('../utils');
const { emojify } = require('node-emoji');
module.exports = class PanelCommand extends Command {
constructor(client) {
const i18n = client.i18n.getLocale(client.config.locale);
super(client, {
internal: true,
name: i18n('commands.panel.name'),
description: i18n('commands.panel.description'),
aliases: [],
process_args: true,
args: [
{
name: i18n('commands.panel.args.title.name'),
alias: i18n('commands.panel.args.title.alias'),
description: i18n('commands.panel.args.title.description'),
example: i18n('commands.panel.args.title.example'),
name: i18n('commands.panel.args.title.name'),
required: false,
// for arg parsing
alias: i18n('commands.panel.args.title.alias'),
type: String
},
{
name: i18n('commands.panel.args.description.name'),
alias: i18n('commands.panel.args.description.alias'),
description: i18n('commands.panel.args.description.description'),
example: i18n('commands.panel.args.description.example'),
name: i18n('commands.panel.args.description.name'),
required: true,
// for arg parsing
alias: i18n('commands.panel.args.description.alias'),
type: String
},
{
name: i18n('commands.panel.args.emoji.name'),
alias: i18n('commands.panel.args.emoji.alias'),
description: i18n('commands.panel.args.emoji.description'),
example: i18n('commands.panel.args.emoji.example'),
required: false,
// for arg parsing
alias: i18n('commands.panel.args.emoji.alias'),
type: String,
multiple: true,
name: i18n('commands.panel.args.emoji.name'),
required: false,
type: String
},
{
name: i18n('commands.panel.args.categories.name'),
alias: i18n('commands.panel.args.categories.alias'),
description: i18n('commands.panel.args.categories.description'),
example: i18n('commands.panel.args.categories.example'),
required: true,
// for arg parsing
alias: i18n('commands.panel.args.categories.alias'),
type: String,
multiple: true,
name: i18n('commands.panel.args.categories.name'),
required: true,
type: String
}
],
permissions: ['MANAGE_GUILD']
description: i18n('commands.panel.description'),
internal: true,
name: i18n('commands.panel.name'),
permissions: ['MANAGE_GUILD'],
process_args: true
});
}
@ -72,21 +72,20 @@ module.exports = class PanelCommand extends Command {
const settings = await message.guild.getSettings();
const i18n = this.client.i18n.getLocale(settings.locale);
if (!args[arg_emoji])
args[arg_emoji] = [];
if (!args[arg_emoji]) args[arg_emoji] = [];
args[arg_emoji] = args[arg_emoji].map(emoji => emojify(emoji.replace(/\\/g, '')));
const invalid_category = await some(args[arg_categories], async id => {
const cat_row = await this.client.db.models.Category.findOne({
where: {
id: id,
guild: message.guild.id
guild: message.guild.id,
id
}
});
return !cat_row;
});
if (invalid_category) {
return await message.channel.send(
new MessageEmbed()
@ -99,36 +98,35 @@ module.exports = class PanelCommand extends Command {
let panel_channel,
panel_message;
let categories_map = args[arg_categories][0];
const embed = new MessageEmbed()
.setColor(settings.colour)
.setFooter(settings.footer, message.guild.iconURL());
if (args[arg_title])
embed.setTitle(args[arg_title]);
if (args[arg_title]) embed.setTitle(args[arg_title]);
if (args[arg_emoji].length === 0) {
// reaction-less panel
panel_channel = await message.guild.channels.create('create-a-ticket', {
type: 'text',
rateLimitPerUser: 30,
permissionOverwrites: [
{
id: message.guild.roles.everyone,
allow: ['VIEW_CHANNEL', 'SEND_MESSAGES', 'READ_MESSAGE_HISTORY'],
deny: ['ATTACH_FILES', 'EMBED_LINKS', 'ADD_REACTIONS']
deny: ['ATTACH_FILES', 'EMBED_LINKS', 'ADD_REACTIONS'],
id: message.guild.roles.everyone
},
{
id: this.client.user.id,
allow: ['EMBED_LINKS']
allow: ['EMBED_LINKS'],
id: this.client.user.id
}
],
position: 1,
reason: `${message.author.tag} created a new reaction-less panel`
rateLimitPerUser: 30,
reason: `${message.author.tag} created a new reaction-less panel`,
type: 'text'
});
embed.setDescription(args[arg_description]);
panel_message = await panel_channel.send(embed);
@ -145,20 +143,20 @@ module.exports = class PanelCommand extends Command {
);
} else {
panel_channel = await message.guild.channels.create('create-a-ticket', {
type: 'text',
permissionOverwrites: [
{
id: message.guild.roles.everyone,
allow: ['VIEW_CHANNEL', 'READ_MESSAGE_HISTORY'],
deny: ['SEND_MESSAGES', 'ADD_REACTIONS']
deny: ['SEND_MESSAGES', 'ADD_REACTIONS'],
id: message.guild.roles.everyone
},
{
id: this.client.user.id,
allow: ['SEND_MESSAGES', 'EMBED_LINKS']
allow: ['SEND_MESSAGES', 'EMBED_LINKS'],
id: this.client.user.id
}
],
position: 1,
reason: `${message.author.tag} created a new panel`
reason: `${message.author.tag} created a new panel`,
type: 'text'
});
if (args[arg_emoji].length === 1) {
@ -172,13 +170,13 @@ module.exports = class PanelCommand extends Command {
// multi category
let description = '';
categories_map = {};
for (const i in args[arg_emoji]) {
categories_map[args[arg_emoji][i]] = args[arg_categories][i];
const cat_row = await this.client.db.models.Category.findOne({
where: {
id: args[arg_categories][i],
guild: message.guild.id
guild: message.guild.id,
id: args[arg_categories][i]
}
});
description += `\n> ${args[arg_emoji][i]} | ${cat_row.name}`;
@ -191,7 +189,7 @@ module.exports = class PanelCommand extends Command {
await panel_message.react(emoji);
await wait(1000); // 1 reaction per second rate-limit
}
}
this.client.log.info(`${message.author.tag} has created a new panel`);
@ -204,7 +202,7 @@ module.exports = class PanelCommand extends Command {
categories: categories_map,
channel: panel_channel.id,
guild: message.guild.id,
message: panel_message.id,
message: panel_message.id
});
}
};

View File

@ -1,30 +1,32 @@
const Command = require('../modules/commands/command');
// eslint-disable-next-line no-unused-vars
const { MessageEmbed, Message } = require('discord.js');
const {
Message, // eslint-disable-line no-unused-vars
MessageEmbed
} = require('discord.js');
module.exports = class RemoveCommand extends Command {
constructor(client) {
const i18n = client.i18n.getLocale(client.config.locale);
super(client, {
internal: true,
name: i18n('commands.remove.name'),
description: i18n('commands.remove.description'),
aliases: [],
process_args: false,
args: [
{
name: i18n('commands.remove.args.member.name'),
description: i18n('commands.remove.args.member.description'),
example: i18n('commands.remove.args.member.example'),
required: true,
name: i18n('commands.remove.args.member.name'),
required: true
},
{
name: i18n('commands.remove.args.ticket.name'),
description: i18n('commands.remove.args.ticket.description'),
example: i18n('commands.remove.args.ticket.example'),
required: false,
name: i18n('commands.remove.args.ticket.name'),
required: false
}
]
],
description: i18n('commands.remove.description'),
internal: true,
name: i18n('commands.remove.name'),
process_args: false
});
}

View File

@ -1,22 +1,24 @@
const Command = require('../modules/commands/command');
const fetch = require('node-fetch');
// eslint-disable-next-line no-unused-vars
const { MessageAttachment, Message } = require('discord.js');
const {
Message, // eslint-disable-line no-unused-vars
MessageAttachment
} = require('discord.js');
const { Validator } = require('jsonschema');
module.exports = class SettingsCommand extends Command {
constructor(client) {
const i18n = client.i18n.getLocale(client.config.locale);
super(client, {
aliases: [
i18n('commands.settings.aliases.config')
],
args: [],
description: i18n('commands.settings.description'),
internal: true,
name: i18n('commands.settings.name'),
description: i18n('commands.settings.description'),
aliases: [
i18n('commands.settings.aliases.config'),
],
process_args: false,
args: [],
permissions: ['MANAGE_GUILD']
permissions: ['MANAGE_GUILD'],
process_args: false
});
this.schema = require('./extra/settings.schema.json');
@ -40,7 +42,9 @@ module.exports = class SettingsCommand extends Command {
this.client.log.info(`Downloading settings for "${message.guild.name}"`);
const data = await (await fetch(attachments[0].url)).json();
const { valid, errors } = this.v.validate(data, this.schema);
const {
valid, errors
} = this.v.validate(data, this.schema);
if (!valid) {
this.client.log.warn('Settings validation error');
@ -60,11 +64,7 @@ module.exports = class SettingsCommand extends Command {
for (const c of data.categories) {
if (c.id) {
// existing category
const cat_row = await this.client.db.models.Category.findOne({
where: {
id: c.id
}
});
const cat_row = await this.client.db.models.Category.findOne({ where: { id: c.id } });
cat_row.claiming = c.claiming;
cat_row.image = c.image;
cat_row.max_per_member = c.max_per_member;
@ -81,15 +81,14 @@ module.exports = class SettingsCommand extends Command {
const cat_channel = await this.client.channels.fetch(c.id);
if (cat_channel) {
if (cat_channel.name !== c.name)
await cat_channel.setName(c.name, `Tickets category updated by ${message.author.tag}`);
if (cat_channel.name !== c.name) await cat_channel.setName(c.name, `Tickets category updated by ${message.author.tag}`);
for (const r of c.roles) {
await cat_channel.updateOverwrite(r, {
VIEW_CHANNEL: true,
ATTACH_FILES: true,
READ_MESSAGE_HISTORY: true,
SEND_MESSAGES: true,
ATTACH_FILES: true
VIEW_CHANNEL: true
}, `Tickets category updated by ${message.author.tag}`);
}
}
@ -97,33 +96,31 @@ module.exports = class SettingsCommand extends Command {
// create a new category
const allowed_permissions = ['VIEW_CHANNEL', 'READ_MESSAGE_HISTORY', 'SEND_MESSAGES', 'EMBED_LINKS', 'ATTACH_FILES'];
const cat_channel = await message.guild.channels.create(c.name, {
type: 'category',
reason: `Tickets category created by ${message.author.tag}`,
position: 1,
permissionOverwrites: [
...[
{
id: message.guild.roles.everyone,
deny: ['VIEW_CHANNEL']
deny: ['VIEW_CHANNEL'],
id: message.guild.roles.everyone
},
{
id: this.client.user.id,
allow: allowed_permissions
allow: allowed_permissions,
id: this.client.user.id
}
],
...c.roles.map(r => {
return {
id: r,
allow: allowed_permissions
};
})
]
...c.roles.map(r => ({
allow: allowed_permissions,
id: r
}))
],
position: 1,
reason: `Tickets category created by ${message.author.tag}`,
type: 'category'
});
await this.client.db.models.Category.create({
id: cat_channel.id,
claiming: c.claiming,
guild: message.guild.id,
id: cat_channel.id,
image: c.image,
max_per_member: c.max_per_member,
name: c.name,
@ -141,11 +138,11 @@ module.exports = class SettingsCommand extends Command {
for (const survey in data.surveys) {
const survey_data = {
guild: message.guild.id,
name: survey,
name: survey
};
const [s_row] = await this.client.db.models.Survey.findOrCreate({
where: survey_data,
defaults: survey_data
defaults: survey_data,
where: survey_data
});
s_row.questions = data.surveys[survey];
await s_row.save();
@ -156,35 +153,25 @@ module.exports = class SettingsCommand extends Command {
} else {
// upload settings as json to be edited
const categories = await this.client.db.models.Category.findAll({
where: {
guild: message.guild.id
}
});
const surveys = await this.client.db.models.Survey.findAll({
where: {
guild: message.guild.id
}
});
const categories = await this.client.db.models.Category.findAll({ where: { guild: message.guild.id } });
const surveys = await this.client.db.models.Survey.findAll({ where: { guild: message.guild.id } });
const data = {
categories: categories.map(c => {
return {
id: c.id,
claiming: c.claiming,
image: c.image,
max_per_member: c.max_per_member,
name: c.name,
name_format: c.name_format,
opening_message: c.opening_message,
opening_questions: c.opening_questions,
ping: c.ping,
require_topic: c.require_topic,
roles: c.roles,
survey: c.survey
};
}),
categories: categories.map(c => ({
claiming: c.claiming,
id: c.id,
image: c.image,
max_per_member: c.max_per_member,
name: c.name,
name_format: c.name_format,
opening_message: c.opening_message,
opening_questions: c.opening_questions,
ping: c.ping,
require_topic: c.require_topic,
roles: c.roles,
survey: c.survey
})),
colour: settings.colour,
command_prefix: settings.command_prefix,
error_colour: settings.error_colour,
@ -197,7 +184,9 @@ module.exports = class SettingsCommand extends Command {
};
for (const survey in surveys) {
const { name, questions } = surveys[survey];
const {
name, questions
} = surveys[survey];
data.surveys[name] = questions;
}
@ -206,9 +195,7 @@ module.exports = class SettingsCommand extends Command {
`Settings for ${message.guild.name}.json`
);
return await message.channel.send({
files: [attachment]
});
return await message.channel.send({ files: [attachment] });
}
}
};

View File

@ -1,24 +1,24 @@
const Command = require('../modules/commands/command');
const Keyv = require('keyv');
// eslint-disable-next-line no-unused-vars
const { MessageEmbed, Message } = require('discord.js');
const {
Message, // eslint-disable-line no-unused-vars
MessageEmbed
} = require('discord.js');
module.exports = class StatsCommand extends Command {
constructor(client) {
const i18n = client.i18n.getLocale(client.config.locale);
super(client, {
aliases: [],
args: [],
description: i18n('commands.stats.description'),
internal: true,
name: i18n('commands.stats.name'),
description: i18n('commands.stats.description'),
aliases: [],
process_args: false,
args: [],
staff_only: true
});
this.cache = new Keyv({
namespace: 'cache.commands.stats'
});
this.cache = new Keyv({ namespace: 'cache.commands.stats' });
}
/**
@ -35,27 +35,19 @@ module.exports = class StatsCommand extends Command {
let stats = await this.cache.get(message.guild.id);
if (!stats) {
const tickets = await this.client.db.models.Ticket.findAndCountAll({
where: {
guild: message.guild.id
}
});
const tickets = await this.client.db.models.Ticket.findAndCountAll({ where: { guild: message.guild.id } });
stats = { // maths
tickets: tickets.count,
messages: settings.log_messages
? await messages.rows
.reduce(async (acc, row) => (await this.client.db.models.Ticket.findOne({
where: {
id: row.ticket
}
})).guild === message.guild.id
.reduce(async (acc, row) => (await this.client.db.models.Ticket.findOne({ where: { id: row.ticket } }))
.guild === message.guild.id
? await acc + 1
: await acc, 0)
: null,
response_time: Math.floor(tickets.rows.reduce((acc, row) => row.first_response
? acc + ((Math.abs(new Date(row.createdAt) - new Date(row.first_response)) / 1000) / 60)
: acc, 0) / tickets.count)
: acc, 0) / tickets.count),
tickets: tickets.count
};
await this.cache.set(message.guild.id, stats, 60 * 60 * 1000); // cache for an hour
}
@ -67,7 +59,7 @@ module.exports = class StatsCommand extends Command {
.addField(i18n('commands.stats.fields.tickets'), stats.tickets, true)
.addField(i18n('commands.stats.fields.response_time.title'), i18n('commands.stats.fields.response_time.minutes', stats.response_time), true)
.setFooter(settings.footer, message.guild.iconURL());
if (stats.messages) guild_embed.addField(i18n('commands.stats.fields.messages'), stats.messages, true);
await message.channel.send(guild_embed);

View File

@ -1,6 +1,10 @@
const Command = require('../modules/commands/command');
// eslint-disable-next-line no-unused-vars
const { MessageAttachment, MessageEmbed, Message } = require('discord.js');
const {
Message, // eslint-disable-line no-unused-vars
MessageAttachment,
MessageEmbed
} = require('discord.js');
const fsp = require('fs').promises;
const { path } = require('../utils/fs');
const mustache = require('mustache');
@ -9,21 +13,21 @@ 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,
name: i18n('commands.survey.args.survey.name'),
required: false
}
],
description: i18n('commands.survey.description'),
internal: true,
name: i18n('commands.survey.name'),
process_args: false,
staff_only: true
});
}
@ -39,49 +43,41 @@ module.exports = class SurveyCommand extends Command {
const survey = await this.client.db.models.Survey.findOne({
where: {
name: args,
guild: message.guild.id
guild: message.guild.id,
name: args
}
});
if (survey) {
const { rows: responses, count } = await this.client.db.models.SurveyResponse.findAndCountAll({
where: {
survey: survey.id
}
});
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
}
});
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'
});
let template = await fsp.readFile(path('./src/commands/extra/survey.template.html'), { encoding: 'utf8' });
template = template.replace(/[\r\n\t]/g, '');
survey.questions.unshift('Ticket #');
const html = mustache.render(template, {
survey: survey.name.charAt(0).toUpperCase() + survey.name.slice(1),
columns: survey.questions,
count: {
responses: count,
users: users.size
},
columns: survey.questions,
responses
responses,
survey: survey.name.charAt(0).toUpperCase() + survey.name.slice(1)
});
const attachment = new MessageAttachment(
@ -89,15 +85,9 @@ module.exports = class SurveyCommand extends Command {
`${survey.name}.html`
);
return await message.channel.send({
files: [attachment]
});
return await message.channel.send({ files: [attachment] });
} else {
const surveys = await this.client.db.models.Survey.findAll({
where: {
guild: message.guild.id
}
});
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(

View File

@ -1,6 +1,8 @@
const Command = require('../modules/commands/command');
// eslint-disable-next-line no-unused-vars
const { MessageEmbed, Message } = require('discord.js');
const {
Message, // eslint-disable-line no-unused-vars
MessageEmbed
} = require('discord.js');
const { parseArgsStringToArgv: argv } = require('string-argv');
const parseArgs = require('command-line-args');
@ -8,41 +10,37 @@ module.exports = class TagCommand extends Command {
constructor(client) {
const i18n = client.i18n.getLocale(client.config.locale);
super(client, {
internal: true,
name: i18n('commands.tag.name'),
description: i18n('commands.tag.description'),
aliases: [
i18n('commands.tag.aliases.faq'),
i18n('commands.tag.aliases.t'),
i18n('commands.tag.aliases.tags'),
i18n('commands.tag.aliases.tags')
],
process_args: false,
args: [
{
name: i18n('commands.tag.args.tag.name'),
description: i18n('commands.tag.args.command.description'),
example: i18n('commands.tag.args.tag.example'),
required: false,
name: i18n('commands.tag.args.tag.name'),
required: false
}
],
description: i18n('commands.tag.description'),
internal: true,
name: i18n('commands.tag.name'),
process_args: false,
staff_only: true
});
}
/**
* @param {Message} message
* @param {string} args
* @param {Message} message
* @param {string} args
* @returns {Promise<void|any>}
*/
async execute(message, args) {
const settings = await message.guild.getSettings();
const i18n = this.client.i18n.getLocale(settings.locale);
const t_row = await this.client.db.models.Ticket.findOne({
where: {
id: message.channel.id
}
});
const t_row = await this.client.db.models.Ticket.findOne({ where: { id: message.channel.id } });
args = args.split(/\s/g); // convert to an array
const tag_name = args.shift(); // shift the first element
@ -63,14 +61,12 @@ module.exports = class TagCommand extends Command {
);
}
let expected = placeholders
const expected = placeholders
.filter(p => p.startsWith(':'))
.map(p => {
return {
name: p.substr(1, p.length),
type: String,
};
});
.map(p => ({
name: p.substr(1, p.length),
type: String
}));
if (expected.length >= 1) {
try {
@ -107,7 +103,7 @@ module.exports = class TagCommand extends Command {
}
// note that this regex is slightly different to the other
const text = tag.replace(/(?<!\\){{1,2}\s?:?([A-Za-z0-9._]+)\s?(?<!\\)}{1,2}/gi, ($, $1) => this.client.i18n.resolve(args, $1));
const text = tag.replace(/(?<!\\){{1,2}\s?:?([A-Za-z0-9._]+)\s?(?<!\\)}{1,2}/gi, (_$, $1) => this.client.i18n.resolve(args, $1));
return await message.channel.send(
new MessageEmbed()
.setColor(settings.colour)

View File

@ -1,24 +1,26 @@
const Command = require('../modules/commands/command');
// eslint-disable-next-line no-unused-vars
const { MessageEmbed, Message } = require('discord.js');
const {
Message, // eslint-disable-line no-unused-vars
MessageEmbed
} = require('discord.js');
module.exports = class TopicCommand extends Command {
constructor(client) {
const i18n = client.i18n.getLocale(client.config.locale);
super(client, {
internal: true,
name: i18n('commands.topic.name'),
description: i18n('commands.topic.description'),
aliases: [],
process_args: false,
args: [
{
name: i18n('commands.topic.args.new_topic.name'),
description: i18n('commands.topic.args.new_topic.description'),
example: i18n('commands.topic.args.new_topic.example'),
required: true,
name: i18n('commands.topic.args.new_topic.name'),
required: true
}
]
],
description: i18n('commands.topic.description'),
internal: true,
name: i18n('commands.topic.name'),
process_args: false
});
}
@ -31,11 +33,7 @@ module.exports = class TopicCommand extends Command {
const settings = await message.guild.getSettings();
const i18n = this.client.i18n.getLocale(settings.locale);
const t_row = await this.client.db.models.Ticket.findOne({
where: {
id: message.channel.id
}
});
const t_row = await this.client.db.models.Ticket.findOne({ where: { id: message.channel.id } });
if (!t_row) {
return await message.channel.send(
@ -47,18 +45,12 @@ module.exports = class TopicCommand extends Command {
);
}
await t_row.update({
topic: this.client.cryptr.encrypt(args)
});
await t_row.update({ topic: this.client.cryptr.encrypt(args) });
const member = await message.guild.members.fetch(t_row.creator);
/* await */message.channel.setTopic(`${member} | ${args}`, { reason: 'User updated ticket topic' });
const cat_row = await this.client.db.models.Category.findOne({
where: {
id: t_row.category
}
});
const cat_row = await this.client.db.models.Category.findOne({ where: { id: t_row.category } });
const description = cat_row.opening_message
.replace(/{+\s?(user)?name\s?}+/gi, member.displayName)
.replace(/{+\s?(tag|ping|mention)?\s?}+/gi, member.user.toString());

View File

@ -1,42 +1,42 @@
module.exports = {
sqlite: {
name: 'SQLite',
dialect: 'sqlite',
packages: ['sqlite3'],
},
mysql: {
name: 'MySQL',
dialect: 'mysql',
packages: ['mysql2']
},
maria: {
name: 'MariaDB',
dialect: 'mariadb',
name: 'MariaDB',
package: ['mariadb']
},
mariadb: {
name: 'MariaDB',
dialect: 'mariadb',
name: 'MariaDB',
package: ['mariadb']
},
microsoft: {
dialect: 'mssql',
name: 'Microsoft SQL',
packages: ['tedious']
},
mysql: {
dialect: 'mysql',
name: 'MySQL',
packages: ['mysql2']
},
postgre: { // this is wrong
name: 'PostgreSQL',
dialect: 'postgres',
name: 'PostgreSQL',
packages: ['pg', 'pg-hstore']
},
postgres: {
name: 'PostgreSQL',
dialect: 'postgres',
name: 'PostgreSQL',
packages: ['pg', 'pg-hstore']
},
postgresql: {
name: 'PostgreSQL',
dialect: 'postgres',
name: 'PostgreSQL',
packages: ['pg', 'pg-hstore']
},
microsoft: {
name: 'Microsoft SQL',
dialect: 'mssql',
packages: ['tedious']
},
sqlite: {
dialect: 'sqlite',
name: 'SQLite',
packages: ['sqlite3']
}
};

View File

@ -3,7 +3,7 @@ const fs = require('fs');
const { path } = require('../utils/fs');
const types = require('./dialects');
module.exports = async (client) => {
module.exports = async client => {
const {
DB_TYPE,
@ -36,8 +36,8 @@ module.exports = async (client) => {
client.log.info('Using SQLite storage');
sequelize = new Sequelize({
dialect: types[type].dialect,
storage: path('./user/database.sqlite'),
logging: text => client.log.debug(text)
logging: text => client.log.debug(text),
storage: path('./user/database.sqlite')
});
client.log.warn('SQLite is not sufficient for a production environment if you want to use ticket archives. You should disable "log_messages" in your servers\' settings or use a different database.');
} else {
@ -45,8 +45,8 @@ module.exports = async (client) => {
sequelize = new Sequelize(DB_NAME, DB_USER, DB_PASS, {
dialect: types[type].dialect,
host: DB_HOST,
port: DB_PORT,
logging: text => client.log.debug(text)
logging: text => client.log.debug(text),
port: DB_PORT
});
}
@ -66,11 +66,7 @@ module.exports = async (client) => {
require(`./models/${model}`)(client, sequelize);
}
sequelize.sync({
alter: {
drop: false
}
});
sequelize.sync({ alter: { drop: false } });
return sequelize;
};

View File

@ -2,67 +2,65 @@ const { DataTypes } = require('sequelize');
module.exports = ({ config }, sequelize) => {
const { DB_TABLE_PREFIX } = process.env;
sequelize.define('Category', {
id: {
type: DataTypes.CHAR(19),
primaryKey: true,
allowNull: false,
},
claiming: {
type: DataTypes.BOOLEAN,
defaultValue: false,
type: DataTypes.BOOLEAN
},
guild: {
type: DataTypes.CHAR(19),
allowNull: false,
references: {
model: DB_TABLE_PREFIX + 'guilds',
key: 'id'
key: 'id',
model: DB_TABLE_PREFIX + 'guilds'
},
type: DataTypes.CHAR(19),
unique: 'name-guild'
},
id: {
allowNull: false,
primaryKey: true,
type: DataTypes.CHAR(19)
},
image: {
type: DataTypes.STRING,
allowNull: true,
type: DataTypes.STRING
},
max_per_member: {
type: DataTypes.INTEGER,
defaultValue: 1
defaultValue: 1,
type: DataTypes.INTEGER
},
name: {
type: DataTypes.STRING,
allowNull: false,
type: DataTypes.STRING,
unique: 'name-guild'
},
name_format: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: config.defaults.name_format
defaultValue: config.defaults.name_format,
type: DataTypes.STRING
},
opening_message: {
type: DataTypes.STRING,
defaultValue: config.defaults.opening_message,
type: DataTypes.STRING
},
opening_questions: {
type: DataTypes.JSON,
allowNull: true,
type: DataTypes.JSON
},
ping: {
type: DataTypes.JSON,
defaultValue: [],
type: DataTypes.JSON
},
require_topic: {
type: DataTypes.BOOLEAN,
defaultValue: false,
type: DataTypes.BOOLEAN
},
roles: {
type: DataTypes.JSON,
allowNull: false,
type: DataTypes.JSON
},
survey: {
type: DataTypes.STRING,
allowNull: true,
type: DataTypes.STRING
}
}, {
tableName: DB_TABLE_PREFIX + 'categories'
});
}, { tableName: DB_TABLE_PREFIX + 'categories' });
};

View File

@ -1,23 +1,21 @@
const { DataTypes } = require('sequelize');
module.exports = (client, sequelize) => {
module.exports = (_client, sequelize) => {
const { DB_TABLE_PREFIX } = process.env;
sequelize.define('ChannelEntity', {
channel: {
type: DataTypes.CHAR(19),
allowNull: false,
type: DataTypes.CHAR(19),
unique: 'channel-ticket'
},
name: DataTypes.TEXT,
ticket: {
type: DataTypes.CHAR(19),
allowNull: false,
unique: 'channel-ticket',
references: {
model: DB_TABLE_PREFIX + 'tickets',
key: 'id'
key: 'id',
model: DB_TABLE_PREFIX + 'tickets'
},
},
}, {
tableName: DB_TABLE_PREFIX + 'channel_entities'
});
type: DataTypes.CHAR(19),
unique: 'channel-ticket'
}
}, { tableName: DB_TABLE_PREFIX + 'channel_entities' });
};

View File

@ -2,48 +2,47 @@ const { DataTypes } = require('sequelize');
module.exports = ({ config }, sequelize) => {
const { DB_TABLE_PREFIX } = process.env;
sequelize.define('Guild', {
id: {
type: DataTypes.CHAR(19),
primaryKey: true,
allowNull: false,
},
blacklist: {
type: DataTypes.JSON,
defaultValue: [],
type: DataTypes.JSON
},
colour: {
type: DataTypes.STRING,
defaultValue: config.defaults.colour
defaultValue: config.defaults.colour,
type: DataTypes.STRING
},
command_prefix: {
type: DataTypes.STRING,
defaultValue: config.defaults.command_prefix
defaultValue: config.defaults.command_prefix,
type: DataTypes.STRING
},
error_colour: {
type: DataTypes.STRING,
defaultValue: 'RED'
defaultValue: 'RED',
type: DataTypes.STRING
},
footer: {
type: DataTypes.STRING,
defaultValue: 'Discord Tickets by eartharoid'
defaultValue: 'Discord Tickets by eartharoid',
type: DataTypes.STRING
},
id: {
allowNull: false,
primaryKey: true,
type: DataTypes.CHAR(19)
},
locale: {
type: DataTypes.STRING,
defaultValue: config.locale
defaultValue: config.locale,
type: DataTypes.STRING
},
log_messages: {
type: DataTypes.BOOLEAN,
defaultValue: config.defaults.log_messages
defaultValue: config.defaults.log_messages,
type: DataTypes.BOOLEAN
},
success_colour: {
type: DataTypes.STRING,
defaultValue: 'GREEN'
defaultValue: 'GREEN',
type: DataTypes.STRING
},
tags: {
type: DataTypes.JSON,
defaultValue: {}
defaultValue: {},
type: DataTypes.JSON
}
}, {
tableName: DB_TABLE_PREFIX + 'guilds'
});
}, { tableName: DB_TABLE_PREFIX + 'guilds' });
};

View File

@ -1,37 +1,36 @@
const { DataTypes } = require('sequelize');
module.exports = (client, sequelize) => {
module.exports = (_client, sequelize) => {
const { DB_TABLE_PREFIX } = process.env;
sequelize.define('Message', {
id: {
type: DataTypes.CHAR(19),
primaryKey: true,
allowNull: false,
},
author: {
type: DataTypes.CHAR(19),
allowNull: false,
type: DataTypes.CHAR(19)
},
data: {
type: DataTypes.TEXT,
allowNull: false,
type: DataTypes.TEXT
},
deleted: {
type: DataTypes.BOOLEAN,
defaultValue: false,
type: DataTypes.BOOLEAN
},
edited: {
type: DataTypes.BOOLEAN,
defaultValue: false,
type: DataTypes.BOOLEAN
},
id: {
allowNull: false,
primaryKey: true,
type: DataTypes.CHAR(19)
},
ticket: {
type: DataTypes.CHAR(19),
allowNull: false,
references: {
model: DB_TABLE_PREFIX + 'tickets',
key: 'id'
key: 'id',
model: DB_TABLE_PREFIX + 'tickets'
},
},
}, {
tableName: DB_TABLE_PREFIX + 'messages'
});
type: DataTypes.CHAR(19)
}
}, { tableName: DB_TABLE_PREFIX + 'messages' });
};

View File

@ -3,26 +3,24 @@ module.exports = (client, sequelize) => {
const { DB_TABLE_PREFIX } = process.env;
sequelize.define('Panel', {
categories: {
type: DataTypes.JSON,
allowNull: false
allowNull: false,
type: DataTypes.JSON
},
channel: {
type: DataTypes.CHAR(19),
allowNull: false
allowNull: false,
type: DataTypes.CHAR(19)
},
guild: {
type: DataTypes.CHAR(19),
allowNull: false,
references: {
model: DB_TABLE_PREFIX + 'guilds',
key: 'id'
}
key: 'id',
model: DB_TABLE_PREFIX + 'guilds'
},
type: DataTypes.CHAR(19)
},
message: {
type: DataTypes.CHAR(19),
allowNull: false
allowNull: false,
type: DataTypes.CHAR(19)
}
}, {
tableName: DB_TABLE_PREFIX + 'panels'
});
}, { tableName: DB_TABLE_PREFIX + 'panels' });
};

View File

@ -3,25 +3,23 @@ module.exports = (client, sequelize) => {
const { DB_TABLE_PREFIX } = process.env;
sequelize.define('RoleEntity', {
colour: {
type: DataTypes.CHAR(6),
defaultValue: '7289DA',
type: DataTypes.CHAR(6)
},
name: DataTypes.TEXT,
role: {
type: DataTypes.CHAR(19),
allowNull: false,
type: DataTypes.CHAR(19),
unique: 'role-ticket'
},
ticket: {
type: DataTypes.CHAR(19),
allowNull: false,
unique: 'role-ticket',
references: {
model: DB_TABLE_PREFIX + 'tickets',
key: 'id'
key: 'id',
model: DB_TABLE_PREFIX + 'tickets'
},
},
}, {
tableName: DB_TABLE_PREFIX + 'role_entities'
});
type: DataTypes.CHAR(19),
unique: 'role-ticket'
}
}, { tableName: DB_TABLE_PREFIX + 'role_entities' });
};

View File

@ -3,24 +3,22 @@ module.exports = (client, sequelize) => {
const { DB_TABLE_PREFIX } = process.env;
sequelize.define('Survey', {
guild: {
type: DataTypes.CHAR(19),
allowNull: false,
references: {
model: DB_TABLE_PREFIX + 'guilds',
key: 'id'
key: 'id',
model: DB_TABLE_PREFIX + 'guilds'
},
type: DataTypes.CHAR(19),
unique: 'name-guild'
},
name: {
type: DataTypes.STRING,
allowNull: false,
type: DataTypes.STRING,
unique: 'name-guild'
},
questions: {
type: DataTypes.JSON,
allowNull: true,
},
}, {
tableName: DB_TABLE_PREFIX + 'surveys'
});
type: DataTypes.JSON
}
}, { tableName: DB_TABLE_PREFIX + 'surveys' });
};

View File

@ -3,28 +3,26 @@ module.exports = (client, sequelize) => {
const { DB_TABLE_PREFIX } = process.env;
sequelize.define('SurveyResponse', {
answers: {
type: DataTypes.JSON,
allowNull: true,
type: DataTypes.JSON
},
survey: {
type: DataTypes.INTEGER,
allowNull: false,
unique: 'survey-ticket',
references: {
model: DB_TABLE_PREFIX + 'surveys',
key: 'id'
key: 'id',
model: DB_TABLE_PREFIX + 'surveys'
},
type: DataTypes.INTEGER,
unique: 'survey-ticket'
},
ticket: {
type: DataTypes.CHAR(19),
allowNull: false,
unique: 'survey-ticket',
references: {
model: DB_TABLE_PREFIX + 'tickets',
key: 'id'
key: 'id',
model: DB_TABLE_PREFIX + 'tickets'
},
},
}, {
tableName: DB_TABLE_PREFIX + 'survey_responses'
});
type: DataTypes.CHAR(19),
unique: 'survey-ticket'
}
}, { tableName: DB_TABLE_PREFIX + 'survey_responses' });
};

View File

@ -1,75 +1,73 @@
const { DataTypes } = require('sequelize');
module.exports = (client, sequelize) => {
module.exports = (_client, sequelize) => {
const { DB_TABLE_PREFIX } = process.env;
sequelize.define('Ticket', {
id: {
type: DataTypes.CHAR(19),
primaryKey: true,
allowNull: false,
},
category: {
type: DataTypes.CHAR(19),
allowNull: false,
references: {
model: DB_TABLE_PREFIX + 'categories',
key: 'id'
key: 'id',
model: DB_TABLE_PREFIX + 'categories'
},
type: DataTypes.CHAR(19)
},
claimed_by: {
type: DataTypes.CHAR(19),
allowNull: true,
type: DataTypes.CHAR(19)
},
closed_by: {
type: DataTypes.CHAR(19),
allowNull: true,
type: DataTypes.CHAR(19)
},
closed_reason: {
type: DataTypes.STRING,
allowNull: true,
type: DataTypes.STRING
},
creator: {
type: DataTypes.CHAR(19),
allowNull: false,
type: DataTypes.CHAR(19)
},
first_response: {
type: DataTypes.DATE,
allowNull: true,
type: DataTypes.DATE
},
guild: {
type: DataTypes.CHAR(19),
allowNull: false,
references: {
model: DB_TABLE_PREFIX + 'guilds',
key: 'id'
key: 'id',
model: DB_TABLE_PREFIX + 'guilds'
},
type: DataTypes.CHAR(19),
unique: 'number-guild'
},
id: {
allowNull: false,
primaryKey: true,
type: DataTypes.CHAR(19)
},
last_message: {
type: DataTypes.DATE,
allowNull: true,
type: DataTypes.DATE
},
number: {
type: DataTypes.INTEGER,
allowNull: false,
type: DataTypes.INTEGER,
unique: 'number-guild'
},
open: {
type: DataTypes.BOOLEAN,
defaultValue: true
defaultValue: true,
type: DataTypes.BOOLEAN
},
opening_message: {
type: DataTypes.CHAR(19),
allowNull: true,
type: DataTypes.CHAR(19)
},
pinned_messages: {
type: DataTypes.JSON,
defaultValue: []
defaultValue: [],
type: DataTypes.JSON
},
topic: {
type: DataTypes.TEXT,
allowNull: true,
},
}, {
tableName: DB_TABLE_PREFIX + 'tickets'
});
type: DataTypes.TEXT
}
}, { tableName: DB_TABLE_PREFIX + 'tickets' });
};

View File

@ -6,30 +6,29 @@ module.exports = (client, sequelize) => {
bot: DataTypes.BOOLEAN,
discriminator: DataTypes.STRING,
display_name: DataTypes.TEXT,
ticket: {
type: DataTypes.CHAR(19),
role: {
allowNull: false,
unique: 'user-ticket',
references: {
model: DB_TABLE_PREFIX + 'tickets',
key: 'id'
key: 'role',
model: DB_TABLE_PREFIX + 'role_entities'
},
type: DataTypes.CHAR(19)
},
user: {
type: DataTypes.CHAR(19),
ticket: {
allowNull: false,
references: {
key: 'id',
model: DB_TABLE_PREFIX + 'tickets'
},
type: DataTypes.CHAR(19),
unique: 'user-ticket'
},
username: DataTypes.TEXT,
role: {
type: DataTypes.CHAR(19),
user: {
allowNull: false,
references: {
model: DB_TABLE_PREFIX + 'role_entities',
key: 'role'
},
type: DataTypes.CHAR(19),
unique: 'user-ticket'
},
}, {
tableName: DB_TABLE_PREFIX + 'user_entities'
});
username: DataTypes.TEXT
}, { tableName: DB_TABLE_PREFIX + 'user_entities' });
};

View File

@ -1,20 +1,21 @@
/* eslint-disable no-console */
/**
* Discord Tickets
* Copyright (C) 2021 Isaac Saunders
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*
* @name @eartharoid/discord-tickets
* @description An open-source Discord bot for ticket management
* @copyright 2021 Isaac Saunders
@ -24,8 +25,7 @@
process.title = 'Discord Tickets';
const node_version = Number(process.versions.node.split('.')[0]);
if (node_version < 14)
return console.log(`\x07Error: Discord Tickets does not work on Node v${node_version}. Please upgrade to v14 or above.`);
if (node_version < 14) return console.log(`\x07Error: Discord Tickets does not work on Node v${node_version}. Please upgrade to v14 or above.`);
const leeks = require('leeks.js');
const fs = require('fs');
@ -55,9 +55,7 @@ if (!checkFile('./.env', './example.env')) {
.randomBytes(24)
.toString('hex');
let data = fs.readFileSync(file, {
encoding: 'utf-8'
});
let data = fs.readFileSync(file, { encoding: 'utf-8' });
data = data.replace(key, key + value);
fs.writeFileSync(file, data);
@ -69,9 +67,7 @@ if (!checkFile('./.env', './example.env')) {
process.exit();
}
require('dotenv').config({
path: path('./.env')
});
require('dotenv').config({ path: path('./.env') });
require('./banner')();
@ -119,11 +115,9 @@ class Bot extends Client {
'REACTION'
],
presence: selectPresence(),
ws: {
intents: Intents.NON_PRIVILEGED,
}
ws: { intents: Intents.NON_PRIVILEGED }
});
(async () => {
/** The global bot configuration */
this.config = require('../user/config');
@ -144,9 +138,7 @@ class Bot extends Client {
fs.readdirSync(path('./src/locales'))
.filter(file => file.endsWith('.json'))
.forEach(file => {
const data = fs.readFileSync(path(`./src/locales/${file}`), {
encoding: 'utf8'
});
const data = fs.readFileSync(path(`./src/locales/${file}`), { encoding: 'utf8' });
const name = file.slice(0, file.length - 5);
locales[name] = JSON.parse(data);
});
@ -161,9 +153,9 @@ class Bot extends Client {
this.db = await require('./database')(this), // this.db.models.Ticket...
this.setMaxListeners(this.config.max_listeners); // set the max listeners for each event
require('./updater')(this); // check for updates
const listeners = new ListenerLoader(this);
listeners.load(); // load listeners
@ -191,17 +183,13 @@ class Bot extends Client {
*/
if (this.config.super_secret_setting) { // you can disable it if you really want
const tickets = await this.db.models.Ticket.count();
await fetch(`https://stats.discordtickets.app/client?id=${this.user.id}&tickets=${tickets}`, {
method: 'post',
}).catch(e => {
await fetch(`https://stats.discordtickets.app/client?id=${this.user.id}&tickets=${tickets}`, { method: 'post' }).catch(e => {
this.log.warn('Failed to post tickets count to stats server (you can disable sending stats by setting "super_secret_setting" to false)');
this.log.debug(e);
});
this.guilds.cache.forEach(async g => {
const members = (await g.fetch()).approximateMemberCount;
await fetch(`https://stats.discordtickets.app/guild?id=${g.id}&members=${members}`, {
method: 'post',
}).catch(e => {
await fetch(`https://stats.discordtickets.app/guild?id=${g.id}&members=${members}`, { method: 'post' }).catch(e => {
// don't spam a warning for each server
this.log.debug(e);
});

View File

@ -2,9 +2,7 @@ const EventListener = require('../modules/listeners/listener');
module.exports = class DebugEventListener extends EventListener {
constructor(client) {
super(client, {
event: 'debug'
});
super(client, { event: 'debug' });
}
async execute(data) {

View File

@ -2,9 +2,7 @@ const EventListener = require('../modules/listeners/listener');
module.exports = class ErrorEventListener extends EventListener {
constructor(client) {
super(client, {
event: 'error'
});
super(client, { event: 'error' });
}
async execute(error) {

View File

@ -2,9 +2,7 @@ const EventListener = require('../modules/listeners/listener');
module.exports = class GuildCreateEventListener extends EventListener {
constructor(client) {
super(client, {
event: 'guildCreate'
});
super(client, { event: 'guildCreate' });
}
async execute(guild) {

View File

@ -2,9 +2,7 @@ const EventListener = require('../modules/listeners/listener');
module.exports = class GuildDeleteEventListener extends EventListener {
constructor(client) {
super(client, {
event: 'guildDelete'
});
super(client, { event: 'guildDelete' });
}
async execute(guild) {

View File

@ -2,9 +2,7 @@ const EventListener = require('../modules/listeners/listener');
module.exports = class GuildMemberRemoveEventListener extends EventListener {
constructor(client) {
super(client, {
event: 'guildMemberRemove'
});
super(client, { event: 'guildMemberRemove' });
}
async execute(member) {

View File

@ -5,9 +5,7 @@ const { footer } = require('../utils/discord');
module.exports = class MessageEventListener extends EventListener {
constructor(client) {
super(client, {
event: 'message'
});
super(client, { event: 'message' });
}
async execute(message) {
@ -16,11 +14,7 @@ module.exports = class MessageEventListener extends EventListener {
const settings = await message.guild.getSettings();
const i18n = this.client.i18n.getLocale(settings.locale);
const t_row = await this.client.db.models.Ticket.findOne({
where: {
id: message.channel.id
}
});
const t_row = await this.client.db.models.Ticket.findOne({ where: { id: message.channel.id } });
if (t_row) {
if (settings.log_messages && !message.system) this.client.tickets.archives.addMessage(message); // add the message to the archives (if it is in a ticket channel)
@ -33,22 +27,14 @@ module.exports = class MessageEventListener extends EventListener {
} else {
if (message.author.bot) return;
const p_row = await this.client.db.models.Panel.findOne({
where: {
channel: message.channel.id
}
});
const p_row = await this.client.db.models.Panel.findOne({ where: { channel: message.channel.id } });
if (p_row && typeof p_row.categories === 'string') {
// handle reaction-less panel
await message.delete();
const cat_row = await this.client.db.models.Category.findOne({
where: {
id: p_row.categories
}
});
const cat_row = await this.client.db.models.Category.findOne({ where: { id: p_row.categories } });
const tickets = await this.client.db.models.Ticket.findAndCountAll({
where: {

View File

@ -2,9 +2,7 @@ const EventListener = require('../modules/listeners/listener');
module.exports = class MessageDeleteEventListener extends EventListener {
constructor(client) {
super(client, {
event: 'messageDelete'
});
super(client, { event: 'messageDelete' });
}
async execute(message) {

View File

@ -5,9 +5,7 @@ const { footer } = require('../utils/discord');
module.exports = class MessageReactionAddEventListener extends EventListener {
constructor(client) {
super(client, {
event: 'messageReactionAdd'
});
super(client, { event: 'messageReactionAdd' });
}
async execute(r, u) {
@ -49,34 +47,20 @@ module.exports = class MessageReactionAddEventListener extends EventListener {
});
}
const t_row = await this.client.db.models.Ticket.findOne({
where: {
id: channel.id
}
});
const t_row = await this.client.db.models.Ticket.findOne({ where: { id: channel.id } });
if (t_row && t_row.opening_message === r.message.id) {
if (r.emoji.name === '🙌' && await member.isStaff()) {
// ticket claiming
await t_row.update({
claimed_by: member.user.id
});
await t_row.update({ claimed_by: member.user.id });
await channel.updateOverwrite(member.user.id, {
VIEW_CHANNEL: true,
}, `Ticket claimed by ${member.user.tag}`);
await channel.updateOverwrite(member.user.id, { VIEW_CHANNEL: true }, `Ticket claimed by ${member.user.tag}`);
const cat_row = await this.client.db.models.Category.findOne({
where: {
id: t_row.category
}
});
const cat_row = await this.client.db.models.Category.findOne({ where: { id: t_row.category } });
for (const role of cat_row.roles) {
await channel.updateOverwrite(role, {
VIEW_CHANNEL: false,
}, `Ticket claimed by ${member.user.tag}`);
await channel.updateOverwrite(role, { VIEW_CHANNEL: false }, `Ticket claimed by ${member.user.tag}`);
}
this.client.log.info(`${member.user.tag} has claimed "${channel.name}" in "${guild.name}"`);
@ -93,11 +77,7 @@ module.exports = class MessageReactionAddEventListener extends EventListener {
await r.users.remove(u.id);
}
} else {
const p_row = await this.client.db.models.Panel.findOne({
where: {
message: r.message.id
}
});
const p_row = await this.client.db.models.Panel.findOne({ where: { message: r.message.id } });
if (p_row && typeof p_row.categories !== 'string') {
// panels
@ -106,11 +86,7 @@ module.exports = class MessageReactionAddEventListener extends EventListener {
const category_id = p_row.categories[r.emoji.name];
if (!category_id) return;
const cat_row = await this.client.db.models.Category.findOne({
where: {
id: category_id
}
});
const cat_row = await this.client.db.models.Category.findOne({ where: { id: category_id } });
const tickets = await this.client.db.models.Ticket.findAndCountAll({
where: {

View File

@ -4,9 +4,7 @@ const { MessageEmbed } = require('discord.js');
module.exports = class MessageReactionRemoveEventListener extends EventListener {
constructor(client) {
super(client, {
event: 'messageReactionRemove'
});
super(client, { event: 'messageReactionRemove' });
}
async execute(r, u) {
@ -38,34 +36,22 @@ module.exports = class MessageReactionRemoveEventListener extends EventListener
const channel = r.message.channel;
const member = await guild.members.fetch(u.id);
const t_row = await this.client.db.models.Ticket.findOne({
where: {
id: channel.id
}
});
const t_row = await this.client.db.models.Ticket.findOne({ where: { id: channel.id } });
if (t_row && t_row.opening_message === r.message.id) {
if (r.emoji.name === '🙌' && await member.isStaff()) {
// ticket claiming
await t_row.update({
claimed_by: null
});
await t_row.update({ claimed_by: null });
await channel.permissionOverwrites
.get(member.user.id)
?.delete(`Ticket released by ${member.user.tag}`);
const cat_row = await this.client.db.models.Category.findOne({
where: {
id: t_row.category
}
});
const cat_row = await this.client.db.models.Category.findOne({ where: { id: t_row.category } });
for (const role of cat_row.roles) {
await channel.updateOverwrite(role, {
VIEW_CHANNEL: true,
}, `Ticket released by ${member.user.tag}`);
await channel.updateOverwrite(role, { VIEW_CHANNEL: true }, `Ticket released by ${member.user.tag}`);
}
this.client.log.info(`${member.user.tag} has released "${channel.name}" in "${guild.name}"`);

View File

@ -2,9 +2,7 @@ const EventListener = require('../modules/listeners/listener');
module.exports = class MessageUpdateEventListener extends EventListener {
constructor(client) {
super(client, {
event: 'messageUpdate'
});
super(client, { event: 'messageUpdate' });
}
async execute(oldm, newm) {

View File

@ -2,9 +2,7 @@ const EventListener = require('../modules/listeners/listener');
module.exports = class RateLimitEventListener extends EventListener {
constructor(client) {
super(client, {
event: 'rateLimit'
});
super(client, { event: 'rateLimit' });
}
async execute(limit) {

View File

@ -2,9 +2,7 @@ const EventListener = require('../modules/listeners/listener');
module.exports = class WarnEventListener extends EventListener {
constructor(client) {
super(client, {
event: 'warn'
});
super(client, { event: 'warn' });
}
async execute(warning) {

View File

@ -2,60 +2,38 @@ const { path } = require('./utils/fs');
const config = require('../user/config');
const Logger = require('leekslazylogger-fastify');
module.exports = new Logger({
name: 'Discord Tickets by eartharoid',
debug: config.debug,
logToFile: config.logs.enabled,
splitFile: config.logs.split,
directory: path('./logs/'),
keepFor: config.logs.keep_for,
timestamp: 'YYYY-MM-DD HH:mm:ss',
levels: {
_logger: {
format: '&f&!7{timestamp}&r [LOGGER] {text}'
},
basic: {
format: '&f&!7{timestamp} {text}'
},
console: {
format: '&f&!7{timestamp} [INFO] {text}'
},
info: {
format: '&f&!7{timestamp}&r &3[INFO] &b{text}'
},
success: {
format: '&f&!7{timestamp}&r &2[SUCCESS] &a{text}'
},
debug: {
format: '&f&!7{timestamp}&r &1[DEBUG] &9{text}'
},
notice: {
format: '&f&!7{timestamp}&r &0&!6[NOTICE] {text}'
},
warn: {
format: '&f&!7{timestamp}&r &6[WARN] &e{text}'
},
error: {
format: '&f&!7{timestamp}&r &4[ERROR] &c{text}'
},
_logger: { format: '&f&!7{timestamp}&r [LOGGER] {text}' },
basic: { format: '&f&!7{timestamp} {text}' },
commands: {
type: 'info',
format: '&f&!7{timestamp}&r &3[INFO] &d(COMMANDS)&r {text}'
},
plugins: {
type: 'info',
format: '&f&!7{timestamp}&r &3[INFO] &d(PLUGINS)&r {text}'
},
tickets: {
type: 'info',
format: '&f&!7{timestamp}&r &3[INFO] &d(TICKETS)&r {text}'
format: '&f&!7{timestamp}&r &3[INFO] &d(COMMANDS)&r {text}',
type: 'info'
},
console: { format: '&f&!7{timestamp} [INFO] {text}' },
debug: { format: '&f&!7{timestamp}&r &1[DEBUG] &9{text}' },
error: { format: '&f&!7{timestamp}&r &4[ERROR] &c{text}' },
http: {
type: 'info',
format: '&f&!7{timestamp}&r &3[INFO] &d(HTTP)&r {text}'
format: '&f&!7{timestamp}&r &3[INFO] &d(HTTP)&r {text}',
type: 'info'
},
info: { format: '&f&!7{timestamp}&r &3[INFO] &b{text}' },
notice: { format: '&f&!7{timestamp}&r &0&!6[NOTICE] {text}' },
plugins: {
format: '&f&!7{timestamp}&r &3[INFO] &d(PLUGINS)&r {text}',
type: 'info'
},
success: { format: '&f&!7{timestamp}&r &2[SUCCESS] &a{text}' },
warn: { format: '&f&!7{timestamp}&r &6[WARN] &e{text}' },
ws: {
type: 'info',
format: '&f&!7{timestamp}&r &3[INFO] &d(WS)&r {text}'
format: '&f&!7{timestamp}&r &3[INFO] &d(WS)&r {text}',
type: 'info'
}
}
},
logToFile: config.logs.enabled,
name: 'Discord Tickets by eartharoid',
splitFile: config.logs.split,
timestamp: 'YYYY-MM-DD HH:mm:ss'
});

View File

@ -1,12 +1,14 @@
// eslint-disable-next-line no-unused-vars
const { MessageEmbed, Message } = require('discord.js');
const {
Message, // eslint-disable-line no-unused-vars
MessageEmbed
} = require('discord.js');
/**
* A command
*/
module.exports = class Command {
/**
*
*
* @typedef CommandArgument
* @property {string} name - The argument's name
* @property {string} description - The argument's description
@ -37,7 +39,7 @@ module.exports = class Command {
}
/**
* The name of the command
* The name of the command
* @type {string}
*/
this.name = data.name;
@ -53,7 +55,7 @@ module.exports = class Command {
/**
* The command description
* @type {string}
*/
*/
this.description = data.description;
/**
@ -61,12 +63,12 @@ module.exports = class Command {
* @type {boolean}
* @default false
*/
this.staff_only = data.staff_only === true ? true : false;
this.staff_only = data.staff_only === true;
/**
* Array of permissions needed for a user to use this command
* @type {string[]}
*/
*/
this.permissions = data.permissions ?? [];
/**
@ -74,9 +76,9 @@ module.exports = class Command {
* @type {boolean}
* @default false
*/
this.process_args = data.process_args === true ? true : false;
this.process_args = data.process_args === true;
/**
/**
* The command options
* @type {CommandArgument[]}
*/
@ -86,7 +88,7 @@ module.exports = class Command {
* True if command is internal, false if it is from a plugin
* @type {boolean}
*/
this.internal = data.internal === true ? true : false;
this.internal = data.internal === true;
if (!this.internal) {
/**
@ -155,7 +157,7 @@ module.exports = class Command {
this.args.forEach(arg => addArgs(embed, arg));
return await channel.send(embed);
}
};

View File

@ -1,5 +1,10 @@
// eslint-disable-next-line no-unused-vars
const { Collection, Client, Message, MessageEmbed } = require('discord.js');
const {
Client, // eslint-disable-line no-unused-vars
Collection,
Message, // eslint-disable-line no-unused-vars
MessageEmbed
} = require('discord.js');
const fs = require('fs');
const { path } = require('../../utils/fs');
@ -13,7 +18,7 @@ const parseArgs = require('command-line-args');
module.exports = class CommandManager {
/**
* Create a CommandManager instance
* @param {import('../..').Bot} client
* @param {import('../..').Bot} client
*/
constructor(client) {
/** The Discord Client */
@ -49,15 +54,13 @@ module.exports = class CommandManager {
if (is_internal) {
const plugin = this.client.plugins.plugins.find(p => p.commands.includes(cmd.name));
if (plugin)
this.client.log.commands(`The "${plugin.name}" plugin has overridden the internal "${cmd.name}" command`);
else
this.client.log.commands(`An unknown plugin has overridden the internal "${cmd.name}" command`);
if (plugin) this.client.log.commands(`The "${plugin.name}" plugin has overridden the internal "${cmd.name}" command`);
else this.client.log.commands(`An unknown plugin has overridden the internal "${cmd.name}" command`);
if(cmd.internal) return;
}
else if (exists)
} else if (exists) {
throw new Error(`A non-internal command with the name "${cmd.name}" already exists`);
}
this.commands.set(cmd.name, cmd);
this.client.log.commands(`Loaded "${cmd.name}" command`);
}
@ -114,7 +117,7 @@ module.exports = class CommandManager {
'MANAGE_CHANNELS',
'MANAGE_MESSAGES',
'READ_MESSAGE_HISTORY',
'SEND_MESSAGES',
'SEND_MESSAGES'
];
if (!bot_permissions.has(required_bot_permissions)) {
@ -182,10 +185,10 @@ module.exports = class CommandManager {
return await cmd.sendUsage(message.channel, cmd_name);
}
}
try {
this.client.log.commands(`Executing "${cmd.name}" command (invoked by ${message.author.tag})`);
await cmd.execute(message, args); // execute the command
await cmd.execute(message, args); // execute the command
} catch (e) {
this.client.log.warn(`An error occurred whilst executing the ${cmd.name} command`);
this.client.log.error(e);

View File

@ -1,9 +1,9 @@
module.exports = class EventListener {
/**
*
* @param {import("../..").Bot} client
* @param {*} data
*
* @param {import("../..").Bot} client
* @param {*} data
*/
constructor(client, data) {
this.client = client;

View File

@ -7,7 +7,7 @@ const { path } = require('../../utils/fs');
module.exports = class ListenerLoader {
/**
* Create a ListenerLoader instance
* @param {import('../..').Bot} client
* @param {import('../..').Bot} client
*/
constructor(client) {
/** The Discord Client */
@ -23,10 +23,8 @@ module.exports = class ListenerLoader {
file = require(`../../listeners/${file}`);
const listener = new file(this.client);
const on = listener.once ? 'once' : 'on';
if (listener.raw)
this.client.ws[on](listener.event, (...data) => listener.execute(...data));
else
this.client[on](listener.event, (...data) => listener.execute(...data));
if (listener.raw) this.client.ws[on](listener.event, (...data) => listener.execute(...data));
else this.client[on](listener.event, (...data) => listener.execute(...data));
} catch (e) {
this.client.log.warn('An error occurred whilst loading a listener');
this.client.log.error(e);

View File

@ -9,12 +9,12 @@ const Plugin = require('./plugin');
module.exports = class PluginManager {
/**
* Create a PluginManager instance
* @param {import('../..').Bot} client
* @param {import('../..').Bot} client
*/
constructor(client) {
/** The Discord Client */
this.client = client;
/**
* A discord.js Collection (Map) of loaded plugins
* @type {Collection<string, import('./plugin')>}
@ -29,8 +29,7 @@ module.exports = class PluginManager {
}
handleError(id) {
if (!this.official.includes(id))
this.client.log.notice(`"${id}" is NOT an official plugin, please do not ask for help with it in the Discord Tickets support server, seek help from the plugin author instead.`);
if (!this.official.includes(id)) this.client.log.notice(`"${id}" is NOT an official plugin, please do not ask for help with it in the Discord Tickets support server, seek help from the plugin author instead.`);
}
/**
@ -56,17 +55,17 @@ module.exports = class PluginManager {
}
const about = {
id,
version,
author,
description
description,
id,
version
};
try {
plugin = new (plugin(Plugin))(this.client, about);
this.plugins.set(id, plugin);
this.client.log.plugins(`Loading "${plugin.name}" v${version} by ${author}`);
plugin.preload();
plugin.preload();
} catch (e) {
this.handleError(id);
this.client.log.warn(`An error occurred whilst loading the "${id}" plugin`);

View File

@ -40,31 +40,31 @@ module.exports = class Plugin {
*/
this.name = options.name ?? id;
/**
/**
* An array of commands from this plugin
* @type {string[]}
*/
this.commands = options.commands;
/**
/**
* The unique ID of the plugin (NPM package name)
* @type {string}
*/
this.id = id;
/**
/**
* The version of the plugin (NPM package version)
* @type {string}
*/
this.version = version;
/**
/**
* The plugin author's name (NPM package author)
* @type {(undefined|string)}
*/
this.author = author;
/**
/**
* The plugin description (NPM package description)
* @type {string}
*/

View File

@ -5,6 +5,5 @@ module.exports = () => {
const files = fs.readdirSync(path('./src/structures'))
.filter(file => file.endsWith('.js'));
for (const file of files)
require(`../structures/${file}`);
for (const file of files) require(`../structures/${file}`);
};

View File

@ -20,26 +20,22 @@ module.exports = class TicketArchives {
try {
// await this.client.db.transaction(async t => {
const t_row = await this.client.db.models.Ticket.findOne({
where: {
id: message.channel.id
},
where: { id: message.channel.id }
/* transaction: t */
});
if (t_row) {
await this.client.db.models.Message.create({
id: message.id,
ticket: t_row.id,
author: message.author.id,
createdAt: new Date(message.createdTimestamp),
data: this.encrypt(JSON.stringify({
attachments: [...message.attachments.values()],
content: message.content,
embeds: message.embeds.map(embed => {
return { embed };
}),
attachments: [...message.attachments.values()]
embeds: message.embeds.map(embed => ({ embed }))
})),
createdAt: new Date(message.createdTimestamp)
}, /* { transaction: t } */);
id: message.id,
ticket: t_row.id
} /* { transaction: t } */);
await this.updateEntities(message);
}
@ -54,19 +50,15 @@ module.exports = class TicketArchives {
try {
// await this.client.db.transaction(async t => {
const m_row = await this.client.db.models.Message.findOne({
where: {
id: message.id
},
where: { id: message.id }
/* transaction: t */
});
if (m_row) {
m_row.data = this.encrypt(JSON.stringify({
attachments: [...message.attachments.values()],
content: message.content,
embeds: message.embeds.map(embed => {
return { embed };
}),
attachments: [...message.attachments.values()]
embeds: message.embeds.map(embed => ({ embed }))
}));
if (message.editedTimestamp) {
@ -87,9 +79,7 @@ module.exports = class TicketArchives {
try {
// await this.client.db.transaction(async t => {
const msg = await this.client.db.models.Message.findOne({
where: {
id: message.id
},
where: { id: message.id }
/* transaction: t */
});
@ -130,27 +120,27 @@ module.exports = class TicketArchives {
try {
// await this.client.db.transaction(async t => {
const u_model_data = {
user: member.user.id,
ticket: ticket_id
ticket: ticket_id,
user: member.user.id
};
const [u_row] = await this.client.db.models.UserEntity.findOrCreate({
where: u_model_data,
defaults: {
...u_model_data,
role: member.roles.highest.id
},
where: u_model_data
/* transaction: t */
});
await u_row.update({
avatar: member.user.avatar,
username: this.encrypt(member.user.username),
bot: member.user.bot,
discriminator: member.user.discriminator,
display_name: this.encrypt(member.displayName),
role: member.roles.highest.id,
bot: member.user.bot
}, /* { transaction: t } */);
username: this.encrypt(member.user.username)
} /* { transaction: t } */);
return u_row;
// });
@ -168,14 +158,12 @@ module.exports = class TicketArchives {
ticket: ticket_id
};
const [c_row] = await this.client.db.models.ChannelEntity.findOrCreate({
where: c_model_data,
defaults: c_model_data,
where: c_model_data
/* transaction: t */
});
await c_row.update({
name: this.encrypt(channel.name)
}, /* { transaction: t } */);
await c_row.update({ name: this.encrypt(channel.name) } /* { transaction: t } */);
return c_row;
// });
@ -193,15 +181,15 @@ module.exports = class TicketArchives {
ticket: ticket_id
};
const [r_row] = await this.client.db.models.RoleEntity.findOrCreate({
where: r_model_data,
defaults: r_model_data,
where: r_model_data
/* transaction: t */
});
await r_row.update({
name: this.encrypt(role.name),
colour: role.color === 0 ? '7289DA' : int2hex(role.color) // 7289DA = 7506394
}, /* { transaction: t } */);
colour: role.color === 0 ? '7289DA' : int2hex(role.color), // 7289DA = 7506394
name: this.encrypt(role.name)
} /* { transaction: t } */);
return r_row;
// });

View File

@ -1,3 +1,4 @@
/* eslint-disable max-lines */
const EventEmitter = require('events');
const TicketArchives = require('./archives');
const { MessageEmbed } = require('discord.js');
@ -25,30 +26,20 @@ module.exports = class TicketManager extends EventEmitter {
* @param {string} guild_id - ID of the guild to create the ticket in
* @param {string} creator_id - ID of the ticket creator (user)
* @param {string} category_id - ID of the ticket category
* @param {string} [topic] - The ticket topic
* @param {string} [topic] - The ticket topic
*/
async create(guild_id, creator_id, category_id, topic) {
if (!topic) topic = '';
const cat_row = await this.client.db.models.Category.findOne({
where: {
id: category_id
}
});
const cat_row = await this.client.db.models.Category.findOne({ where: { id: category_id } });
if (!cat_row) throw new Error('Ticket category does not exist');
if (!cat_row)
throw new Error('Ticket category does not exist');
const cat_channel = await this.client.channels.fetch(category_id);
if (cat_channel.children.size >= 50)
throw new Error('Ticket category has reached child channel limit (50)');
if (cat_channel.children.size >= 50) throw new Error('Ticket category has reached child channel limit (50)');
const number = (await this.client.db.models.Ticket.count({
where: {
guild: guild_id
}
})) + 1;
const number = (await this.client.db.models.Ticket.count({ where: { guild: guild_id } })) + 1;
const guild = this.client.guilds.cache.get(guild_id);
const creator = await guild.members.fetch(creator_id);
@ -57,25 +48,25 @@ module.exports = class TicketManager extends EventEmitter {
.replace(/{+\s?num(ber)?\s?}+/gi, number);
const t_channel = await guild.channels.create(name, {
type: 'text',
topic: `${creator}${topic.length > 0 ? ` | ${topic}` : ''}`,
parent: category_id,
reason: `${creator.user.tag} requested a new ticket channel`
reason: `${creator.user.tag} requested a new ticket channel`,
topic: `${creator}${topic.length > 0 ? ` | ${topic}` : ''}`,
type: 'text'
});
t_channel.updateOverwrite(creator_id, {
VIEW_CHANNEL: true,
ATTACH_FILES: true,
READ_MESSAGE_HISTORY: true,
SEND_MESSAGES: true,
ATTACH_FILES: true
VIEW_CHANNEL: true
}, `Ticket channel created by ${creator.user.tag}`);
const t_row = await this.client.db.models.Ticket.create({
id: t_channel.id,
number,
guild: guild_id,
category: category_id,
creator: creator_id,
guild: guild_id,
id: t_channel.id,
number,
topic: topic.length === 0 ? null : this.client.cryptr.encrypt(topic)
});
@ -115,9 +106,7 @@ module.exports = class TicketManager extends EventEmitter {
const sent = await t_channel.send(creator.user.toString(), embed);
await sent.pin({ reason: 'Ticket opening message' });
await t_row.update({
opening_message: sent.id
});
await t_row.update({ opening_message: sent.id });
const pinned = t_channel.messages.cache.last();
@ -147,17 +136,13 @@ module.exports = class TicketManager extends EventEmitter {
.setFooter(footer(settings.footer, i18n('collector_expires_in', 120)), guild.iconURL())
);
const collector_filter = (message) => message.author.id === t_row.creator;
const collector_filter = message => message.author.id === t_row.creator;
const collector = t_channel.createMessageCollector(collector_filter, {
time: 120000
});
const collector = t_channel.createMessageCollector(collector_filter, { time: 120000 });
collector.on('collect', async (message) => {
collector.on('collect', async message => {
topic = message.content;
await t_row.update({
topic: this.client.cryptr.encrypt(topic)
});
await t_row.update({ topic: this.client.cryptr.encrypt(topic) });
await t_channel.setTopic(`${creator} | ${topic}`, { reason: 'User updated ticket topic' });
await sent.edit(
new MessageEmbed()
@ -225,9 +210,9 @@ module.exports = class TicketManager extends EventEmitter {
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,
open: false,
pinned_messages: [...pinned.keys()]
});
@ -276,11 +261,7 @@ module.exports = class TicketManager extends EventEmitter {
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
}
});
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({
@ -302,13 +283,9 @@ module.exports = class TicketManager extends EventEmitter {
await r_collector_message.react('✅');
const collector_filter = (reaction, user) => {
return user.id === creator.user.id && reaction.emoji.name === '✅';
};
const collector_filter = (reaction, user) => user.id === creator.user.id && reaction.emoji.name === '✅';
const r_collector = r_collector_message.createReactionCollector(collector_filter, {
time: 60000
});
const r_collector = r_collector_message.createReactionCollector(collector_filter, { time: 60000 });
r_collector.on('collect', async () => {
r_collector.stop();
@ -325,7 +302,11 @@ module.exports = class TicketManager extends EventEmitter {
);
try {
const collected = await channel.awaitMessages(filter, { max: 1, time: 60000, errors: ['time'] });
const collected = await channel.awaitMessages(filter, {
errors: ['time'],
max: 1,
time: 60000
});
answers.push(collected.first().content);
} catch (collected) {
return await close();
@ -351,7 +332,7 @@ module.exports = class TicketManager extends EventEmitter {
});
r_collector.on('end', async (collected) => {
r_collector.on('end', async collected => {
if (collected.size === 0) {
await close();
}
@ -367,7 +348,7 @@ module.exports = class TicketManager extends EventEmitter {
}
/**
*
*
* @param {(string|number)} ticket_id - ID or number of the ticket
* @param {string} [guild_id] - The ID of the ticket's guild (used if a ticket number is provided instead of ID)
*/
@ -375,16 +356,12 @@ module.exports = class TicketManager extends EventEmitter {
let t_row;
if (this.client.channels.resolve(ticket_id)) {
t_row = await this.client.db.models.Ticket.findOne({
where: {
id: ticket_id
}
});
t_row = await this.client.db.models.Ticket.findOne({ where: { id: ticket_id } });
} else {
t_row = await this.client.db.models.Ticket.findOne({
where: {
number: ticket_id,
guild: guild_id
guild: guild_id,
number: ticket_id
}
});
}

View File

@ -1,25 +1,21 @@
const { Structures } = require('discord.js');
Structures.extend('Guild', Guild => {
return class extends Guild {
constructor(client, data) {
super(client, data);
}
Structures.extend('Guild', Guild => class extends Guild {
constructor(client, data) {
super(client, data);
}
async deleteSettings() {
const row = await this.settings;
return await row.destroy();
}
async deleteSettings() {
const row = await this.settings;
return await row.destroy();
}
async getSettings() {
const data = {
id: this.id
};
const [settings] = await this.client.db.models.Guild.findOrCreate({
defaults: data,
where: data
});
return settings;
}
};
async getSettings() {
const data = { id: this.id };
const [settings] = await this.client.db.models.Guild.findOrCreate({
defaults: data,
where: data
});
return settings;
}
});

View File

@ -1,20 +1,14 @@
const { Structures } = require('discord.js');
Structures.extend('GuildMember', GuildMember => {
return class extends GuildMember {
constructor(client, data, guild) {
super(client, data, guild);
}
Structures.extend('GuildMember', GuildMember => class extends GuildMember {
constructor(client, data, guild) {
super(client, data, guild);
}
async isStaff() {
const guild_categories = await this.client.db.models.Category.findAll({
where: {
guild: this.guild.id
}
});
return guild_categories.some(cat => cat.roles.some(r => this.roles.cache.has(r)));
}
async isStaff() {
const guild_categories = await this.client.db.models.Category.findAll({ where: { guild: this.guild.id } });
return guild_categories.some(cat => cat.roles.some(r => this.roles.cache.has(r)));
}
};
});

View File

@ -1,3 +1,4 @@
/* eslint-disable no-console */
const fetch = require('node-fetch');
const boxen = require('boxen');
const link = require('terminal-link');
@ -26,11 +27,11 @@ module.exports = async client => {
console.log(
boxen(format(lines.join('\n')), {
padding: 1,
margin: 1,
align: 'center',
borderColor: 'yellow',
borderStyle: 'round'
borderStyle: 'round',
margin: 1,
padding: 1
})
);
}

View File

@ -4,9 +4,9 @@ let current_presence = -1;
module.exports = {
/**
*
* @param {string} text
* @param {string} [additional]
*
* @param {string} text
* @param {string} [additional]
* @returns {string}
*/
footer: (text, additional) => {
@ -20,16 +20,17 @@ module.exports = {
selectPresence: () => {
const length = config.presence.presences.length;
if (length === 0) return {};
let num;
if (length === 1)
if (length === 1) {
num = 0;
else if (config.presence.randomise)
} else if (config.presence.randomise) {
num = Math.floor(Math.random() * length);
else {
} else {
current_presence = current_presence + 1; // ++ doesn't work on negative numbers
if (current_presence === length)
if (current_presence === length) {
current_presence = 0;
}
num = current_presence;
}
@ -48,5 +49,5 @@ module.exports = {
},
status
};
},
}
};

View File

@ -25,7 +25,7 @@ module.exports = {
W: '🇼',
X: '🇽',
Y: '🇾',
Z: '🇿',
Z: '🇿'
},
numbers: {
0: '0⃣',
@ -38,6 +38,6 @@ module.exports = {
7: '7⃣',
8: '8⃣',
9: '9⃣',
10: '🔟',
10: '🔟'
}
};

View File

@ -6,5 +6,5 @@ module.exports = {
* @param {string} path - A path relative to the root of the project (like "./user/config.js")
* @returns {string} absolute path
*/
path: path => join(__dirname, '../../', path),
path: path => join(__dirname, '../../', path)
};

View File

@ -1,10 +1,10 @@
module.exports = {
int2hex: (int) => int.toString(16).toUpperCase(),
int2hex: int => int.toString(16).toUpperCase(),
some: async (array, func) => {
for (const element of array) {
if (await func(element)) return true;
}
return false;
},
wait: (time) => new Promise(res => setTimeout(res, time)),
wait: time => new Promise(res => setTimeout(res, time))
};