v2 rewrite with discord.js@v12 support. Now with SQLite or MySQL storage.

Code is at least 80% less bad.
This commit is contained in:
Isaac (eartharoid) 2020-08-12 23:02:33 +01:00
parent c77f38f209
commit 8431b2d241
44 changed files with 4072 additions and 802 deletions

37
.eslintrc.js Normal file
View File

@ -0,0 +1,37 @@
module.exports = {
'env': {
'commonjs': true,
'es6': true,
'browser': false,
'node': true
},
'extends': 'eslint:recommended',
'globals': {
'Atomics': 'readonly',
'SharedArrayBuffer': 'readonly'
},
'parserOptions': {
'ecmaVersion': 2018
},
'rules': {
'indent': [
'warn',
'tab'
],
'linebreak-style': [
'off',
'windows'
],
'quotes': [
'warn',
'single'
],
'semi': [
'error',
'always'
],
'no-control-regex': [
'off'
]
}
};

27
.github/CONTRIBUTING.md vendored Normal file
View File

@ -0,0 +1,27 @@
# Contributing
## Submitting issues
### Submitting a bug report
To submit a bug report, please use the "Bug report" template when creating a [new issue](https://github.com/eartharoid/DiscordTickets/issues/new/choose). Describe the bug in as much detail as possible, including how to reproduce the problem, using screenshots or code snippets if possible. Check that someone hasn't already filed an issue before creating another, you can comment on it if you want.
### Submitting a feature request
To submit a new feature request, please use the "Feature request" template when creating a [new issue](https://github.com/eartharoid/DiscordTickets/issues/new/choose).
### Submitting other issues
For issues not related to feature requests or bugs, you can [create a blank issue](https://github.com/eartharoid/DiscordTickets/issues/new). Please give us as much information as possible. If you just want to talk, you can join the [Discord server](https://github.com/eartharoid/DiscordTickets#support).
## Submitting a pull request
To contribute code to this project, create a new [pull request](https://github.com/eartharoid/DiscordTickets/pulls). For anything other than patches (bug fixes, documentation or minor code changes that have no affect on usage), such as a new feature, please create a [new issue](https://github.com/eartharoid/DiscordTickets/issues/new/choose) first, describing what you intend to change and why. Please ensure you update the documentation if needed.
When contributing, you should follow the same code style already used throughout, to ensure code is consistent.
1. Use single quote marks (`'`) when possible
2. Template literals are preferred
3. Commas should always have a space after them
4. Use tabs, not spaces, and always indent
5. Use arrow functions

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
ko_fi: eartharoid

View File

@ -1,6 +1,6 @@
---
name: Bug report
about: Create a report to help us improve
about: Report an issue or bug
title: ''
labels: ''
assignees: ''
@ -11,11 +11,7 @@ assignees: ''
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
Steps to reproduce the behaviour:
**Expected behavior**
A clear and concise description of what you expected to happen.
@ -23,16 +19,5 @@ A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@ -8,7 +8,7 @@ assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
A clear and concise description of what the problem is.
**Describe the solution you'd like**
A clear and concise description of what you want to happen.

22
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,22 @@
#### Information
> Check one
- [ ] This includes major changes (new features, version required)
- [ ] This includes minor changes (doesn't change usage)
- [ ] This includes patches (bug fixes, documentation changes etc)
#### Is this related to an issue?
> Reference any issues here
#### Changes made
> Describe your changes
#### Confirmations
> Check all that apply
- [ ] This uses consistent code style
- [ ] This is tested and works

9
.gitignore vendored
View File

@ -1,6 +1,7 @@
package-lock.json
logs/
*.log
node_modules/
config.json
user/.env
user/storage.db
user/transcripts/*.txt
user/transcripts/*.log
user/transcripts/*.json

View File

@ -1,5 +1,6 @@
[![Run on Repl.it](https://repl.it/badge/github/eartharoid/DiscordTickets)](https://repl.it/github/eartharoid/DiscordTickets)
# DiscordTickets
## ENABLE UPDATER
A simple (ish) Discord™ bot to manage support tickets to allow you and your team to provide better and quicker assistance to your community members.
**Go to the [wiki](https://github.com/Eartharoid/DiscordTickets/wiki) for more information**
@ -13,6 +14,3 @@ A simple (ish) Discord™ bot to manage support tickets to allow you and your
The help menu provides an example for each command, along with usage (parameters info) and alias info.
An example command / template is provided if you want to create your own command (such as `!website` for example)
### Credits
- [ohlookitsderpy / leeks.js](https://github.com/ohlookitsderpy/leeks.js)

View File

@ -1,75 +0,0 @@
const Discord = require('discord.js');
const config = require('../config.json');
const log = require(`leekslazylogger`);
module.exports = {
name: 'add',
description: 'Add a member to a ticket channel',
usage: '<@member>',
aliases: ['adduser'],
example: 'add @exampleUser',
args: true,
cooldown: config.cooldown,
guildOnly: true,
execute(message, args) {
const client = message.client;
// command starts here
message.delete();
if (!message.channel.name.startsWith('ticket-')) {
if (config.useEmbeds) {
const notTicket = new Discord.RichEmbed()
.setColor("#E74C3C")
.setDescription(`:x: **This command can only be used within a ticket channel**`);
return message.channel.send(notTicket);
} else {
return message.channel.send(`:x: **This command can only be used within a ticket channel**`);
}
}
let user = message.guild.member(message.mentions.users.first() || message.guild.members.get(args[0]));
if (!user) {
if (config.useEmbeds) {
const err1 = new Discord.RichEmbed()
.setColor("#E74C3C")
.setDescription(`:x: **Unknown user.** Please mention a valid user.`);
return message.channel.send(err1);
} else {
return message.channel.send(`:x: **Unknown user.** Please mention a valid user.`);
}
}
try {
message.channel.overwritePermissions(user, {
SEND_MESSAGES: true,
READ_MESSAGES: true,
ATTACH_FILES: true
});
if(config.useEmbeds) {
const added = new Discord.RichEmbed()
.setColor(config.colour)
.setDescription(`${user} has been added.`);
message.channel.send(added);
} else {
message.channel.send(`${user} has been added.`);
}
// log
if (config.useEmbeds) {
const embed = new Discord.RichEmbed()
.setAuthor(`${client.user.username} / Ticket Log`, client.user.avatarURL)
.setTitle("User Added")
.setColor(config.colour)
.addField("Username", user, true)
.addField("Added by", message.author, true)
.addField("Channel", message.channel, true)
.setFooter(`DiscordTickets`)
.setTimestamp();
client.channels.get(config.logChannel).send(embed);
} else {
client.channels.get(config.logChannel).send(`User added to a ticket by **${message.author.tag} (${message.author.id})**`);
}
log.info(`${message.author.tag} added a user to a ticket (#${message.channel})`)
} catch (error) {
log.error(error);
}
// command ends here
},
};

View File

@ -1,51 +0,0 @@
const Discord = require('discord.js');
const config = require('../config.json');
const log = require(`leekslazylogger`);
module.exports = {
name: 'close',
description: 'Close a ticket',
usage: '',
aliases: ['none'],
example: '',
args: false,
cooldown: config.cooldown,
guildOnly: true,
execute(message, args) {
const client = message.client;
// command starts here
message.delete();
if(!message.channel.name.startsWith('ticket-')) { // // !message.channel.name.length() == 15 &&
if(config.useEmbeds) {
const notTicket = new Discord.RichEmbed()
.setColor("#E74C3C")
.setDescription(`:x: **This command can only be used within a ticket channel**`);
return message.channel.send(notTicket);
} else {
return message.channel.send(`:x: **This command can only be used within a ticket channel**`);
}
} else {
try {
message.channel.delete();
// log
if (config.useEmbeds) {
const embed = new Discord.RichEmbed()
.setAuthor(`${client.user.username} / Ticket Log`, client.user.avatarURL)
.setTitle("Ticket Closed")
.setColor(config.colour)
.addField("Username", message.author, true)
.addField("Channel", message.channel.name, true)
.setFooter(`DiscordTickets`)
.setTimestamp();
client.channels.get(config.logChannel).send(embed);
} else {
client.channels.get(config.logChannel).send(`Ticket closed by **${message.author.tag} (${message.author.id})**`);
}
log.info(`${message.author.tag} closed a ticket (#${message.channel.name})`);
} catch (error) {
log.error(log.colour.red(error));
}
}
// command ends here
},
};

View File

@ -1,23 +0,0 @@
const Discord = require('discord.js');
const config = require('../config.json');
const log = require(`leekslazylogger`);
module.exports = {
name: 'example-command',
description: 'An example command',
usage: '[args]',
aliases: ['none'],
example: 'example-command',
args: false,
cooldown: config.cooldown,
guildOnly: true,
execute(message, args) {
const client = message.client;
// command starts here
message.delete();
// command ends here
},
};

View File

@ -1,108 +0,0 @@
const Discord = require('discord.js');
const config = require('../config.json');
const log = require(`leekslazylogger`);
module.exports = {
name: 'help',
description: 'Displays help menu',
usage: '[command]',
aliases: ['command', 'commands'],
example: 'help new',
args: false,
cooldown: config.cooldown,
guildOnly: true,
execute(message, args) {
// command starts here
message.delete();
const data = [];
const { commands } = message.client;
if (config.useEmbeds) {
if (!args.length) {
data.push('__**Commands**__');
data.push(commands.map(command => `**${config.prefix}${command.name}** : \`${command.description}\``).join('\n'));
data.push(`\nType \`${config.prefix}help [command]\` for more information about a specific command.`);
const embed = new Discord.RichEmbed()
.setTitle("Commands")
.setColor(config.colour)
.setDescription(`\nType \`${config.prefix}help [command]\` for more information about a specific command.`)
// .addField("...", `...`, true)
// .addField("...", `...`, true)
.setFooter(`DiscordTickets by Eartharoid`);
let cmds = [];
cmds.push(commands.map(command => embed.addField(`${config.prefix}${command.name}`, `\`${command.description}\``)));
message.channel.send(embed)
.then(() => {
if (message.channel.type === 'dm') return;
// message.channel.send(`A list of commands has been sent to you.`);
})
.catch(() => {
// console.error(`Could not send help DM to ${message.author.tag}.\n`, error);
log.warn(`Could not DM help menu to ${message.author.tag}, sending to server channel instead`);
message.channel.send(`:x: **Sorry!** There was an error whilst sending the help menu via DMs.`);
message.channel.send(data, { split: true });
});
} else {
const name = args[0].toLowerCase();
const command = commands.get(name) || commands.find(c => c.aliases && c.aliases.includes(name));
if (!command) {
const notCmd = new Discord.RichEmbed()
.setColor("#E74C3C")
.setDescription(`:x: **Invalid command name** (\`${config.prefix}help\`)`);
return message.channel.send(notCmd);
}
const cmd = new Discord.RichEmbed()
.setColor(config.colour)
.addField(`Command`,`\`${command.name}\``, true)
.setFooter(`DiscordTickets by Eartharoid`);
if (command.aliases) cmd.addField("Aliases", `\`${command.aliases.join(', ')}\``, true);
if (command.description) cmd.addField("Description", `\`${command.description}\``, false);
if (command.usage) cmd.addField("Usage", `\`${config.prefix}${command.name} ${command.usage}\``, false)
if (command.example) cmd.addField("Example", `\`${config.prefix}${command.example}\``, false);
message.channel.send(cmd);
}
} else {
// message.channel.send(`**Prefix =** \`${config.prefix}\`\n**Bot Version =** \`${version}\``)
if (!args.length) {
data.push('__**Commands**__');
data.push(commands.map(command => `**${config.prefix}${command.name}** : \`${command.description}\``).join('\n'));
data.push(`\nType \`${config.prefix}help [command]\` for more information about a specific command.`);
return message.author.send(data, { split: true })
.then(() => {
if (message.channel.type === 'dm') return;
// message.channel.send(`A list of commands has been sent to you.`);
})
.catch(error => {
// console.error(`Could not send help DM to ${message.author.tag}.\n`, error);
log.warn(`Could not DM help menu to ${message.author.tag}, sending to server channel instead`);
message.channel.send(`:x: **Sorry!** There was an error whilst sending the help menu via DMs.`)
message.channel.send(data, { split: true })
});
}
const name = args[0].toLowerCase();
const command = commands.get(name) || commands.find(c => c.aliases && c.aliases.includes(name));
if (!command) return message.reply(':x: **Invalid command**');
data.push(`**Command:** \`${command.name}\``);
if (command.aliases) data.push(`**Aliases:** \`${command.aliases.join(', ')}\``);
if (command.description) data.push(`**Description:** \`${command.description}\``);
if (command.usage) data.push(`**Usage:** \`${config.prefix}${command.name} ${command.usage}\``);
if (command.example) data.push(`**Example:** \`${command.example}\``);
data.push(`**Cooldown:** \`${command.cooldown || 3} second(s)\``);
message.channel.send(data, { split: true });
}
// command ends here
},
};

View File

@ -1,114 +0,0 @@
const Discord = require('discord.js');
const config = require('../config.json');
const log = require(`leekslazylogger`);
// const randomString = require('random-string');
module.exports = {
name: 'new',
description: 'Create a new ticket',
usage: '<brief description>',
aliases: ['ticket'],
example: 'new I found an error',
args: true,
cooldown: config.cooldown,
guildOnly: true,
execute(message, args) {
const client = message.client;
// command starts here
message.delete();
let topic = args.join(" ");
// let num = randomString({
// length: 4,
// numeric: true,
// letters: false,
// special: false,
// });
let id = message.author.id.toString().substr(0, 4) + message.author.discriminator;
let chan = `ticket-${id}`;
if (message.guild.channels.find(channel => channel.name === chan)) {
if (config.useEmbeds) {
const err1 = new Discord.RichEmbed()
.setColor("#E74C3C")
.setDescription(`:x: You already have an open ticket.`);
return message.channel.send(err1);
} else {
message.channel.send(`:x: You already have an open ticket.`);
}
};
message.guild.createChannel(`ticket-${id}`, {
type: 'text'
}).then(async c => {
c.setParent(config.ticketsCat);
// let supportRole = message.guild.roles.find(`id`, config.supportRole)
let supportRole = message.guild.roles.get(config.supportRole)
if (!supportRole) return message.channel.send(":x: No **Support Team** role found.");
c.overwritePermissions(message.guild.defaultRole, {
VIEW_CHANNEL: false,
SEND_MESSAGES: false
});
c.overwritePermissions(message.member, {
VIEW_CHANNEL: true,
SEND_MESSAGES: true
});
c.overwritePermissions(supportRole, {
VIEW_CHANNEL: true,
SEND_MESSAGES: true
});
c.setTopic(`${message.author} | ${topic}`);
if (config.tagHereOnly) {
await c.send(`@here, a user has created a new ticket.\n`);
} else {
await c.send(`<@&${config.supportRole}>, a user has created a new ticket.\n`);
};
if (config.ticketImage) {
await c.send(`__**Here's your ticket channel, ${message.author}**__`, {
files: [`./image.png`]
});
} else {
await c.send(`__**Here's your ticket channel, ${message.author}**__`);
}
const created = new Discord.RichEmbed()
.setColor(config.colour)
.setDescription(`Your ticket (${c}) has been created.\nPlease read the information sent and follow any instructions given.`)
.setTimestamp();
const welcome = new Discord.RichEmbed()
.setColor(config.colour)
.setDescription(`**Ticket topic:** \`${topic}\`\n\n${config.ticketText}`);
if (config.useEmbeds) {
message.channel.send(created);
let w = await c.send(welcome);
await w.pin();
// c.fetchMessage(c.lastMessageID).delete()
} else {
message.channel.send(`Your ticket (${c}) has been created.\nPlease read the information sent and follow any instructions given.`);
let w = await c.send(`**Ticket topic:** \`${topic}\`\n\n${config.ticketText}`);
await w.pin();
// c.fetchMessage(c.lastMessageID).delete()
}
// log
if (config.useEmbeds) {
const embed = new Discord.RichEmbed()
.setAuthor(`${client.user.username} / Ticket Log`, client.user.avatarURL)
.setTitle("New Ticket")
.setColor(config.colour)
.setDescription(`\`${topic}\``)
.addField("Username", message.author, true)
.addField("Channel", c, true)
.setFooter(`DiscordTickets`)
.setTimestamp();
client.channels.get(config.logChannel).send(embed);
} else {
client.channels.get(config.logChannel).send(`New ticket created by **${message.author.tag} (${message.author.id})**`);
}
log.info(`${message.author.tag} created a new ticket (#ticket-${id})`);
});
// command ends here
},
};

View File

@ -1,24 +0,0 @@
const Discord = require('discord.js');
const config = require('../config.json');
module.exports = {
name: 'ping',
description: 'Calculate latency',
usage: '',
aliases: ['none'],
example: '',
args: false,
cooldown: config.cooldown,
guildOnly: true,
execute(message, args) {
// command starts here
message.delete();
const embed = new Discord.RichEmbed()
.setTitle("Pong!")
.setColor(config.colour)
.setTimestamp()
.addField("API Latency", `${Math.round(message.client.ping)}ms`, true);
message.channel.send(embed);
// command ends here
},
};

View File

@ -1,75 +0,0 @@
const Discord = require('discord.js');
const config = require('../config.json');
const log = require(`leekslazylogger`);
module.exports = {
name: 'remove',
description: 'Remove a member from a ticket',
usage: '<@member>',
aliases: ['kick'],
example: 'remove @exampleUser',
args: true,
cooldown: config.cooldown,
guildOnly: true,
execute(message, args) {
const client = message.client;
// command starts here
message.delete();
if (!message.channel.name.startsWith('ticket-')) {
if (config.useEmbeds) {
const notTicket = new Discord.RichEmbed()
.setColor("#E74C3C")
.setDescription(`:x: **This command can only be used within a ticket channel**`)
return message.channel.send(notTicket);
} else {
return message.channel.send(`:x: **This command can only be used within a ticket channel**`)
}
}
let user = message.guild.member(message.mentions.users.first() || message.guild.members.get(args[0]));
if (!user) {
if (config.useEmbeds) {
const err1 = new Discord.RichEmbed()
.setColor("#E74C3C")
.setDescription(`:x: **Unknown user.** Please mention a valid user.`)
return message.channel.send(err1);
} else {
return message.channel.send(`:x: **Unknown user.** Please mention a valid user.`);
}
}
try {
message.channel.overwritePermissions(user, {
SEND_MESSAGES: false,
READ_MESSAGES: false
});
if (config.useEmbeds) {
const removed = new Discord.RichEmbed()
.setColor(config.colour)
.setDescription(`${user} has been removed.`)
message.channel.send(removed);
} else {
message.channel.send(`${user} has been removed.`);
}
// log
if (config.useEmbeds) {
const embed = new Discord.RichEmbed()
.setAuthor(`${client.user.username} / Ticket Log`, client.user.avatarURL)
.setTitle("User Removed")
.setColor(config.colour)
.addField("Username", user, true)
.addField("Removed by", message.author, true)
.addField("Channel", message.channel, true)
.setFooter(`DiscordTickets`)
.setTimestamp();
client.channels.get(config.logChannel).send(embed);
} else {
client.channels.get(config.logChannel).send(`User removed to a ticket by **${message.author.tag} (${message.author.id})**`);
}
log.info(`${message.author.tag} removed a user to a ticket (#${message.channel})`)
} catch(error) {
log.error(error);
}
// command ends here
},
};

View File

@ -1,20 +0,0 @@
{
"token": "token",
"prefix": "!",
"name": "DiscordTickets",
"guildID": "id",
"supportRole": "id",
"ticketsCat": "id",
"logChannel": "id",
"colour": "#009999",
"playing": "with tickets (!help)",
"activityType": "PLAYING",
"status": "ONLINE",
"useEmbeds": true,
"logDMs": true,
"cooldown": 3,
"ticketImage": true,
"tagHereOnly": false,
"ticketText": "Thank you for reaching out to our support team.\nA member of staff will come to assist you shortly. Please describe the issue in detail and explain what you have done to resolve the issue so far.\n\n*If you feel that the support you receive is inadequate, please feel free to submit a formal complaint to a senior member of staff.*",
"debugLevel": 0
}

BIN
image.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

261
index.js
View File

@ -1,261 +0,0 @@
/**
###############################################################################################
____ _ _____ _ _
| _ \ (_) ___ ___ ___ _ __ __| | |_ _| (_) ___ | | __ ___ | |_ ___
| | | | | | / __| / __| / _ \ | '__| / _` | | | | | / __| | |/ / / _ \ | __| / __|
| |_| | | | \__ \ | (__ | (_) | | | | (_| | | | | | | (__ | < | __/ | |_ \__ \
|____/ |_| |___/ \___| \___/ |_| \__,_| |_| |_| \___| |_|\_\ \___| \__| |___/
===============================================================================================
---------------------
Discord Tickets
---------------------
A bot created by Eartharoid for the Discord (TM) platform. [GNU-GPLv3.0]
> The bot manages user-created support tickets to allow your support team to provide quicker
and better assistance to your community members.
---------------------
Quick Start
---------------------
> For detailed instructions, visit the Github repository and read the documentation.
> Assuming you have created a Discord application, edit 'config.json' to allow the bot to
function correctly.
> You will need your bot token (keep it a secret) and channel & role IDs from your server
> It is recommended that you do not change much / anything in any of the .js files unless
you know what you are doing, to prevent critical errors from occuring.
===============================================================================================
> For support, visit https://github.com/eartharoid/DiscordTickets/#readme
> My website: https://eartharoid.me
@name DiscordTickets
@author Eartharoid <contact@eartharoid.me>
@license GNU-GPLv3
###############################################################################################
*/
const fs = require('fs');
const Discord = require('discord.js');
const leeks = require('leeks.js');
const log = require(`leekslazylogger`);
const config = require('./config.json');
const { version, homepage } = require('./package.json');
const client = new Discord.Client();
client.commands = new Discord.Collection();
const cooldowns = new Discord.Collection();
const now = Date.now();
const commands = fs.readdirSync('./commands').filter(file => file.endsWith('.js'));
console.log(log.colour.magentaBright(`
######## #### ###### ###### ####### ######## ########
## ## ## ## ## ## ## ## ## ## ## ## ##
## ## ## ## ## ## ## ## ## ## ##
## ## ## ###### ## ## ## ######## ## ##
## ## ## ## ## ## ## ## ## ## ##
## ## ## ## ## ## ## ## ## ## ## ## ##
######## #### ###### ###### ####### ## ## ########
######## #### ###### ## ## ######## ######## ######
## ## ## ## ## ## ## ## ## ##
## ## ## ## ## ## ## ##
## ## ## ##### ###### ## ######
## ## ## ## ## ## ## ##
## ## ## ## ## ## ## ## ## ##
## #### ###### ## ## ######## ## ######
`)); // banner appears in console
console.log(log.colour.yellow(leeks.styles.bold(`DiscordTickets v${version} - Made By Eartharoid`)));
console.log(log.colour.yellow(leeks.styles.bold(homepage)));
console.log('\n\n');
console.log(log.colour.bgGrey(log.colour.grey(`\n\n==========================================================================\n\n`)))
console.log('\n\n');
log.init('DiscordTickets (bot created by Eartharoid)');
// all log.* functions are logged to ./log/file.log from here onwards
log.info(`Starting up...`);
client.once('ready', () => { // after bot has logged in
log.info(`Initialising bot...`);
for (const file of commands) {
const command = require(`./commands/${file}`);
client.commands.set(command.name, command);
log.console(`> Loading '${config.prefix}${command.name}' command`);
}
log.success(`Connected to Discord API`)
log.success(`Logged in as ${client.user.tag}`)
client.user.setPresence({game: {name: config.playing, type: config.activityType},status: config.status})
// .then(log.basic)
.catch(log.error);
if (config.useEmbeds) {
const embed = new Discord.RichEmbed()
.setAuthor(`${client.user.username} / Ticket Log`, client.user.avatarURL)
.setColor("#2ECC71")
.setDescription(":white_check_mark: **Started succesfully**")
.setFooter(`DiscordTickets by Eartharoid`);
client.channels.get(config.logChannel).send(embed)
} else {
client.channels.get(config.logChannel).send(":white_check_mark: **Started succesfully**");
}
if (client.guilds.get(config.guildID).member(client.user).hasPermission("ADMINISTRATOR", false)) {
log.info(`Checking permissions...`);
setTimeout(() => log.success(`Required permissions have been granted\n\n`), 1250);
if (config.useEmbeds) {
const embed = new Discord.RichEmbed()
.setAuthor(`${client.user.username} / Ticket Log`, client.user.avatarURL)
.setColor("#2ECC71")
.setDescription(":white_check_mark: **Required permissions have been granted**")
.setFooter(`DiscordTickets by Eartharoid`);
client.channels.get(config.logChannel).send(embed);
} else {
client.channels.get(config.logChannel).send(":white_check_mark: **Started succesfully**")
}
} else {
log.error(`Required permissions have not been granted`);
log.error(`Please give the bot the 'ADMINISTRATOR' permission\n\n`);
if (config.useEmbeds) {
const embed = new Discord.RichEmbed()
.setAuthor(`${client.user.username} / Ticket Log`, client.user.avatarURL)
.setColor("#E74C3C")
.setDescription(":x: **Required permissions have not been granted**\nPlease give the bot the `ADMINISTRATOR` permission")
.setFooter(`DiscordTickets by Eartharoid`);
client.channels.get(config.logChannel).send(embed);
} else {
client.channels.get(config.logChannel).send(":white_check_mark: **Started succesfully**");
}
}
});
client.on('message', async (message) => {
// if (!message.content.startsWith(config.prefix) || message.author.bot) return;
if (message.author.bot) return;
if (message.channel.type === "dm") {
if (message.author.id === client.user.id) return;
// message.channel.send(`Sorry, commands can only be used on the server.`)
if (config.logDMs) {
if (config.useEmbeds) {
const embed = new Discord.RichEmbed()
.setAuthor(`${client.user.username} / Ticket Log`, client.user.avatarURL)
.setTitle("DM Logger")
.addField("Username", message.author.tag, true)
.addField("Message", message.content, true)
.setFooter(`DiscordTickets by Eartharoid`);
client.channels.get(config.logChannel).send(embed)
} else {
client.channels.get(config.logChannel).send(`DM received from **${message.author.tag} (${message.author.id})** : \n\n\`\`\`${message.content}\`\`\``);
}
} else return;
}
// const args = message.content.slice(config.prefix.length).split(/ +/);
const prefixRegex = new RegExp(`^(<@!?${client.user.id}>|\\${config.prefix})\\s*`);
if (!prefixRegex.test(message.content)) return;
const [, matchedPrefix] = message.content.match(prefixRegex);
const args = message.content.slice(matchedPrefix.length).trim().split(/ +/);
const commandName = args.shift().toLowerCase();
// if (!client.commands.has(commandName)) return;
// const command = client.commands.get(commandName);
const command = client.commands.get(commandName) || client.commands.find(cmd => cmd.aliases && cmd.aliases.includes(commandName));
if (!command) return;
if (command.guildOnly && message.channel.type !== 'text') return message.channel.send(`Sorry, this command can only be used on the server.`);
if (command.args && !args.length) {
// let reply = `:x: **Arguments were expected but none were provided.**`;
//
// if (command.usage) {
// reply += `\n**Usage:** \`${config.prefix}${command.name} ${command.usage}\``;
// }
//
// return message.channel.send(reply);
if (config.useEmbeds) {
const embed = new Discord.RichEmbed()
.setColor("#E74C3C")
.setDescription(`\n**Usage:** \`${config.prefix}${command.name} ${command.usage}\`\nType \`${config.prefix}help ${command.name}\` for more information`);
return message.channel.send(embed);
} else {
return message.channel.send(`**Usage:** \`${config.prefix}${command.name} ${command.usage}\`\nType \`${config.prefix}help ${command.name}\` for more information`);
}
};
if (!cooldowns.has(command.name)) cooldowns.set(command.name, new Discord.Collection());
const timestamps = cooldowns.get(command.name);
const cooldownAmount = (command.cooldown || 3) * 1000;
if (timestamps.has(message.author.id)) {
const expirationTime = timestamps.get(message.author.id) + cooldownAmount;
if (now < expirationTime) {
const timeLeft = (expirationTime - now) / 1000;
if (config.useEmbeds) {
const embed = new Discord.RichEmbed()
.setColor("#E74C3C")
.setDescription(`:x: **Please do not spam commands** (wait ${timeLeft.toFixed(1)}s)`)
return message.channel.send(embed);
} else {
return message.reply(`please do not spam commands (wait ${timeLeft.toFixed(1)}s)`);
}
}
}
timestamps.set(message.author.id, now);
setTimeout(() => timestamps.delete(message.author.id), cooldownAmount);
try {
// client.commands.get(command).execute(message, args, config);
command.execute(message, args);
if (config.useEmbeds) {
const embed = new Discord.RichEmbed()
.setAuthor(`${client.user.username} / Command Log`, client.user.avatarURL)
.setTitle("Command Used")
.addField("Username", message.author, true)
.addField("Command", command.name, true)
.setFooter(`DiscordTickets`)
.setTimestamp();
client.channels.get(config.logChannel).send(embed);
} else {
client.channels.get(config.logChannel).send(`**${message.author.tag} (${message.author.id})** used the \`${command.name}\` command`);
}
log.console(`${message.author.tag} used the '${command.name}' command`)
} catch (error) {
log.error(error);
message.channel.send(`:x: **Oof!** An error occured whilst executing that command.\nThe issue has been reported.`);
log.error(`An unknown error occured whilst executing the '${command.name}' command`);
}
});
client.on('error', (error) => {
log.warn(`Potential error detected\n(likely Discord API connection issue)\n`);
log.error(`Client error:\n${error}`);
});
client.on('warn', (e) => log.warn(`${e}`));
if (config.debugLevel == 1) client.on('debug', (e) => log.debug(`${e}`));
process.on('unhandledRejection', (error) => {
log.warn(`An error was not caught`);
log.error(`Uncaught error: \n${error.stack}`);
});
process.on('beforeExit', (code) => {
log.basic(log.colour.yellowBright(`Disconected from Discord API`));
log.basic(`Exiting (${code})`);
});
client.login(config.token);

2700
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,28 +1,40 @@
{
"name": "discordtickets",
"version": "1.1.5",
"description": "A Discord ticket/support - an open-source & self-hosted bot",
"main": "index.js",
"dependencies": {
"discord.js": "11.x",
"leekslazylogger": "^1.1.9"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/eartharoid/DiscordTickets.git"
},
"keywords": [
"discord",
"bot",
"tickets"
],
"author": "Eartharoid",
"license": "GPL-3.0",
"bugs": {
"url": "https://github.com/eartharoid/DiscordTickets/issues"
},
"homepage": "https://github.com/eartharoid/DiscordTickets"
"name": "@eartharoid/discordtickets",
"version": "2.0.0",
"description": "A Discord ticket/support - an open-source & self-hosted bot",
"main": "src/index.js",
"dependencies": {
"discord.js": "^12.2.0",
"dotenv": "^8.2.0",
"leekslazylogger": "^2.0.0-alpha.5",
"node-fetch": "^2.6.0",
"sequelize": "^6.3.4",
"sqlite3": "^5.0.0"
},
"peerDependencies": {
"mysql2": "2.x"
},
"devDependencies": {
"eslint": "^7.6.0",
"nodemon": "^2.0.4"
},
"scripts": {
"start": "node src/",
"test": "echo \"Nothing to test! Run with 'npm start'\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/eartharoid/DiscordTickets.git"
},
"keywords": [
"discord",
"bot",
"tickets"
],
"author": "eartharoid",
"license": "GPL-3.0",
"bugs": {
"url": "https://github.com/eartharoid/DiscordTickets/issues"
},
"homepage": "https://github.com/eartharoid/DiscordTickets"
}

117
src/commands/add.js Normal file
View File

@ -0,0 +1,117 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const Discord = require('discord.js');
const config = require('../../user/config.js');
const ChildLogger = require('leekslazylogger').ChildLogger;
const log = new ChildLogger();
module.exports = {
name: 'add',
description: 'Add a member to a ticket channel',
usage: '<@member> [... #channel]',
aliases: ['+'],
example: 'add @member to #ticket-23',
args: true,
async execute(client, message, args, Ticket) {
const notTicket = new Discord.MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle(':x: **This isn\'t a ticket channel**')
.setDescription('Use this command in the ticket channel you want to add a user to, or mention the channel.')
.addField('Usage', `\`${config.prefix}${this.name} ${this.usage}\`\n`)
.addField('Help', `Type \`${config.prefix}help ${this.name}\` for more information`)
.setFooter(message.guild.name, message.guild.iconURL());
let ticket;
let channel = message.mentions.channels.first();
if(!channel) {
channel = message.channel;
ticket = await Ticket.findOne({ where: { channel: message.channel.id } });
if(!ticket)
return message.channel.send(notTicket);
} else {
ticket = await Ticket.findOne({ where: { channel: channel.id } });
if(!ticket) {
notTicket
.setTitle(':x: **Channel is not a ticket**')
.setDescription(`${channel} is not a ticket channel.`);
return message.channel.send(notTicket);
}
}
if(message.author.id !== ticket.get('creator') && !message.member.roles.cache.has(config.staff_role))
return message.channel.send(
new Discord.MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle(':x: **No permission**')
.setDescription(`You don't have permission to alter ${channel} as it does not belong to you and you are not staff.`)
.addField('Usage', `\`${config.prefix}${this.name} ${this.usage}\`\n`)
.addField('Help', `Type \`${config.prefix}help ${this.name}\` for more information`)
.setFooter(message.guild.name, message.guild.iconURL())
);
let member = message.guild.member(message.mentions.users.first() || message.guild.members.cache.get(args[0]));
if(!member)
return message.channel.send(
new Discord.MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle(':x: **Unknown member**')
.setDescription('Please mention a valid member.')
.addField('Usage', `\`${config.prefix}${this.name} ${this.usage}\`\n`)
.addField('Help', `Type \`${config.prefix}help ${this.name}\` for more information`)
.setFooter(message.guild.name, message.guild.iconURL())
);
try {
channel.updateOverwrite(member.user, {
VIEW_CHANNEL: true,
SEND_MESSAGES: true,
ATTACH_FILES: true,
READ_MESSAGE_HISTORY: true
});
if(channel.id !== message.channel.id)
channel.send(
new Discord.MessageEmbed()
.setColor(config.colour)
.setAuthor(member.user.username, member.user.displayAvatarURL())
.setTitle('**Member added**')
.setDescription(`${member} has been added by ${message.author}`)
.setFooter(message.guild.name, message.guild.iconURL())
);
message.channel.send(
new Discord.MessageEmbed()
.setColor(config.colour)
.setAuthor(member.user.username, member.user.displayAvatarURL())
.setTitle(':white_check_mark: **Member added**')
.setDescription(`${member} has been added to <#${ticket.get('channel')}>`)
.setFooter(message.guild.name, message.guild.iconURL())
);
log.info(`${message.author.tag} added a user to a ticket (#${message.channel.id})`);
} catch (error) {
log.error(error);
}
// command ends here
},
};

106
src/commands/close.js Normal file
View File

@ -0,0 +1,106 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const ChildLogger = require('leekslazylogger').ChildLogger;
const log = new ChildLogger();
const Discord = require('discord.js');
const config = require('../../user/config');
module.exports = {
name: 'close',
description: 'Close a ticket; either a specified (mentioned) channel, or the channel the command is used in.',
usage: '[ticket]',
aliases: ['none'],
example: 'close #ticket-17',
args: false,
async execute(client, message, args, Ticket) {
const notTicket = new Discord.MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle(':x: **This isn\'t a ticket channel**')
.setDescription('Use this command in the ticket channel you want to close, or mention the channel.')
.addField('Usage', `\`${config.prefix}${this.name} ${this.usage}\`\n`)
.addField('Help', `Type \`${config.prefix}help ${this.name}\` for more information`)
.setFooter(message.guild.name, message.guild.iconURL());
let ticket;
const channel = message.mentions.channels.first();
// let channel = message.guild.channels.resolve(message.mentions.channels.first()); // not necessary
if(!channel) {
ticket = await Ticket.findOne({ where: { channel: message.channel.id } });
if(!ticket)
return message.channel.send(notTicket);
ticket.update({ open: false}, { where: { channel: message.channel.id } });
message.channel.send(
new Discord.MessageEmbed()
.setColor(config.colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle(`:white_check_mark: **Ticket ${ticket.id} closed**`)
.setDescription('The channel will be automatically deleted once the contents have been archived.')
.setFooter(message.guild.name, message.guild.iconURL())
);
setTimeout(() => message.channel.delete(), 5000);
} else {
ticket = await Ticket.findOne({ where: { channel: channel.id } });
if(!ticket) {
notTicket
.setTitle(':x: **Channel is not a ticket**')
.setDescription(`${channel} is not a ticket channel.`);
return message.channel.send(notTicket);
}
if(message.author.id !== ticket.get('creator') && !message.member.roles.cache.has(config.staff_role))
return message.channel.send(
new Discord.MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle(':x: **No permission**')
.setDescription(`You don't have permission to close ${channel} as it does not belong to you and you are not staff.`)
.addField('Usage', `\`${config.prefix}${this.name} ${this.usage}\`\n`)
.addField('Help', `Type \`${config.prefix}help ${this.name}\` for more information`)
.setFooter(message.guild.name, message.guild.iconURL())
);
ticket.update({ open: false}, { where: { channel: channel.id } });
message.channel.send(
new Discord.MessageEmbed()
.setColor(config.colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle(`:white_check_mark: **Ticket ${ticket.id} closed**`)
.setDescription('The channel will be automatically deleted once the contents have been archived.')
.setFooter(message.guild.name, message.guild.iconURL())
);
setTimeout(() => channel.delete(), 5000);
}
log.info(`${message.author.tag} closed a ticket (#ticket-${ticket.get('id')})`);
if (config.logs.discord.enabled)
client.channels.cache.get(config.logs.discord.channel).send(
new Discord.MessageEmbed()
.setColor(config.colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('Ticket closed')
.addField('Creator', `<@${ticket.get('creator')}>` , true)
.addField('Closed by', message.author, true)
.setFooter(client.user.username, client.user.avatarURL())
.setTimestamp()
);
},
};

98
src/commands/help.js Normal file
View File

@ -0,0 +1,98 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const ChildLogger = require('leekslazylogger').ChildLogger;
const log = new ChildLogger();
const Discord = require('discord.js');
const config = require('../../user/config');
module.exports = {
name: 'help',
description: 'Display help menu',
usage: '[command]',
aliases: ['command', 'commands'],
example: 'help new',
args: false,
execute(client, message, args) {
const commands = Array.from(client.commands.values());
if (!args.length) {
let cmds = [];
for (let command of commands) {
if (command.hide)
continue;
if (command.permission && !message.member.hasPermission(command.permission))
continue;
let desc = command.description;
if (desc.length > 50)
desc = desc.substring(0, 50) + '...';
cmds.push(`**${config.prefix}${command.name}** **·** ${desc}`);
}
message.channel.send(
new Discord.MessageEmbed()
.setTitle('Commands')
.setColor(config.colour)
.setDescription(
`\nThe commands you have access to are listed below. Type \`${config.prefix}help [command]\` for more information about a specific command.
\n${cmds.join('\n\n')}
\nPlease contact a member of staff if you require assistance.`
)
.setFooter(message.guild.name, message.guild.iconURL())
.setTimestamp()
).catch((error) => {
log.warn('Could not send help menu');
log.error(error);
});
} else {
const name = args[0].toLowerCase();
const command = client.commands.get(name) || client.commands.find(c => c.aliases && c.aliases.includes(name));
if (!command)
return message.channel.send(
new Discord.MessageEmbed()
.setColor(config.err_colour)
.setDescription(`:x: **Invalid command name** (\`${config.prefix}help\`)`)
);
const cmd = new Discord.MessageEmbed()
.setColor(config.colour)
.setTitle(command.name);
if (command.long) {
cmd.setDescription(command.long);
} else {
cmd.setDescription(command.description);
}
if (command.aliases) cmd.addField('Aliases', `\`${command.aliases.join(', ')}\``, true);
if (command.usage) cmd.addField('Usage', `\`${config.prefix}${command.name} ${command.usage}\``, false);
if (command.usage) cmd.addField('Example', `\`${config.prefix}${command.example}\``, false);
if (command.permission && !message.member.hasPermission(command.permission)) {
cmd.addField('Required Permission', `\`${command.permission}\` :exclamation: You don't have permission to use this command`, true);
} else {
cmd.addField('Required Permission', `\`${command.permission || 'none'}\``, true);
}
message.channel.send(cmd);
}
// command ends here
},
};

195
src/commands/new.js Normal file
View File

@ -0,0 +1,195 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const ChildLogger = require('leekslazylogger').ChildLogger;
const log = new ChildLogger();
const Discord = require('discord.js');
const fs = require('fs');
const config = require('../../user/config');
module.exports = {
name: 'new',
description: 'Create a new support ticket',
usage: '[brief description]',
aliases: ['ticket', 'open'],
example: 'new my server won\'t start',
args: false,
async execute(client, message, args, Ticket) {
const supportRole = message.guild.roles.cache.get(config.staff_role);
if (!supportRole)
return message.channel.send(
new Discord.MessageEmbed()
.setColor(config.err_colour)
.setTitle(':x: **Error**')
.setDescription(`${config.name} has not been set up correctly. Could not find a 'support team' role with the id \`${config.staff_role}\``)
.setFooter(message.guild.name, message.guild.iconURL())
);
let tickets = await Ticket.findAndCountAll({
where: {
creator: message.author.id,
open: true
},
limit: config.tickets.max
});
if (tickets.count >= config.tickets.max) {
let ticketList = [];
for (let t in tickets.rows) {
let desc = tickets.rows[t].topic.substring(0, 20);
ticketList
.push(`<#${tickets.rows[t].channel}>: \`${desc}${desc.length > 20 ? '...' : ''}\``);
}
let m = await message.channel.send(
new Discord.MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle(`:x: **You already have ${tickets.count} or more open tickets**`)
.setDescription(`Use \`${config.prefix}close\` to close unneeded tickets.\n\n${ticketList.join(',\n')}`)
.setFooter(message.guild.name + ' | This message will be deleted in 15 seconds', message.guild.iconURL())
);
return setTimeout(async () => {
await message.delete();
await m.delete();
}, 15000);
}
let topic = args.join(' ');
if (topic.length > 256)
return message.channel.send(
new Discord.MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle(':x: **Description too long**')
.setDescription('Please limit your ticket topic to less than 256 characters. A short sentence will do.')
.setFooter(message.guild.name, message.guild.iconURL())
);
else if (topic.length < 1)
topic = 'No topic given';
let ticket = await Ticket.create({
channel: '',
creator: message.author.id,
open: true,
archived: false,
topic: topic
});
let name = 'ticket-' + ticket.get('id');
message.guild.channels.create(name, {
type: 'text',
topic: `${message.author} | ${topic}`,
parent: config.tickets.category,
permissionOverwrites: [{
id: message.guild.roles.everyone,
deny: ['VIEW_CHANNEL', 'SEND_MESSAGES']
},
{
id: message.member,
allow: ['VIEW_CHANNEL', 'SEND_MESSAGES', 'ATTACH_FILES', 'READ_MESSAGE_HISTORY']
},
{
id: supportRole,
allow: ['VIEW_CHANNEL', 'SEND_MESSAGES', 'ATTACH_FILES', 'READ_MESSAGE_HISTORY']
}
],
reason: 'User requested a new support ticket channel'
}).then(async c => {
Ticket.update({
channel: c.id
}, {
where: {
id: ticket.id
}
});
let m = await message.channel.send(
new Discord.MessageEmbed()
.setColor(config.colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle(':white_check_mark: **Ticket created**')
.setDescription(`Your ticket has been created: ${c}`)
.setFooter(client.user.username + ' | This message will be deleted in 15 seconds', client.user.avatarURL())
);
setTimeout(async () => {
await message.delete();
await m.delete();
}, 15000);
let ping;
switch (config.tickets.ping) {
case 'staff':
ping = `<@&${config.staff_role}>,\n`;
break;
case false:
ping = '';
break;
default:
ping = `@${config.tickets.ping},\n`;
}
await c.send(ping + `${message.author} has created a new ticket`);
if (config.tickets.send_img) {
const images = fs.readdirSync('user/images');
await c.send({
files: [
'user/images/' +
images[Math.floor(Math.random() * images.length)]
]
});
}
let text = config.tickets.text
.replace('{{ name }}', message.author.username)
.replace('{{ tag }}', message.author);
let w = await c.send(
new Discord.MessageEmbed()
.setColor(config.colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setDescription(text)
.addField('Topic', `\`${topic}\``)
.setFooter(client.user.username, client.user.avatarURL())
);
if (config.tickets.pin)
await w.pin();
// await w.pin().then(m => m.delete()); // oopsie, this deletes the pinned message
if (config.logs.discord.enabled)
client.channels.cache.get(config.logs.discord.channel).send(
new Discord.MessageEmbed()
.setColor(config.colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('New ticket')
.setDescription(`\`${topic}\``)
.addField('Creator', message.author, true)
.addField('Channel', c, true)
.setFooter(client.user.username, client.user.avatarURL())
.setTimestamp()
);
log.info(`${message.author.tag} created a new ticket (#${name})`);
}).catch(log.error);
},
};

117
src/commands/remove.js Normal file
View File

@ -0,0 +1,117 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const Discord = require('discord.js');
const config = require('../../user/config.js');
const ChildLogger = require('leekslazylogger').ChildLogger;
const log = new ChildLogger();
module.exports = {
name: 'remove',
description: 'Remove a member from ticket channel',
usage: '<@member> [... #channel]',
aliases: ['-'],
example: 'remove @member from #ticket-23',
args: true,
async execute(client, message, args, Ticket) {
const notTicket = new Discord.MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle(':x: **This isn\'t a ticket channel**')
.setDescription('Use this command in the ticket channel you want to remove a user from, or mention the channel.')
.addField('Usage', `\`${config.prefix}${this.name} ${this.usage}\`\n`)
.addField('Help', `Type \`${config.prefix}help ${this.name}\` for more information`)
.setFooter(message.guild.name, message.guild.iconURL());
let ticket;
let channel = message.mentions.channels.first();
if(!channel) {
channel = message.channel;
ticket = await Ticket.findOne({ where: { channel: message.channel.id } });
if(!ticket)
return message.channel.send(notTicket);
} else {
ticket = await Ticket.findOne({ where: { channel: channel.id } });
if(!ticket) {
notTicket
.setTitle(':x: **Channel is not a ticket**')
.setDescription(`${channel} is not a ticket channel.`);
return message.channel.send(notTicket);
}
}
if(message.author.id !== ticket.get('creator') && !message.member.roles.cache.has(config.staff_role))
return message.channel.send(
new Discord.MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle(':x: **No permission**')
.setDescription(`You don't have permission to alter ${channel} as it does not belong to you and you are not staff.`)
.addField('Usage', `\`${config.prefix}${this.name} ${this.usage}\`\n`)
.addField('Help', `Type \`${config.prefix}help ${this.name}\` for more information`)
.setFooter(message.guild.name, message.guild.iconURL())
);
let member = message.guild.member(message.mentions.users.first() || message.guild.members.cache.get(args[0]));
if(!member)
return message.channel.send(
new Discord.MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle(':x: **Unknown member**')
.setDescription('Please mention a valid member.')
.addField('Usage', `\`${config.prefix}${this.name} ${this.usage}\`\n`)
.addField('Help', `Type \`${config.prefix}help ${this.name}\` for more information`)
.setFooter(message.guild.name, message.guild.iconURL())
);
try {
channel.updateOverwrite(member.user, {
VIEW_CHANNEL: false,
SEND_MESSAGES: false,
ATTACH_FILES: false,
READ_MESSAGE_HISTORY: false
});
if(channel.id !== message.channel.id)
channel.send(
new Discord.MessageEmbed()
.setColor(config.colour)
.setAuthor(member.user.username, member.user.displayAvatarURL())
.setTitle('**Member remove**')
.setDescription(`${member} has been removed by ${message.author}`)
.setFooter(message.guild.name, message.guild.iconURL())
);
message.channel.send(
new Discord.MessageEmbed()
.setColor(config.colour)
.setAuthor(member.user.username, member.user.displayAvatarURL())
.setTitle(':white_check_mark: **Member removed**')
.setDescription(`${member} has been removed from <#${ticket.get('channel')}>`)
.setFooter(message.guild.name, message.guild.iconURL())
);
log.info(`${message.author.tag} removed a user from a ticket (#${message.channel.id})`);
} catch (error) {
log.error(error);
}
// command ends here
},
};

114
src/commands/tickets.js Normal file
View File

@ -0,0 +1,114 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const Discord = require('discord.js');
const fs = require('fs');
const config = require('../../user/config');
module.exports = {
name: 'tickets',
description: 'List your recent tickets to access transcripts / archives.',
usage: '[@member]',
aliases: ['list'],
example: '',
args: false,
async execute(client, message, args, Ticket) {
const supportRole = message.guild.roles.cache.get(config.staff_role);
if (!supportRole)
return message.channel.send(
new Discord.MessageEmbed()
.setColor(config.err_colour)
.setTitle(':x: **Error**')
.setDescription(`${config.name} has not been set up correctly. Could not find a 'support team' role with the id \`${config.staff_role}\``)
.setFooter(message.guild.name, message.guild.iconURL())
);
let context;
let user = message.mentions.users.first() || message.guild.members.cache.get(args[0]);
if(!user) {
if(!message.member.roles.cache.has(config.staff_role))
return message.channel.send(
new Discord.MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle(':x: **No permission**')
.setDescription('You don\'t have permission to list others\' tickets as you are not staff.')
.addField('Usage', `\`${config.prefix}${this.name} ${this.usage}\`\n`)
.addField('Help', `Type \`${config.prefix}help ${this.name}\` for more information`)
.setFooter(message.guild.name, message.guild.iconURL())
);
user = message.author;
context = 'staff';
}
context = 'self';
let openTickets = await Ticket.findAndCountAll({
where: {
creator: user.id,
open: true
}
});
let closedTickets = await Ticket.findAndCountAll({
where: {
creator: user.id,
open: false
}
});
closedTickets.rows = closedTickets.rows.slice(-10); // get most recent 10
let embed = new Discord.MessageEmbed()
.setColor(config.colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('Your tickets')
.setFooter(message.guild.name + ' | This message will be deleted in 60 seconds', message.guild.iconURL());
if(config.transcripts.web.enabled)
embed.setDescription(`You can access all of your ticket archives on the [web portal](${config.transcripts.web.server}/${user.id}).`);
let open = [],
closed = [];
for (let t in openTickets.rows) {
let desc = openTickets.rows[t].topic.substring(0, 30);
open.push(`> <#${openTickets.rows[t].channel}>: \`${desc}${desc.length > 20 ? '...' : ''}\``);
}
for (let t in closedTickets.rows) {
let desc = closedTickets.rows[t].topic.substring(0, 30);
let transcript = '';
if(fs.existsSync(`user/transcripts/text/${closedTickets.rows[t].channel}.txt`))
transcript = `\n> Type \`${config.prefix}transcript ${closedTickets.rows[t].id}\` to download text transcript.`;
closed.push(`> #${closedTickets.rows[t].id}: \`${desc}${desc.length > 20 ? '...' : ''}\`${transcript}`);
}
let pre = context === 'self' ? 'You have' : user + ' has';
embed.addField('Open tickets', openTickets.count === 0 ? `${pre} no open tickets.` : open.join('\n\n'), false);
embed.addField('Closed tickets', closedTickets.count === 0 ? `${pre} no old tickets` : closed.join('\n\n'), false);
let m = await message.channel.send(embed);
return setTimeout(async () => {
await message.delete();
await m.delete();
}, 60000);
},
};

View File

@ -0,0 +1,23 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const Discord = require('discord.js');
const fs = require('fs');
const config = require('../../user/config');
module.exports = {
name: 'transcript',
description: 'Download a transcript',
usage: '<ticket-id>',
aliases: ['archive', 'download'],
example: 'transcript 57',
args: false,
async execute(client, message, args, Ticket) {
/** @TODO TRY TO SEND ATTACHMENT TO DM */
}
};

17
src/events/debug.js Normal file
View File

@ -0,0 +1,17 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const ChildLogger = require('leekslazylogger').ChildLogger;
const log = new ChildLogger();
module.exports = {
event: 'debug',
execute(client, e) {
log.debug(e);
}
};

17
src/events/error.js Normal file
View File

@ -0,0 +1,17 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const ChildLogger = require('leekslazylogger').ChildLogger;
const log = new ChildLogger();
module.exports = {
event: 'error',
execute(client, e) {
log.error(e);
}
};

87
src/events/message.js Normal file
View File

@ -0,0 +1,87 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const Discord = require('discord.js');
const ChildLogger = require('leekslazylogger').ChildLogger;
const log = new ChildLogger();
const config = require('../../user/config');
module.exports = {
event: 'message',
async execute(client, message, Ticket) {
if (message.author.bot || message.author.id === client.user.id) return;
if (message.channel.type === 'dm') {
log.console(`Received a DM from ${message.author.tag}: ${message.cleanContent}`);
return message.channel.send(`Hello there, ${message.author.username}!
I am the support bot for **${client.guilds.cache.get(config.guild)}**.
Type \`${config.prefix}new\` on the server to create a new ticket.`);
}
const prefixRegex = new RegExp(`^(<@!?${client.user.id}>|\\${config.prefix})\\s*`);
if (!prefixRegex.test(message.content)) return;
const [, matchedPrefix] = message.content.match(prefixRegex);
const args = message.content.slice(matchedPrefix.length).trim().split(/ +/);
const commandName = args.shift().toLowerCase();
const command = client.commands.get(commandName) || client.commands.find(cmd => cmd.aliases && cmd.aliases.includes(commandName));
if (!command || commandName === 'none') return;
if (command.permission && !message.member.hasPermission(command.permission)) {
log.console(`${message.author.tag} tried to use the '${command.name}' command without permission`);
return message.channel.send(
new Discord.MessageEmbed()
.setColor(config.err_colour)
.setTitle(':x: No permission')
.setDescription(`**You do not have permission to use the \`${command.name}\` command** (requires \`${command.permission}\`).`)
.setFooter(message.guild.name, message.guild.iconURL())
);
}
if (command.args && !args.length)
return message.channel.send(
new Discord.MessageEmbed()
.setColor(config.err_colour)
.addField('Usage', `\`${config.prefix}${command.name} ${command.usage}\`\n`)
.addField('Help', `Type \`${config.prefix}help ${command.name}\` for more information`)
.setFooter(message.guild.name, message.guild.iconURL())
);
if (!client.cooldowns.has(command.name)) client.cooldowns.set(command.name, new Discord.Collection());
const now = Date.now();
const timestamps = client.cooldowns.get(command.name);
const cooldownAmount = (command.cooldown || config.cooldown) * 1000;
if (timestamps.has(message.author.id)) {
const expirationTime = timestamps.get(message.author.id) + cooldownAmount;
if (now < expirationTime) {
const timeLeft = (expirationTime - now) / 1000;
log.console(`${message.author.tag} attempted to use the '${command.name}' command before the cooldown was over`);
return message.channel.send(
new Discord.MessageEmbed()
.setColor(config.err_colour)
.setDescription(`:x: Please wait ${timeLeft.toFixed(1)} second(s) before reusing the \`${command.name}\` command.`)
.setFooter(message.guild.name, message.guild.iconURL())
);
}
}
timestamps.set(message.author.id, now);
setTimeout(() => timestamps.delete(message.author.id), cooldownAmount);
try {
command.execute(client, message, args, Ticket);
log.console(`${message.author.tag} used the '${command.name}' command`);
} catch (error) {
log.warn(`An error occurred whilst executing the '${command.name}' command`);
log.error(error);
message.channel.send(`:x: An error occurred whilst executing the \`${command.name}\` command.\nThe issue has been reported.`);
}
}
};

18
src/events/rateLimit.js Normal file
View File

@ -0,0 +1,18 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const ChildLogger = require('leekslazylogger').ChildLogger;
const log = new ChildLogger();
module.exports = {
event: 'rateLimit',
execute(client, limit) {
log.warn('Rate-limited!');
log.debug(limit);
}
};

42
src/events/ready.js Normal file
View File

@ -0,0 +1,42 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const ChildLogger = require('leekslazylogger').ChildLogger;
const log = new ChildLogger();
const config = require('../../user/config');
module.exports = {
event: 'ready',
execute(client) {
log.success(`Authenticated as ${client.user.tag}`);
const updatePresence = () => {
let num = Math.floor(Math.random() * config.activities.length);
client.user.setPresence({
activity: {
name: config.activities[num] + ` | ${config.prefix}help`,
type: config.activity_types[num]
}
}).catch(log.error);
log.debug(`Updated presence: ${config.activity_types[num]} ${config.activities[num]}`);
};
updatePresence();
setInterval(() => {
updatePresence();
}, 15000);
if (client.guilds.cache.get(config.guild).member(client.user).hasPermission('ADMINISTRATOR', false))
log.success('\'ADMINISTRATOR\' permission has been granted');
else
log.warn('Bot does not have \'ADMINISTRATOR\' permission');
}
};

17
src/events/warn.js Normal file
View File

@ -0,0 +1,17 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const ChildLogger = require('leekslazylogger').ChildLogger;
const log = new ChildLogger();
module.exports = {
event: 'warn',
execute(client, e) {
log.warn(e);
}
};

99
src/index.js Normal file
View File

@ -0,0 +1,99 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
require('dotenv').config({path: 'user/.env'});
const Discord = require('discord.js');
const fs = require('fs');
const leeks = require('leeks.js');
const client = new Discord.Client({
autoReconnect: true
});
client.events = new Discord.Collection();
client.commands = new Discord.Collection();
client.cooldowns = new Discord.Collection();
require('./utils/banner')(leeks); // big coloured text thing
const config = require('../user/config');
const Logger = require('leekslazylogger');
const log = new Logger({
name: config.name,
logToFile: config.logs.files.enabled,
maxAge: config.logs.files.keep_for,
debug: config.debug
});
log.multi(log); // required to allow other files to access the logger
require('./utils/updater')(); // check for updates
/**
* storage
*/
const { Sequelize, Model, DataTypes } = require('sequelize');
let sequelize;
if(config.storage.type === 'mysql') {
log.info('Connecting to MySQL database...');
sequelize = new Sequelize(process.env.DB_NAME, process.env.DB_USER, process.env.DB_PASS, {
dialect: 'mysql',
host: process.env.DB_HOST,
logging: log.debug
});
} else {
log.info('Using SQLite storage');
sequelize = new Sequelize({
dialect: 'sqlite',
storage: 'user/storage.db',
logging: log.debug
});
}
class Ticket extends Model {}
Ticket.init({
channel: DataTypes.STRING,
creator: DataTypes.STRING,
open: DataTypes.BOOLEAN,
topic: DataTypes.TEXT
}, {
sequelize,
modelName: 'ticket'
});
Ticket.sync();
/**
* event loader
*/
const events = fs.readdirSync('src/events').filter(file => file.endsWith('.js'));
for (const file of events) {
const event = require(`./events/${file}`);
client.events.set(event.event, event);
client.on(event.event, e => client.events.get(event.event).execute(client, e, Ticket));
log.console(log.format(`> Loaded &7${event.event}&f event`));
}
/**
* command loader
*/
const commands = fs.readdirSync('src/commands').filter(file => file.endsWith('.js'));
for (const file of commands) {
const command = require(`./commands/${file}`);
client.commands.set(command.name, command);
log.console(log.format(`> Loaded &7${config.prefix}${command.name}&f command`));
}
log.info(`Loaded ${events.length} events and ${commands.length} commands`);
process.on('unhandledRejection', error => {
log.warn('An error was not caught');
log.error(`Uncaught error: \n${error.stack}`);
});
client.login(process.env.TOKEN);

22
src/utils/archive.js Normal file
View File

@ -0,0 +1,22 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const ChildLogger = require('leekslazylogger').ChildLogger;
const log = new ChildLogger();
module.exports.create = (client, channel) => {
};
module.exports.addUser = (client, channel, user) => {
};
module.exports.addMessage = (client, channel, message) => {
};

31
src/utils/banner.js Normal file
View File

@ -0,0 +1,31 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const { version, homepage } = require('../../package.json');
module.exports = (leeks) => {
console.log(leeks.colours.cyan(`
######## #### ###### ###### ####### ######## ########
## ## ## ## ## ## ## ## ## ## ## ## ##
## ## ## ## ## ## ## ## ## ## ##
## ## ## ###### ## ## ## ######## ## ##
## ## ## ## ## ## ## ## ## ## ##
## ## ## ## ## ## ## ## ## ## ## ## ##
######## #### ###### ###### ####### ## ## ########
######## #### ###### ## ## ######## ######## ######
## ## ## ## ## ## ## ## ## ##
## ## ## ## ## ## ## ##
## ## ## ##### ###### ## ######
## ## ## ## ## ## ## ##
## ## ## ## ## ## ## ## ## ##
## #### ###### ## ## ######## ## ######
`));
console.log(leeks.colours.cyanBright(`DiscordTickets bot v${version} by eartharoid`));
console.log(leeks.colours.cyanBright(homepage));
console.log('\n\n');
};

31
src/utils/updater.js Normal file
View File

@ -0,0 +1,31 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const ChildLogger = require('leekslazylogger').ChildLogger;
const log = new ChildLogger();
const fetch = require('node-fetch');
const config = require('../../user/config');
let {version} = require('../../package.json');
version = 'v' + version;
module.exports = () => {
if(!config.updater)
return;
fetch('https://api.github.com/repos/eartharoid/DiscordTickets/releases')
.then(res => res.json())
.then(json => {
const update = json[0];
if (version !== update.tag_name) {
log.notice('There is an update available for Discord Tickets');
log.info(`Download "&f${update.name}&3" from &6https://github.com/eartharoid/DiscordTickets/releases/`);
log.notice(`You currently have ${version}; The latest is ${update.tag_name}`);
}
});
};

79
user/config.js Normal file
View File

@ -0,0 +1,79 @@
/**
* ###############################################################################################
* ____ _ _____ _ _
* | _ \ (_) ___ ___ ___ _ __ __| | |_ _| (_) ___ | | __ ___ | |_ ___
* | | | | | | / __| / __| / _ \ | '__| / _` | | | | | / __| | |/ / / _ \ | __| / __|
* | |_| | | | \__ \ | (__ | (_) | | | | (_| | | | | | | (__ | < | __/ | |_ \__ \
* |____/ |_| |___/ \___| \___/ |_| \__,_| |_| |_| \___| |_|\_\ \___| \__| |___/
*
* ---------------------
* Quick Start
* ---------------------
*
* > For detailed instructions, visit the GitHub repository and read the documentation:
* https://github.com/eartharoid/DiscordTickets/#readme
*
* > IMPORTANT: Also rename 'user/example.env' to 'user/.env' and edit the TOKEN
*
* ---------------------
* Support
* ---------------------
*
* > Information: https://github.com/eartharoid/DiscordTickets/#readme
* > Discord Support Server: https://go.eartharoid.me/discord
* > Wiki: https://github.com/eartharoid/DiscordTickets/wiki
*
* ###############################################################################################
*/
module.exports = {
prefix: '-',
name: 'DiscordTickets',
activities: ['-new', 'with tickets', 'for new tickets'], /** @INFO " | {PRE}help" */
activity_types: ['PLAYING', 'PLAYING', 'WATCHING'], /** @INFO paired */
colour: '#009999',
err_colour: '#E74C3C',
guild: '451745464480432129', // ID of your guild
staff_role: '451745586564169728', // ID of your Support Team role
tickets: {
category: '620272351988285480', // ID of your tickets category
send_img: true,
ping: 'here', /** @INFO here, everyone, staff, false */
text: `Hello there, {{ tag }}!
A member of staff will assist you shortly.
In the mean time, please describe your issue in as much detail as possible! :)`, // {{ name }} and {{ tag }}
pin: false,
max: 3 /** @INFO OPEN */
},
transcripts: {
text: {
enabled: true,
keep_for: 90,
},
web: {
enabled: false,
server: 'https://tickets.example.com', // WITHOUT! trailing /
}
},
storage: {
type: 'sqlite'
},
logs: {
files: {
enabled: true,
keep_for: 7
},
discord: {
enabled: true,
channel: '573957980152791080' // ID of your log channel
}
},
debug: false,
updater: false /** @INFO ENABLE BY DEFAULT */
};

8
user/example.env Normal file
View File

@ -0,0 +1,8 @@
TOKEN=
ARCHIVES_KEY=
DB_HOST=
DB_NAME=
DB_USER=
DB_PASS=

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

View File