Set up for v3

This commit is contained in:
Isaac 2021-02-15 18:34:59 +00:00
parent 7763889a2d
commit 9863c45fdf
No known key found for this signature in database
GPG Key ID: 279D1F53391CED07
60 changed files with 2762 additions and 5717 deletions

View File

@ -11,7 +11,7 @@ module.exports = {
'SharedArrayBuffer': 'readonly'
},
'parserOptions': {
'ecmaVersion': 2018
'ecmaVersion': 2021
},
'rules': {
'indent': [

View File

@ -4,26 +4,20 @@
### 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.
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 else hasn't already created a similar first.
### 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).
~~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).~~
You can request new features on [Feedbacky](https://app.feedbacky.net/b/dsctickets/).
### 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).
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 (if you need help or have questions), you can join the [Discord server](https://go.eartharoid.me/discord) or use the [Discussions tab](https://github.com/eartharoid/DiscordTickets/discussions).
## 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
**Note**: Create `user/dev.env` and `user/dev.config.js` for testing.

View File

@ -1,23 +1,23 @@
---
name: Bug report
about: Report an issue or bug
title: ''
labels: ''
about: Report a bug
title: '[BUG] '
labels: 'bug'
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
<!-- A clear and concise description of what the bug is -->
**To Reproduce**
Steps to reproduce the behaviour:
**To reproduce**
<!-- Steps to reproduce the behaviour -->
**Expected behavior**
A clear and concise description of what you expected to happen.
<!-- A clear and concise description of what you expected to happen -->
**Screenshots**
If applicable, add screenshots to help explain your problem.
<!-- If applicable, add screenshots to help explain your problem -->
**Additional context**
Add any other context about the problem here.
<!-- Add any other context about the problem here -->

View File

@ -1,20 +1,24 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
title: '[FEATURE] '
labels: 'feature_request'
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is.
<!--
Note that you can now submit feature requests on Feedbacky: https://app.feedbacky.net/b/dsctickets
-->
**Is your feature request related to a problem?**
<!-- A clear and concise description of what the problem is. Reference any relevant issues. -->
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
<!-- A clear and concise description of what you want to happen. -->
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
**Additional context**
Add any other context or screenshots about the feature request here.
<!-- Add any other context or screenshots about the feature request here. -->

View File

@ -1,22 +1,28 @@
<!--
Please read the CONTRIBUTING file (.github/CONTRIBUTING.md) before creating a pull request.
Unless you are doing something small like fixing a typo, please create an issue first!
-->
#### Information
> Check one
<!-- Please select **one** by replacing the space with an `x`: `[X]` -->
- [ ] This includes major changes (breaking changes)
- [ ] This includes minor changes (minimal usage changes, minor new features)
- [ ] This includes patches (bug fixes, documentation changes etc)
- [ ] This includes patches (bug or typo fixes)
- [ ] This is includes **only** documentation changes
#### Is this related to an issue?
> Reference any issues here
<!-- Reference any issues here -->
#### Changes made
> Describe your changes
<!-- Describe your changes -->
#### Confirmations
> Check all that apply
<!-- Select **all that apply** by replacing the space with an `x`: `[X]` -->
- [ ] I have updated any necessary documentation
- [ ] This uses consistent code style

15
.github/SECURITY.md vendored
View File

@ -1,14 +1,15 @@
# Security Policy
# Security policy
## Supported Versions
## Supported versions
Release versions that will receive security updates.
| Version | Supported |
| ------- | ------------------ |
| 2.x | ✅ |
| < 2.0 | |
| Version | Supported |
| ------- | -------------- |
| 3.x | ✅ |
| 2.x | ⚠️ Deprecated |
| < 2.0 | |
## Reporting a Vulnerability
## Reporting a vulnerability
If you find a vulnerability, please [email me](mailto:contact@eartharoid.me).

15
.github/workflows/deploy-docs.yml vendored Normal file
View File

@ -0,0 +1,15 @@
name: Build and deploy docs
on:
push:
branches:
- master
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: 3.x
- run: pip install -r requirements.txt
- run: mkdocs gh-deploy --force

14
.gitignore vendored
View File

@ -1,10 +1,10 @@
logs/
# directories
.vscode/
node_modules/
user/dev.env
user/dev.config.js
user/storage.db
user/transcripts/text/*.txt
user/transcripts/raw/*.log
user/transcripts/raw/entities/*.json
logs/
site/
# files
.env
user/config.js
*.code-workspace

View File

@ -1,3 +1,4 @@
# Please download from [releases](https://github.com/eartharoid/DiscordTickets/releases) or [v2 branch](https://github.com/eartharoid/DiscordTickets/tree/v2) - master branch is currently a work in progress
# DiscordTickets
[![Run on Repl.it](https://repl.it/badge/github/eartharoid/DiscordTickets)](https://repl.it/github/eartharoid/DiscordTickets) [![GitHub issues](https://img.shields.io/github/issues/eartharoid/DiscordTickets?style=flat-square)](https://github.com/eartharoid/DiscordTickets/issues) [![GitHub stars](https://img.shields.io/github/stars/eartharoid/DiscordTickets?style=flat-square)](https://github.com/eartharoid/DiscordTickets/stargazers) [![GitHub forks](https://img.shields.io/github/forks/eartharoid/DiscordTickets?style=flat-square)](https://github.com/eartharoid/DiscordTickets/network) [![GitHub license](https://img.shields.io/github/license/eartharoid/DiscordTickets?style=flat-square)](https://github.com/eartharoid/DiscordTickets/blob/master/LICENSE) ![Codacy grade](https://img.shields.io/codacy/grade/14e6851c85444424b75b8bc3f93e93db?logo=codacy&style=flat-square) [![Discord support server](https://discordapp.com/api/guilds/451745464480432129/embed.png?style=shield)](https://discord.gg/pXc9vyC)

3
docs/.pages Normal file
View File

@ -0,0 +1,3 @@
arrange:
- index.md
- ...

1
docs/README Normal file
View File

@ -0,0 +1 @@
This documentation is intended to be access through the website (https://eartharoid.github.io/discordtickets)

BIN
docs/img/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

BIN
docs/img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

1
docs/index.md Normal file
View File

@ -0,0 +1 @@
# Home

View File

@ -0,0 +1,24 @@
a code {
color: var(--md-primary-fg-color) !important;
background-color: rgba(213, 0, 44, 0.1) !important;
transition: color 125ms;
transition: background-color 125ms
}
a code:hover {
/* ONLY WORKS ON SLATE THEME
color: #81E8ED !important;
background-color: rgba(129, 232, 237, 0.1) !important; */
color: var(--md-primary-fg-color--dark) !important;
}
.md-announce {
background-color: var(--md-primary-fg-color--dark) !important;
color: white;
}
.md-announce a, .md-announce a:hover {
color: white !important;
/* text-decoration: underline; */
font-weight: bold;
}

6
example.env Normal file
View File

@ -0,0 +1,6 @@
DISCORD_TOKEN=
DB_HOST=
DB_NAME=
DB_USER=
DB_PASS=

82
mkdocs.yml Normal file
View File

@ -0,0 +1,82 @@
# Project information
site_name: DiscordTickets
site_description: An open-source & self-hosted Discord bot for ticket management.
site_author: eartharoid
site_url: https://eartharoid.github.io/discordtickets
# Repository
repo_name: eartharoid/DiscordTickets
repo_url: https://github.com/eartharoid/DiscordTickets
edit_uri: blob/master/docs/
# Copyright
copyright: '&copy; 2021 Isaac Saunders'
# Configuration
extra_css:
- stylesheets/extra.css
theme:
name: material
language: en
custom_dir: overrides/
palette:
scheme: default
primary: primary
accent: primary
font:
text: Roboto
code: Roboto Mono
features:
- instant
- tabs
- navigation.expand
logo: /img/logo.png
favicon: /img/favicon.ico
# Extras
extra:
social:
- icon: fontawesome/brands/github-alt
link: https://github.com/eartharoid
- icon: fontawesome/brands/twitter
link: https://twitter.com/eartharoid
plugins:
- search # necessary for search to work
- git-revision-date-localized # last modified date at bottom of page
- awesome-pages # custom nav order
# Extensions
markdown_extensions:
- admonition
- codehilite:
guess_lang: false
- toc:
permalink: true
- footnotes
- meta
# pymd
- pymdownx.arithmatex
- pymdownx.betterem:
smart_enable: all
- pymdownx.caret
- pymdownx.critic
- pymdownx.details
- pymdownx.emoji:
emoji_index: !!python/name:materialx.emoji.twemoji
emoji_generator: !!python/name:materialx.emoji.to_svg
- pymdownx.inlinehilite
- pymdownx.magiclink
- pymdownx.mark
- pymdownx.smartsymbols
- pymdownx.superfences
- pymdownx.tasklist:
custom_checkbox: true
- pymdownx.tabbed
- pymdownx.tilde

2
overrides/announce.html Normal file
View File

@ -0,0 +1,2 @@
<span class="twemoji">{% include ".icons/material/alert-decagram.svg" %}</span>
<a href="/changelog">See what's new in v3.0.</a>

2
overrides/main.html Normal file
View File

@ -0,0 +1,2 @@
{% extends "base.html" %}
{% block announce %}{% include "announce.html" ignore missing %}{% endblock %}

2915
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "@eartharoid/discordtickets",
"version": "2.1.3",
"version": "3.0.0",
"private": true,
"description": "An open-source & self-hosted Discord bot for ticket management.",
"main": "src/index.js",
@ -34,7 +34,7 @@
"test": "echo \"Nothing to test! Run with 'npm start'\" && exit 1"
},
"engines": {
"node": ">=12"
"node": ">=14"
},
"repository": {
"type": "git",
@ -42,8 +42,8 @@
},
"keywords": [
"discord",
"bot",
"tickets"
"tickets",
"bot"
],
"author": "eartharoid",
"license": "GPL-3.0",

2462
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

3
requirements.txt Normal file
View File

@ -0,0 +1,3 @@
mkdocs-material
mkdocs-awesome-pages-plugin
mkdocs-git-revision-date-localized-plugin

View File

@ -1,111 +0,0 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const { MessageEmbed } = require('discord.js');
module.exports = {
name: 'add',
description: 'Add a member to a ticket channel',
usage: '<@member> [... #channel]',
aliases: ['none'],
example: 'add @member to #ticket-23',
args: true,
async execute(client, message, args, log, {config, Ticket}) {
const guild = client.guilds.cache.get(config.guild);
const notTicket = new MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('❌ **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(guild.name, 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('❌ **Channel is not a ticket**')
.setDescription(`${channel} is not a ticket channel.`);
return message.channel.send(notTicket);
}
}
if (message.author.id !== ticket.creator && !message.member.roles.cache.has(config.staff_role)) {
return message.channel.send(
new MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('❌ **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(guild.name, guild.iconURL())
);
}
let member = guild.member(message.mentions.users.first() || guild.members.cache.get(args[0]));
if (!member) {
return message.channel.send(
new MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('❌ **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(guild.name, 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 MessageEmbed()
.setColor(config.colour)
.setAuthor(member.user.username, member.user.displayAvatarURL())
.setTitle('**Member added**')
.setDescription(`${member} has been added by ${message.author}`)
.setFooter(guild.name, guild.iconURL())
);
}
message.channel.send(
new MessageEmbed()
.setColor(config.colour)
.setAuthor(member.user.username, member.user.displayAvatarURL())
.setTitle('✅ **Member added**')
.setDescription(`${member} has been added to <#${ticket.channel}>`)
.setFooter(guild.name, guild.iconURL())
);
log.info(`${message.author.tag} added a user to a ticket (#${message.channel.id})`);
} catch (error) {
log.error(error);
}
// command ends here
},
};

View File

@ -1,238 +0,0 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const { MessageEmbed } = require('discord.js');
const fs = require('fs');
const { join } = require('path');
const archive = require('../modules/archive');
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, log, { config, Ticket }) {
const guild = client.guilds.cache.get(config.guild);
const notTicket = new MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('❌ **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(guild.name, guild.iconURL());
let ticket;
let channel = message.mentions.channels.first();
// || client.channels.resolve(await Ticket.findOne({ where: { id: args[0] } }).channel) // channels.fetch()
if (!channel) {
channel = message.channel;
ticket = await Ticket.findOne({
where: {
channel: channel.id
}
});
if (!ticket) return message.channel.send(notTicket);
} else {
ticket = await Ticket.findOne({
where: {
channel: channel.id
}
});
if (!ticket) {
notTicket
.setTitle('❌ **Channel is not a ticket**')
.setDescription(`${channel} is not a ticket channel.`);
return message.channel.send(notTicket);
}
}
let paths = {
text: join(__dirname, `../../user/transcripts/text/${ticket.get('channel')}.txt`),
log: join(__dirname, `../../user/transcripts/raw/${ticket.get('channel')}.log`),
json: join(__dirname, `../../user/transcripts/raw/entities/${ticket.get('channel')}.json`)
};
if (message.author.id !== ticket.creator && !message.member.roles.cache.has(config.staff_role))
return message.channel.send(
new MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('❌ **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(guild.name, guild.iconURL())
);
if (config.commands.close.confirmation) {
let success;
let pre = fs.existsSync(paths.text) || fs.existsSync(paths.log)
? `You will be able to view an archived version later with \`${config.prefix}transcript ${ticket.id}\``
: '';
let confirm = await message.channel.send(
new MessageEmbed()
.setColor(config.colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('❔ Are you sure?')
.setDescription(`${pre}\n**React with ✅ to confirm.**`)
.setFooter(guild.name + ' | Expires in 15 seconds', guild.iconURL())
);
await confirm.react('✅');
const collector = confirm.createReactionCollector(
(r, u) => r.emoji.name === '✅' && u.id === message.author.id, {
time: 15000
});
collector.on('collect', async () => {
if (channel.id !== message.channel.id) {
channel.send(
new MessageEmbed()
.setColor(config.colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('**Ticket closed**')
.setDescription(`Ticket closed by ${message.author}`)
.setFooter(guild.name, guild.iconURL())
);
}
confirm.reactions.removeAll();
confirm.edit(
new MessageEmbed()
.setColor(config.colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle(`✅ **Ticket ${ticket.id} closed**`)
.setDescription('The channel will be automatically deleted in a few seconds, once the contents have been archived.')
.setFooter(guild.name, guild.iconURL())
);
if (channel.id !== message.channel.id)
message.delete({
timeout: 5000
}).then(() => confirm.delete());
success = true;
close();
});
collector.on('end', () => {
if (!success) {
confirm.reactions.removeAll();
confirm.edit(
new MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('❌ **Expired**')
.setDescription('You took too long to react; confirmation failed.')
.setFooter(guild.name, guild.iconURL()));
message.delete({
timeout: 10000
}).then(() => confirm.delete());
}
});
} else {
close();
}
async function close () {
let users = [];
if (config.transcripts.text.enabled || config.transcripts.web.enabled) {
let u = await client.users.fetch(ticket.creator);
if (u) {
let dm;
try {
dm = u.dmChannel || await u.createDM();
} catch (e) {
log.warn(`Could not create DM channel with ${u.tag}`);
}
let res = {};
const embed = new MessageEmbed()
.setColor(config.colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle(`Ticket ${ticket.id}`)
.setFooter(guild.name, guild.iconURL());
if (fs.existsSync(paths.text)) {
embed.addField('Text transcript', 'See attachment');
res.files = [{
attachment: paths.text,
name: `ticket-${ticket.id}-${ticket.get('channel')}.txt`
}];
}
if (fs.existsSync(paths.log) && fs.existsSync(paths.json)) {
let data = JSON.parse(fs.readFileSync(paths.json));
for (u in data.entities.users) users.push(u);
embed.addField('Web archive', await archive.export(Ticket, channel)); // this will also delete these files
}
if (embed.fields.length < 1) {
embed.setDescription(`No text transcripts or archive data exists for ticket ${ticket.id}`);
}
res.embed = embed;
try {
if (config.commands.close.send_transcripts) dm.send(res);
if (config.transcripts.channel.length > 1) client.channels.cache.get(config.transcripts.channel).send(res);
} catch (e) {
message.channel.send('❌ Couldn\'t send DM or transcript log message');
}
}
}
// update database
ticket.update({
open: false
}, {
where: {
channel: channel.id
}
});
// delete channel
channel.delete({
timeout: 5000
});
log.info(`${message.author.tag} closed a ticket (#ticket-${ticket.id})`);
if (config.logs.discord.enabled) {
let embed = new MessageEmbed()
.setColor(config.colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle(`Ticket ${ticket.id} closed`)
.addField('Creator', `<@${ticket.creator}>`, true)
.addField('Closed by', message.author, true)
.setFooter(guild.name, guild.iconURL())
.setTimestamp();
if (users.length > 1)
embed.addField('Members', users.map(u => `<@${u}>`).join('\n'));
client.channels.cache.get(config.logs.discord.channel).send(embed);
}
}
}
};

View File

@ -1,258 +0,0 @@
/**
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const { MessageEmbed } = require('discord.js');
const fs = require('fs');
const { join } = require('path');
const config = require(join(__dirname, '../../user/', require('../').config));
const archive = require('../modules/archive');
const { plural } = require('../modules/utils');
const { Op } = require('sequelize');
const toTime = require('to-time-monthsfork');
// A slight modification to the 'close' command to allow multiple tickets to be closed at once
module.exports = {
name: 'closeall',
description: 'Closes all currently open tickets older than a specified time length',
usage: '[time]',
aliases: ['ca'],
example: 'closeall 1mo 1w',
args: false,
disabled: !config.commands.closeall.enabled,
async execute(client, message, args, log, {
config,
Ticket
}) {
const guild = client.guilds.cache.get(config.guild);
if (!message.member.roles.cache.has(config.staff_role))
return message.channel.send(
new MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('❌ **No permission**')
.setDescription('You do not have permission to use this command as you are not a staff member.')
.addField('Usage', `\`${config.prefix}${this.name}${' ' + this.usage}\`\n`)
.addField('Help', `Type \`${config.prefix}help ${this.name}\` for more information`)
.setFooter(guild.name, guild.iconURL())
);
let tickets;
if (args.length > 0) {
let time, maxDate;
let timestamp = args.join(' ');
try {
time = toTime(timestamp).milliseconds();
maxDate = new Date(Date.now() - time);
} catch (error) {
return message.channel.send(
new MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('❌ **Invalid Timestamp**')
.setDescription(`The timestamp that you specified, \`${timestamp}\`, was invalid.`)
.addField('Usage', `\`${config.prefix}${this.name}${' ' + this.usage}\`\n`)
.addField('Help', `Type \`${config.prefix}help ${this.name}\` for more information`)
.setFooter(guild.name, guild.iconURL())
);
}
tickets = await Ticket.findAndCountAll({
where: {
open: true,
updatedAt: {
[Op.lte]: maxDate,
}
},
});
} else {
tickets = await Ticket.findAndCountAll({
where: {
open: true,
},
});
}
if (tickets.count === 0)
return message.channel.send(
new MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.display)
.setTitle('❌ **No open tickets**')
.setDescription('There are no open tickets to close.')
.setFooter(guild.name, guild.iconURL())
);
log.info(`Found ${tickets.count} open tickets`);
if (config.commands.close.confirmation) {
let success;
let pre = config.transcripts.text.enabled || config.transcripts.web.enabled
? `You will be able to view an archived version of each ticket later with \`${config.prefix}transcript <id>\``
: '';
let confirm = await message.channel.send(
new MessageEmbed()
.setColor(config.colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle(`❔ Are you sure you want to close **${tickets.count}** tickets?`)
.setDescription(`${pre}\n**React with ✅ to confirm.**`)
.setFooter(guild.name + ' | Expires in 15 seconds', guild.iconURL())
);
await confirm.react('✅');
const collector = confirm.createReactionCollector(
(reaction, user) => reaction.emoji.name === '✅' && user.id === message.author.id, {
time: 15000,
});
collector.on('collect', async () => {
message.channel.send(
new MessageEmbed()
.setColor(config.colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle(`**\`${tickets.count}\` tickets closed**`)
.setDescription(`**\`${tickets.count}\`** tickets closed by ${message.author}`)
.setFooter(guild.name, guild.iconURL())
);
confirm.reactions.removeAll();
confirm.edit(
new MessageEmbed()
.setColor(config.colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle(`✅ ** \`${tickets.count}\` tickets closed**`)
.setDescription('The channels will be automatically deleted in a few seconds, once the contents have been archived.')
.setFooter(guild.name, guild.iconURL())
);
message.delete({
timeout: 5000,
}).then(() => confirm.delete());
success = true;
closeAll();
});
collector.on('end', () => {
if (!success) {
confirm.reactions.removeAll();
confirm.edit(
new MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('❌ **Expired**')
.setDescription('You took too long to react; confirmation failed.')
.setFooter(guild.name, guild.iconURL()));
message.delete({
timeout: 10000
}).then(() => confirm.delete());
}
});
} else {
closeAll();
}
async function closeAll() {
tickets.rows.forEach(async ticket => {
let users = [];
if (config.transcripts.text.enabled || config.transcripts.web.enabled) {
let {
channel,
id,
creator
} = ticket;
let user = await client.users.fetch(creator);
let paths = {
text: join(__dirname, `../../user/transcripts/text/${channel}.txt`),
log: join(__dirname, `../../user/transcripts/raw/${channel}.log`),
json: join(__dirname, `../../user/transcripts/raw/entities/${channel}.json`)
};
if (user) {
let dm;
try {
dm = user.dmChannel || await user.createDM();
} catch (e) {
log.warn(`Could not create DM channel with ${user.tag}`);
}
let res = {};
const embed = new MessageEmbed()
.setColor(config.colour)
.setAuthor(message.author.username)
.setTitle(`Ticket ${id}`)
.setFooter(guild.name, guild.iconURL());
if (fs.existsSync(paths.text)) {
embed.addField('Text Transcript', 'See attachment');
res.files = [{
attachment: paths.text,
name: `ticket-${id}-${channel}.txt`
}];
}
if (fs.existsSync(paths.log) && fs.existsSync(paths.json)) {
let data = JSON.parse(fs.readFileSync(paths.json));
data.entities.users.forEach(u => users.push(u));
embed.addField('Web archive', await archive.export(Ticket, channel));
}
res.embed = embed;
try {
if (config.commands.close.send_transcripts) dm.send(res);
if (config.transcripts.channel.length > 1) client.channels.cache.get(config.transcripts.channel).send(res);
} catch (e) {
message.channel.send('❌ Couldn\'t send DM or transcript log message');
}
}
await Ticket.update({
open: false,
}, {
where: {
id,
}
});
log.info(log.format(`${message.author.tag} closed ticket &7${id}&f`));
client.channels.fetch(channel)
.then(c => c.delete()
.then(o => log.info(`Deleted channel with name: '#${o.name}' <${o.id}>`))
.catch(e => log.error(e)))
.catch(e => log.error(e));
if (config.logs.discord.enabled) {
let embed = new MessageEmbed()
.setColor(config.colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle(`${tickets.count} ${plural('ticket', tickets.count)} closed (${config.prefix}closeall)`)
.addField('Closed by', message.author, true)
.setFooter(guild.name, guild.iconURL())
.setTimestamp();
if (users.length > 1)
embed.addField('Members', users.map(u => `<@${u}>`).join('\n'));
client.channels.cache.get(config.logs.discord.channel).send(embed);
}
}
});
}
},
};

View File

@ -1,185 +0,0 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const {
MessageEmbed
} = require('discord.js');
const fs = require('fs');
const { join } = require('path');
module.exports = {
name: 'delete',
description: 'Delete a ticket. Similar to closing a ticket, but does not save transcript or archives.',
usage: '[ticket]',
aliases: ['del'],
example: 'delete #ticket-17',
args: false,
async execute(client, message, _args, log, {
config,
Ticket
}) {
const guild = client.guilds.cache.get(config.guild);
const notTicket = new MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('❌ **This isn\'t a ticket channel**')
.setDescription('Use this command in the ticket channel you want to delete, 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(guild.name, guild.iconURL());
let ticket;
let channel = message.mentions.channels.first();
// || client.channels.resolve(await Ticket.findOne({ where: { id: args[0] } }).channel) // channels.fetch()
if (!channel) {
channel = message.channel;
ticket = await Ticket.findOne({
where: {
channel: channel.id
}
});
if (!ticket) return channel.send(notTicket);
} else {
ticket = await Ticket.findOne({
where: {
channel: channel.id
}
});
if (!ticket) {
notTicket
.setTitle('❌ **Channel is not a ticket**')
.setDescription(`${channel} is not a ticket channel.`);
return message.channel.send(notTicket);
}
}
if (message.author.id !== ticket.creator && !message.member.roles.cache.has(config.staff_role))
return channel.send(
new MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('❌ **No permission**')
.setDescription(`You don't have permission to delete ${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(guild.name, guild.iconURL())
);
if (config.commands.delete.confirmation) {
let success;
let confirm = await message.channel.send(
new MessageEmbed()
.setColor(config.colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('❔ Are you sure?')
.setDescription(
`:warning: This action is **irreversible**, the ticket will be completely removed from the database.
You will **not** be able to view a transcript/archive of the channel later.
Use the \`close\` command instead if you don't want this behaviour.\n**React with ✅ to confirm.**`)
.setFooter(guild.name + ' | Expires in 15 seconds', guild.iconURL())
);
await confirm.react('✅');
const collector = confirm.createReactionCollector(
(r, u) => r.emoji.name === '✅' && u.id === message.author.id, {
time: 15000
});
collector.on('collect', async () => {
if (channel.id !== message.channel.id)
channel.send(
new MessageEmbed()
.setColor(config.colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('**Ticket deleted**')
.setDescription(`Ticket deleted by ${message.author}`)
.setFooter(guild.name, guild.iconURL())
);
confirm.reactions.removeAll();
confirm.edit(
new MessageEmbed()
.setColor(config.colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle(`✅ **Ticket ${ticket.id} deleted**`)
.setDescription('The channel will be automatically deleted in a few seconds.')
.setFooter(guild.name, guild.iconURL())
);
if (channel.id !== message.channel.id)
message.delete({
timeout: 5000
}).then(() => confirm.delete());
success = true;
del();
});
collector.on('end', () => {
if (!success) {
confirm.reactions.removeAll();
confirm.edit(
new MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('❌ **Expired**')
.setDescription('You took too long to react; confirmation failed.')
.setFooter(guild.name, guild.iconURL()));
message.delete({
timeout: 10000
}).then(() => confirm.delete());
}
});
} else {
del();
}
async function del () {
let txt = join(__dirname, `../../user/transcripts/text/${ticket.get('channel')}.txt`),
raw = join(__dirname, `../../user/transcripts/raw/${ticket.get('channel')}.log`),
json = join(__dirname, `../../user/transcripts/raw/entities/${ticket.get('channel')}.json`);
if (fs.existsSync(txt)) fs.unlinkSync(txt);
if (fs.existsSync(raw)) fs.unlinkSync(raw);
if (fs.existsSync(json)) fs.unlinkSync(json);
// update database
ticket.destroy(); // remove ticket from database
// channel
channel.delete({
timeout: 5000
});
log.info(`${message.author.tag} deleted a ticket (#ticket-${ticket.id})`);
if (config.logs.discord.enabled) {
client.channels.cache.get(config.logs.discord.channel).send(
new MessageEmbed()
.setColor(config.colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('Ticket deleted')
.addField('Creator', `<@${ticket.creator}>`, true)
.addField('Deleted by', message.author, true)
.setFooter(guild.name, guild.iconURL())
.setTimestamp()
);
}
}
}
};

View File

@ -1,87 +0,0 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const { MessageEmbed } = require('discord.js');
module.exports = {
name: 'help',
description: 'Display help menu',
usage: '[command]',
aliases: ['command', 'commands'],
example: 'help new',
args: false,
execute(client, message, args, log, {config}) {
const guild = client.guilds.cache.get(config.guild);
const commands = Array.from(client.commands.values());
if (!args.length) {
let cmds = [];
for (let command of commands) {
if (command.hide || command.disabled) 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 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(guild.name, guild.iconURL())
).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 MessageEmbed()
.setColor(config.err_colour)
.setDescription(`❌ **Invalid command name** (\`${config.prefix}help\`)`)
);
const cmd = new 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
},
};

View File

@ -1,205 +0,0 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const { MessageEmbed } = require('discord.js');
const fs = require('fs');
const { join } = require('path');
const config = require(join(__dirname, '../../user/', require('../').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,
disabled: !config.commands.new.enabled,
async execute(client, message, args, log, {config, Ticket}) {
if (!config.commands.new.enabled) return; // stop if the command is disabled
const guild = client.guilds.cache.get(config.guild);
const supportRole = guild.roles.cache.get(config.staff_role);
if (!supportRole)
return message.channel.send(
new MessageEmbed()
.setColor(config.err_colour)
.setTitle('❌ **Error**')
.setDescription(`${config.name} has not been set up correctly. Could not find a 'support team' role with the id \`${config.staff_role}\``)
.setFooter(guild.name, 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, 30);
ticketList
.push(`<#${tickets.rows[t].channel}>: \`${desc}${desc.length > 30 ? '...' : ''}\``);
}
let m = await message.channel.send(
new MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle(`❌ **You already have ${tickets.count} or more open tickets**`)
.setDescription(`Use \`${config.prefix}close\` to close unneeded tickets.\n\n${ticketList.join(',\n')}`)
.setFooter(guild.name + ' | This message will be deleted in 15 seconds', 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 MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('❌ **Description too long**')
.setDescription('Please limit your ticket topic to less than 256 characters. A short sentence will do.')
.setFooter(guild.name, guild.iconURL())
);
}
else if (topic.length < 1) {
topic = config.tickets.default_topic.command;
}
let ticket = await Ticket.create({
channel: '',
creator: message.author.id,
open: true,
archived: false,
topic: topic
});
let name = 'ticket-' + ticket.get('id');
guild.channels.create(name, {
type: 'text',
topic: `${message.author} | ${topic}`,
parent: config.tickets.category,
permissionOverwrites: [{
id: guild.roles.everyone,
deny: ['VIEW_CHANNEL', 'SEND_MESSAGES']
},
{
id: client.user,
allow: ['VIEW_CHANNEL', 'SEND_MESSAGES', 'ATTACH_FILES', 'READ_MESSAGE_HISTORY']
},
{
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 MessageEmbed()
.setColor(config.colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('✅ **Ticket created**')
.setDescription(`Your ticket has been created: ${c}`)
.setFooter(client.user.username + ' | This message will be deleted in 15 seconds', client.user.displayAvatarURL())
);
setTimeout(async () => {
await message.delete();
await m.delete();
}, 15000);
// require('../modules/archive').create(client, c); // create files
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(join(__dirname, '../../user/images'));
await c.send({
files: [
join(__dirname, '../../user/images', images[Math.floor(Math.random() * images.length)])
]
});
}
let text = config.tickets.text
.replace(/{{ ?name ?}}/gmi, message.author.username)
.replace(/{{ ?(tag|mention) ?}}/gmi, message.author);
let w = await c.send(
new MessageEmbed()
.setColor(config.colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setDescription(text)
.addField('Topic', `\`${topic}\``)
.setFooter(guild.name, guild.iconURL())
);
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 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(guild.name, guild.iconURL())
.setTimestamp()
);
log.info(`${message.author.tag} created a new ticket (#${name})`);
}).catch(log.error);
},
};

View File

@ -1,65 +0,0 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const { MessageEmbed } = require('discord.js');
module.exports = {
name: 'panel',
description: 'Create or a panel widget in the channel the command is used in. Note that there can only be 1 panel.',
usage: '',
aliases: ['widget'],
args: false,
permission: 'MANAGE_GUILD',
async execute(client, message, _args, log, {config, Setting}) {
const guild = client.guilds.cache.get(config.guild);
let msgID = await Setting.findOne({ where: { key: 'panel_msg_id' } });
let chanID = await Setting.findOne({ where: { key: 'panel_chan_id' } });
let panel;
if (!chanID) {
chanID = await Setting.create({
key: 'panel_chan_id',
value: message.channel.id,
});
}
if (!msgID) {
msgID = await Setting.create({
key: 'panel_msg_id',
value: '',
});
} else {
try {
panel = await client.channels.cache.get(chanID.get('value')).messages.fetch(msgID.get('value')); // get old panel message
if (panel) {
panel.delete({ reason: 'Creating new panel/widget' }).then(() => log.info('Deleted old panel')).catch(e => log.warn(e)); // delete old panel
}
} catch (e) {
log.warn('Couldn\'t delete old panel');
}
}
message.delete();
panel = await message.channel.send(
new MessageEmbed()
.setColor(config.colour)
.setTitle(config.panel.title)
.setDescription(config.panel.description)
.setFooter(guild.name, guild.iconURL())
); // send new panel
let emoji = panel.guild.emojis.cache.get(config.panel.reaction) || config.panel.reaction;
panel.react(emoji); // add reaction
Setting.update({ value: message.channel.id }, { where: { key: 'panel_chan_id' }}); // update database
Setting.update({ value: panel.id }, { where: { key: 'panel_msg_id' }}); // update database
log.info(`${message.author.tag} created a panel widget`);
}
};

View File

@ -1,112 +0,0 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const { MessageEmbed } = require('discord.js');
module.exports = {
name: 'remove',
description: 'Remove a member from ticket channel',
usage: '<@member> [... #channel]',
aliases: ['none'],
example: 'remove @member from #ticket-23',
args: true,
async execute(client, message, args, log, {config, Ticket}) {
const guild = client.guilds.cache.get(config.guild);
const notTicket = new MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('❌ **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(guild.name, 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('❌ **Channel is not a ticket**')
.setDescription(`${channel} is not a ticket channel.`);
return message.channel.send(notTicket);
}
}
if (message.author.id !== ticket.creator && !message.member.roles.cache.has(config.staff_role)) {
return message.channel.send(
new MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('❌ **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(guild.name, guild.iconURL())
);
}
let member = guild.member(message.mentions.users.first() || guild.members.cache.get(args[0]));
if (!member || member.id === message.author.id || member.id === guild.me.id)
return message.channel.send(
new MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('❌ **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(guild.name, 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 MessageEmbed()
.setColor(config.colour)
.setAuthor(member.user.username, member.user.displayAvatarURL())
.setTitle('**Member removed**')
.setDescription(`${member} has been removed by ${message.author}`)
.setFooter(guild.name, guild.iconURL())
);
}
message.channel.send(
new MessageEmbed()
.setColor(config.colour)
.setAuthor(member.user.username, member.user.displayAvatarURL())
.setTitle('✅ **Member removed**')
.setDescription(`${member} has been removed from <#${ticket.channel}>`)
.setFooter(guild.name, guild.iconURL())
);
log.info(`${message.author.tag} removed a user from a ticket (#${message.channel.id})`);
} catch (error) {
log.error(error);
}
},
};

View File

@ -1,63 +0,0 @@
/**
*
* @name DiscordTickets
* @author iFusion for eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const { MessageEmbed } = require('discord.js');
module.exports = {
name: 'rename',
description: 'Rename a ticket channel',
usage: '<new name>',
aliases: ['none'],
example: 'rename important-ticket',
args: true,
async execute(client, message, args, {config, Ticket}) {
const guild = client.guilds.cache.get(config.guild);
let ticket = await Ticket.findOne({
where: {
channel: message.channel.id
}
});
if (!ticket) {
return message.channel.send(
new MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('❌ **This isn\'t a ticket channel**')
.setDescription('Use this command in the ticket channel you want to rename.')
.addField('Usage', `\`${config.prefix}${this.name} ${this.usage}\`\n`)
.addField('Help', `Type \`${config.prefix}help ${this.name}\` for more information`)
.setFooter(guild.name, guild.iconURL())
);
}
if (!message.member.roles.cache.has(config.staff_role))
return message.channel.send(
new MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('❌ **No permission**')
.setDescription('You don\'t have permission to rename this channel 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(guild.name, guild.iconURL())
);
message.channel.setName(args.join('-')); // new channel name
message.channel.send(
new MessageEmbed()
.setColor(config.colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('✅ **Ticket updated**')
.setDescription('The name has been changed.')
.setFooter(client.user.username, client.user.displayAvatarURL())
);
}
};

View File

@ -1,34 +0,0 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const { MessageEmbed } = require('discord.js');
module.exports = {
name: 'stats',
description: 'View ticket stats.',
usage: '',
aliases: ['data', 'statistics'],
args: false,
async execute(client, message, _args, {config, Ticket}) {
const guild = client.guilds.cache.get(config.guild);
let open = await Ticket.count({ where: { open: true } });
let closed = await Ticket.count({ where: { open: false } });
message.channel.send(
new MessageEmbed()
.setColor(config.colour)
.setTitle(':bar_chart: Statistics')
.addField('Open tickets', open, true)
.addField('Closed tickets', closed, true)
.addField('Total tickets', open + closed, true)
.setFooter(guild.name, guild.iconURL())
);
}
};

View File

@ -1,116 +0,0 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const { MessageEmbed } = require('discord.js');
const fs = require('fs');
const { join } = require('path');
module.exports = {
name: 'tickets',
description: 'List your recent tickets to access transcripts / archives.',
usage: '[@member]',
aliases: ['list'],
args: false,
async execute(client, message, args, {config, Ticket}) {
const guild = client.guilds.cache.get(config.guild);
const supportRole = guild.roles.cache.get(config.staff_role);
if (!supportRole) {
return message.channel.send(
new MessageEmbed()
.setColor(config.err_colour)
.setTitle('❌ **Error**')
.setDescription(`${config.name} has not been set up correctly. Could not find a 'support team' role with the id \`${config.staff_role}\``)
.setFooter(guild.name, guild.iconURL())
);
}
let context = 'self';
let user = message.mentions.users.first() || guild.members.cache.get(args[0]);
if (user) {
if (!message.member.roles.cache.has(config.staff_role)) {
return message.channel.send(
new MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('❌ **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(guild.name, guild.iconURL())
);
}
context = 'staff';
} else user = message.author;
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 MessageEmbed()
.setColor(config.colour)
.setAuthor(user.username, user.displayAvatarURL())
.setTitle(`${context === 'self' ? 'Your' : user.username + '\'s'} tickets`)
.setFooter(guild.name + ' | This message will be deleted in 60 seconds', 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 = '';
let c = closedTickets.rows[t].channel;
if (config.transcripts.web.enabled || fs.existsSync(join(__dirname, `../../user/transcripts/text/${c}.txt`))) {
transcript = `\n> Type \`${config.prefix}transcript ${closedTickets.rows[t].id}\` to view.`;
}
closed.push(`> **#${closedTickets.rows[t].id}**: \`${desc}${desc.length > 20 ? '...' : ''}\`${transcript}`);
}
let pre = context === 'self' ? 'You have' : user.username + ' 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);
message.delete({timeout: 15000});
let channel;
try {
channel = message.author.dmChannel || await message.author.createDM();
message.channel.send('Sent to DM').then(msg => msg.delete({timeout: 15000}));
} catch (e) {
channel = message.channel;
}
let m = await channel.send(embed);
m.delete({timeout: 60000});
},
};

View File

@ -1,71 +0,0 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const { MessageEmbed } = require('discord.js');
module.exports = {
name: 'topic',
description: 'Edit a ticket topic',
usage: '<topic>',
aliases: ['edit'],
example: 'topic need help error',
args: true,
async execute(client, message, args, {config, Ticket}) {
const guild = client.guilds.cache.get(config.guild);
let ticket = await Ticket.findOne({
where: {
channel: message.channel.id
}
});
if (!ticket) {
return message.channel.send(
new MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('❌ **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(guild.name, guild.iconURL())
);
}
let topic = args.join(' ');
if (topic.length > 256) {
return message.channel.send(
new MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('❌ **Description too long**')
.setDescription('Please limit your ticket topic to less than 256 characters. A short sentence will do.')
.setFooter(guild.name, guild.iconURL())
);
}
message.channel.setTopic(`<@${ticket.creator}> | ` + topic);
Ticket.update({
topic: topic
}, {
where: {
channel: message.channel.id
}
});
message.channel.send(
new MessageEmbed()
.setColor(config.colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('✅ **Ticket updated**')
.setDescription('The topic has been changed.')
.setFooter(client.user.username, client.user.displayAvatarURL())
);
}
};

View File

@ -1,95 +0,0 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const fs = require('fs');
const { join } = require('path');
const {
MessageEmbed
} = require('discord.js');
module.exports = {
name: 'transcript',
description: 'Download a transcript',
usage: '<ticket-id>',
aliases: ['archive', 'download'],
example: 'transcript 57',
args: true,
async execute(client, message, args, {config, Ticket}) {
const guild = client.guilds.cache.get(config.guild);
const id = args[0];
let ticket = await Ticket.findOne({
where: {
id: id,
open: false
}
});
if (!ticket) {
return message.channel.send(
new MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('❌ **Unknown ticket**')
.setDescription('Couldn\'t find a closed ticket with that ID')
.setFooter(guild.name, guild.iconURL())
);
}
if (message.author.id !== ticket.creator && !message.member.roles.cache.has(config.staff_role)) {
return message.channel.send(
new MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('❌ **No permission**')
.setDescription(`You don't have permission to view ticket ${id} as it does not belong to you and you are not staff.`)
.setFooter(guild.name, guild.iconURL())
);
}
let res = {};
const embed = new MessageEmbed()
.setColor(config.colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle(`Ticket ${id}`)
.setFooter(guild.name, guild.iconURL());
let file = `../../user/transcripts/text/${ticket.channel}.txt`;
if (fs.existsSync(join(__dirname, file))) {
embed.addField('Text transcript', 'See attachment');
res.files = [
{
attachment: join(__dirname, file),
name: `ticket-${id}-${ticket.channel}.txt`
}
];
}
const BASE_URL = config.transcripts.web.server;
if (config.transcripts.web.enabled) embed.addField('Web archive', `${BASE_URL}/${ticket.creator}/${ticket.channel}`);
if (embed.fields.length < 1) embed.setDescription(`No text transcripts or archive data exists for ticket ${id}`);
res.embed = embed;
let channel;
try {
channel = message.author.dmChannel || await message.author.createDM();
} catch (e) {
channel = message.channel;
}
channel.send(res).then(m => {
if (channel.id === message.channel.id) m.delete({timeout: 15000});
});
message.delete({timeout: 1500});
}
};

View File

@ -1,87 +0,0 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const { MessageEmbed } = require('discord.js');
module.exports = {
name: 'transfer',
description: 'Transfer ownership of a ticket channel',
usage: '<@member>',
aliases: ['none'],
example: 'transfer @user',
args: true,
async execute(client, message, args, { config, Ticket }) {
const guild = client.guilds.cache.get(config.guild);
let ticket = await Ticket.findOne({
where: {
channel: message.channel.id
}
});
if (!ticket) {
return message.channel.send(
new MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('❌ **This isn\'t a ticket channel**')
.setDescription('Use this command in the ticket channel you want to change owner.')
.addField('Usage', `\`${config.prefix}${this.name} ${this.usage}\`\n`)
.addField('Help', `Type \`${config.prefix}help ${this.name}\` for more information`)
.setFooter(guild.name, guild.iconURL())
);
}
if (!message.member.roles.cache.has(config.staff_role))
return message.channel.send(
new MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('❌ **No permission**')
.setDescription('You don\'t have permission to change ownership of this channel 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(guild.name, guild.iconURL())
);
let member = guild.member(message.mentions.users.first() || guild.members.cache.get(args[0]));
if (!member) {
return message.channel.send(
new MessageEmbed()
.setColor(config.err_colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('❌ **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(guild.name, guild.iconURL())
);
}
message.channel.setTopic(`${member} | ${ticket.topic}`);
Ticket.update({
creator: member.user.id
}, {
where: {
channel: message.channel.id
}
});
message.channel.send(
new MessageEmbed()
.setColor(config.colour)
.setAuthor(message.author.username, message.author.displayAvatarURL())
.setTitle('✅ **Ticket transferred**')
.setDescription(`Ownership of this ticket has been transferred to ${member}.`)
.setFooter(client.user.username, client.user.displayAvatarURL())
);
}
};

View File

@ -1,14 +0,0 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
module.exports = {
event: 'debug',
execute(_client, log, [e]) {
log.debug(e);
}
};

View File

@ -1,14 +0,0 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
module.exports = {
event: 'error',
execute(_client, log, [e]) {
log.error(e);
}
};

View File

@ -1,113 +0,0 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const { Collection, MessageEmbed } = require('discord.js');
const archive = require('../modules/archive');
module.exports = {
event: 'message',
async execute(client, log, [message], {config, Ticket, Setting}) {
const guild = client.guilds.cache.get(config.guild);
if (message.channel.type === 'dm' && !message.author.bot) {
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 **${guild}**.
Type \`${config.prefix}new\` on the server to create a new ticket.`);
} // stop here if is DM
/**
* Ticket transcripts
* (bots currently still allowed)
*/
let ticket = await Ticket.findOne({ where: { channel: message.channel.id } });
if (ticket) {
archive.add(message); // add message to archive
// Update the ticket updated at so closeall can get most recent
ticket.changed('updatedAt', true);
ticket.save();
}
if (message.author.bot || message.author.id === client.user.id) return; // goodbye bots
/**
* Command handler
* (no bots / self)
*/
const regex = new RegExp(`^(<@!?${client.user.id}>|\\${config.prefix.toLowerCase()})\\s*`);
if (!regex.test(message.content.toLowerCase())) return; // not a command
const [, prefix] = message.content.toLowerCase().match(regex);
const args = message.content.slice(prefix.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; // not an existing command
if (message.guild.id !== guild.id) return message.reply(`This bot can only be used within the "${guild}" server`); // not in this server
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 MessageEmbed()
.setColor(config.err_colour)
.setTitle('❌ No permission')
.setDescription(`**You do not have permission to use the \`${command.name}\` command** (requires \`${command.permission}\`).`)
.setFooter(guild.name, guild.iconURL())
);
}
if (command.args && !args.length) {
return message.channel.send(
new 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(guild.name, guild.iconURL())
);
}
if (!client.cooldowns.has(command.name)) client.cooldowns.set(command.name, new 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 MessageEmbed()
.setColor(config.err_colour)
.setDescription(`❌ Please wait ${timeLeft.toFixed(1)} second(s) before reusing the \`${command.name}\` command.`)
.setFooter(guild.name, guild.iconURL())
);
}
}
timestamps.set(message.author.id, now);
setTimeout(() => timestamps.delete(message.author.id), cooldownAmount);
try {
command.execute(client, message, args, log, {config, Ticket, Setting});
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(`❌ An error occurred whilst executing the \`${command.name}\` command.`);
}
}
};

View File

@ -1,46 +0,0 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const fs = require('fs');
const { join } = require('path');
module.exports = {
event: 'messageDelete',
async execute(_client, log, [message], {config, Ticket}) {
if (!config.transcripts.web.enabled) return;
if (message.partial) {
try {
await message.fetch();
} catch (err) {
log.warn('Failed to fetch deleted message');
log.error(err.message);
return;
}
}
let ticket = await Ticket.findOne({ where: { channel: message.channel.id } });
if (!ticket) return;
let path = `../../user/transcripts/raw/${message.channel.id}.log`;
let embeds = [];
for (let embed in message.embeds) embeds.push(message.embeds[embed].toJSON());
fs.appendFileSync(join(__dirname, path), JSON.stringify({
id: message.id,
author: message.author.id,
content: message.content, // do not use cleanContent!
time: message.createdTimestamp,
embeds: embeds,
attachments: [...message.attachments.values()],
edited: message.edits.length > 1,
deleted: true // delete the message
}) + '\n');
}
};

View File

@ -1,191 +0,0 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const { MessageEmbed } = require('discord.js');
const fs = require('fs');
const { join } = require('path');
module.exports = {
event: 'messageReactionAdd',
async execute(client, log, [r, u], {config, Ticket, Setting}) {
if (r.partial) {
try {
await r.fetch();
} catch (err) {
log.error(err);
return;
}
}
let panelID = await Setting.findOne({ where: { key: 'panel_msg_id' } });
if (!panelID) return;
if (r.message.id !== panelID.get('value')) return;
if (u.id === client.user.id) return;
if (r.emoji.name !== config.panel.reaction && r.emoji.id !== config.panel.reaction) return;
let channel = r.message.channel;
const supportRole = channel.guild.roles.cache.get(config.staff_role);
if (!supportRole) {
return channel.send(
new MessageEmbed()
.setColor(config.err_colour)
.setTitle('❌ **Error**')
.setDescription(`${config.name} has not been set up correctly. Could not find a 'support team' role with the id \`${config.staff_role}\``)
.setFooter(channel.guild.name, channel.guild.iconURL())
);
}
// everything is cool
await r.users.remove(u.id); // effectively cancel reaction
let tickets = await Ticket.findAndCountAll({
where: {
creator: u.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, 30);
ticketList
.push(`<#${tickets.rows[t].channel}>: \`${desc}${desc.length > 30 ? '...' : ''}\``);
}
let dm = u.dmChannel || await u.createDM();
try {
return dm.send(
new MessageEmbed()
.setColor(config.err_colour)
.setAuthor(u.username, u.displayAvatarURL())
.setTitle(`❌ **You already have ${tickets.count} or more open tickets**`)
.setDescription(`Use \`${config.prefix}close\` in a server channel to close unneeded tickets.\n\n${ticketList.join(',\n')}`)
.setFooter(channel.guild.name, channel.guild.iconURL())
);
} catch (e) {
let m = await channel.send(
new MessageEmbed()
.setColor(config.err_colour)
.setAuthor(u.username, u.displayAvatarURL())
.setTitle(`❌ **You already have ${tickets.count} or more open tickets**`)
.setDescription(`Use \`${config.prefix}close\` to close unneeded tickets.\n\n${ticketList.join(',\n')}`)
.setFooter(channel.guild.name + ' | This message will be deleted in 15 seconds', channel.guild.iconURL())
);
return m.delete({ timeout: 15000 });
}
}
let topic = config.tickets.default_topic.command;
let ticket = await Ticket.create({
channel: '',
creator: u.id,
open: true,
archived: false,
topic: topic
});
let name = 'ticket-' + ticket.id;
channel.guild.channels.create(name, {
type: 'text',
topic: `${u} | ${topic}`,
parent: config.tickets.category,
permissionOverwrites: [{
id: channel.guild.roles.everyone,
deny: ['VIEW_CHANNEL', 'SEND_MESSAGES']
},
{
id: client.user,
allow: ['VIEW_CHANNEL', 'SEND_MESSAGES', 'ATTACH_FILES', 'READ_MESSAGE_HISTORY']
},
{
id: channel.guild.member(u),
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 (panel reaction)'
}).then(async c => {
Ticket.update({
channel: c.id
}, {
where: {
id: ticket.id
}
});
// require('../modules/archive').create(client, c); // create files
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 + `${u} has created a new ticket`);
if (config.tickets.send_img) {
const images = fs.readdirSync(join(__dirname, '../../user/images'));
await c.send({
files: [
join(__dirname, '../../user/images', images[Math.floor(Math.random() * images.length)])
]
});
}
let text = config.tickets.text
.replace(/{{ ?name ?}}/gmi, u.username)
.replace(/{{ ?(tag|mention) ?}}/gmi, u);
let w = await c.send(
new MessageEmbed()
.setColor(config.colour)
.setAuthor(u.username, u.displayAvatarURL())
.setDescription(text)
.addField('Topic', `\`${topic}\``)
.setFooter(channel.guild.name, channel.guild.iconURL())
);
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 MessageEmbed()
.setColor(config.colour)
.setAuthor(u.username, u.displayAvatarURL())
.setTitle('New ticket (via panel)')
.setDescription(`\`${topic}\``)
.addField('Creator', u, true)
.addField('Channel', c, true)
.setFooter(channel.guild.name, channel.guild.iconURL())
.setTimestamp()
);
log.info(`${u.tag} created a new ticket (#${name}) via panel`);
}).catch(log.error);
}
};

View File

@ -1,53 +0,0 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const fs = require('fs');
const { join } = require('path');
module.exports = {
event: 'messageUpdate',
async execute(_client, log, [o, n], {config, Ticket}) {
if (!config.transcripts.web.enabled) return;
if (o.partial) {
try {
await o.fetch();
} catch (err) {
log.error(err);
return;
}
}
if (n.partial) {
try {
await n.fetch();
} catch (err) {
log.error(err);
return;
}
}
let ticket = await Ticket.findOne({ where: { channel: n.channel.id } });
if (!ticket) return;
let path = `../../user/transcripts/raw/${n.channel.id}.log`;
let embeds = [];
for (let embed in n.embeds) embeds.push({ ...n.embeds[embed] });
fs.appendFileSync(join(__dirname, path), JSON.stringify({
id: n.id,
author: n.author.id,
content: n.content, // do not use cleanContent!
time: n.createdTimestamp,
embeds: embeds,
attachments: [...n.attachments.values()],
edited: true
}) + '\n');
}
};

View File

@ -1,15 +0,0 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
module.exports = {
event: 'rateLimit',
execute(_client, log, [limit]) {
log.warn('Rate-limited! (Enable debug mode in config for details)');
log.debug(limit);
}
};

View File

@ -1,41 +0,0 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const Logger = require('leekslazylogger');
const log = new Logger();
const config = require('../../user/' + require('../').config);
module.exports = {
event: 'ready',
execute(client, log) {
log.success(`Authenticated as ${client.user.tag}`);
const updatePresence = () => {
const presence = config.presences[Math.floor(Math.random() * config.presences.length)];
let activity = presence.activity + config.append_presence;
activity = activity.replace(/%s/g, config.prefix);
client.user.setPresence({
activity: {
name: activity,
type: presence.type.toUpperCase()
}
}).catch(log.error);
log.debug(`Updated presence: ${activity} ${presence.type}`);
};
updatePresence();
setInterval(() => {
updatePresence();
}, 60000);
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');
}
};

View File

@ -1,14 +0,0 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
module.exports = {
event: 'warn',
execute(_client, log, [e]) {
log.warn(e);
}
};

View File

@ -1,174 +1,22 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
* DiscordTickets Copyright (C) 2020 Isaac "eartharoid" Saunders
* This program comes with ABSOLUTELY NO WARRANTY.
* This is free software, and you are welcome to redistribute it
* under certain conditions. See the included LICENSE file for details.
*
* DiscordTickets
* 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/discordtickets
* @description An open-source & self-hosted Discord bot for ticket management.
* @copyright 2021 Isaac Saunders
* @license GNU-GPLv3
*/
const version = Number(process.version.split('.')[0].replace('v', ''));
if (version < 12) return console.log('Please upgrade to Node v12 or higher');
const fs = require('fs');
const { join } = require('path');
let dev = fs.existsSync(join(__dirname, '../user/dev.env')) && fs.existsSync(join(__dirname, '../user/dev.config.js'));
require('dotenv').config({ path: join(__dirname, '../user/', dev ? 'dev.env' : '.env') });
module.exports.config = dev ? 'dev.config.js' : 'config.js';
const config = require(join(__dirname, '../user/', module.exports.config));
const Discord = require('discord.js');
const client = new Discord.Client({
autoReconnect: true,
partials: ['MESSAGE', 'CHANNEL', 'REACTION'],
});
client.events = new Discord.Collection();
client.commands = new Discord.Collection();
client.cooldowns = new Discord.Collection();
const utils = require('./modules/utils');
const leeks = require('leeks.js');
require('./modules/banner')(leeks); // big coloured text thing
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
});
require('./modules/updater')(); // check for updates
/**
* storage
*/
const { Sequelize, Model, DataTypes } = require('sequelize');
let sequelize;
switch (config.storage.type) {
case '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
});
break;
case 'mariadb':
log.info('Connecting to MariaDB database...');
sequelize = new Sequelize(process.env.DB_NAME, process.env.DB_USER, process.env.DB_PASS, {
dialect: 'mariadb',
host: process.env.DB_HOST,
logging: log.debug
});
break;
case 'postgre':
log.info('Connecting to PostgreSQL database...');
sequelize = new Sequelize(process.env.DB_NAME, process.env.DB_USER, process.env.DB_PASS, {
dialect: 'postgres',
host: process.env.DB_HOST,
logging: log.debug
});
break;
case 'microsoft':
log.info('Connecting to Microsoft SQL Server database...');
sequelize = new Sequelize(process.env.DB_NAME, process.env.DB_USER, process.env.DB_PASS, {
dialect: 'mssql',
host: process.env.DB_HOST,
logging: log.debug
});
break;
default:
log.info('Using SQLite storage');
sequelize = new Sequelize({
dialect: 'sqlite',
storage: join(__dirname, '../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'
});
class Setting extends Model {}
Setting.init({
key: DataTypes.STRING,
value: DataTypes.STRING,
}, {
sequelize,
modelName: 'setting'
});
Ticket.sync();
Setting.sync();
/**
* event loader
*/
const events = fs.readdirSync(join(__dirname, '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, Setting));
client.on(event.event, (e1, e2) => client.events.get(event.event).execute(client, log, [e1, e2], {config, Ticket, Setting}));
log.console(log.format(`> Loaded &7${event.event}&f event`));
}
/**
* command loader
*/
const commands = fs.readdirSync(join(__dirname, '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`);
const one_day = 1000 * 60 * 60 * 24;
const txt = '../user/transcripts/text';
const clean = () => {
const files = fs.readdirSync(join(__dirname, txt)).filter(file => file.endsWith('.txt'));
let total = 0;
for (const file of files) {
let diff = (new Date() - new Date(fs.statSync(join(__dirname, txt, file)).mtime));
if (Math.floor(diff / one_day) > config.transcripts.text.keep_for) {
fs.unlinkSync(join(__dirname, txt, file));
total++;
}
}
if (total > 0) log.info(`Deleted ${total} old text ${utils.plural('transcript', total)}`);
};
if (config.transcripts.text.enabled) {
clean();
setInterval(clean, one_day);
}
process.on('unhandledRejection', error => {
log.warn('An error was not caught');
log.warn(`Uncaught ${error.name}: ${error.message}`);
log.error(error);
});
client.login(process.env.TOKEN);

View File

@ -1,161 +0,0 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const Logger = require('leekslazylogger');
const log = new Logger();
const Readlines = require('n-readlines');
const fs = require('fs');
const { join } = require('path');
const dtf = require('@eartharoid/dtf');
const config = require('../../user/' + require('../').config);
const fetch = require('node-fetch');
module.exports.add = (message) => {
if (message.type !== 'DEFAULT') return;
if (config.transcripts.text.enabled) { // text transcripts
let path = `../../user/transcripts/text/${message.channel.id}.txt`,
time = dtf('HH:mm:ss n_D MMM YY', message.createdAt),
msg = message.cleanContent;
message.attachments.each(a => msg += '\n' + a.url);
let string = `[${time}] [${message.author.tag}] :> ${msg}`;
fs.appendFileSync(join(__dirname, path), string + '\n');
}
if (config.transcripts.web.enabled) { // web archives
let raw = `../../user/transcripts/raw/${message.channel.id}.log`,
json = `../../user/transcripts/raw/entities/${message.channel.id}.json`;
let embeds = [];
for (let embed in message.embeds) embeds.push({ ...message.embeds[embed] });
// message
fs.appendFileSync(join(__dirname, raw), JSON.stringify({
id: message.id,
author: message.author.id,
content: message.content, // do not use cleanContent, we want to include the mentions!
time: message.createdTimestamp,
embeds: embeds,
attachments: [...message.attachments.values()]
}) + '\n');
// channel entities
if (!fs.existsSync(join(__dirname, json)))
fs.writeFileSync(join(__dirname, json), JSON.stringify({
entities: {
users: {},
channels: {},
roles: {}
}
})); // create new
let data = JSON.parse(fs.readFileSync(join(__dirname, json)));
// if (!data.entities.users[message.author.id])
data.entities.users[message.author.id] = {
avatar: message.author.displayAvatarURL(),
username: message.author.username,
discriminator: message.author.discriminator,
displayName: message.member.displayName,
color: message.member.displayColor === 0 ? null : message.member.displayColor,
badge: message.author.bot ? 'bot' : null
};
// mentions.users
message.mentions.members.each(m => data.entities.users[m.id] = { // for mentions
avatar: m.user.displayAvatarURL(),
username: m.user.username,
discriminator: m.user.discriminator,
displayName: m.user.displayName || m.user.username,
color: m.displayColor === 0 ? null : m.displayColor,
badge: m.user.bot ? 'bot' : null
});
message.mentions.channels.each(c => data.entities.channels[c.id] = { // for mentions only
name: c.name
});
message.mentions.roles.each(r => data.entities.roles[r.id] = { // for mentions only
name: r.name,
color: r.color === 0 ? 7506394 : r.color
});
fs.writeFileSync(join(__dirname, json), JSON.stringify(data));
}
};
module.exports.export = (Ticket, channel) => new Promise((resolve, reject) => {
(async () => {
let ticket = await Ticket.findOne({
where: {
channel: channel.id
}
});
let raw = `../../user/transcripts/raw/${channel.id}.log`,
json = `../../user/transcripts/raw/entities/${channel.id}.json`;
if (!config.transcripts.web.enabled || !fs.existsSync(join(__dirname, raw)) || !fs.existsSync(join(__dirname, json))) return reject(false);
let data = JSON.parse(fs.readFileSync(join(__dirname, json)));
data.ticket = {
id: ticket.id,
name: channel.name,
creator: ticket.creator,
channel: channel.id,
topic: channel.topic
};
data.messages = [];
let line;
const lineByLine = new Readlines(join(__dirname, raw));
// eslint-disable-next-line no-cond-assign
while (line = lineByLine.next()) {
let message = JSON.parse(line.toString('utf8'));
let index = data.messages.findIndex(m => m.id === message.id);
if (index === -1) data.messages.push(message);
else data.messages[index] = message;
}
let endpoint = config.transcripts.web.server;
if (endpoint[endpoint.length - 1] === '/') endpoint = endpoint.slice(0, -1);
endpoint += `/${data.ticket.creator}/${data.ticket.channel}/?key=${process.env.ARCHIVES_KEY}`;
fetch(encodeURI(endpoint), {
method: 'post',
body: JSON.stringify(data),
headers: { 'Content-Type': 'application/json' },
})
.then(res => res.json())
.then(res => {
if (res.status !== 200) {
log.warn(res);
return resolve(new Error(`${res.status} (${res.message})`));
}
log.success(`Uploaded ticket #${ticket.id} archive to server`);
fs.unlinkSync(join(__dirname, raw));
fs.unlinkSync(join(__dirname, json));
resolve(res.url);
}).catch(e => {
log.warn(e);
return resolve(e);
});
})();
});

View File

@ -1,34 +0,0 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const { version, homepage } = require('../../package.json');
const link = require('terminal-link');
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 + '\n'));
console.log(leeks.colours.cyanBright(`Please ${link('donate', 'https://ko-fi.com/eartharoid')} if you find this bot useful`));
console.log('\n\n');
};

View File

@ -1,41 +0,0 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
const Logger = require('leekslazylogger');
const log = new Logger();
const fetch = require('node-fetch');
const config = require('../../user/' + require('../').config);
let {version} = require('../../package.json');
version = 'v' + version;
const boxen = require('boxen');
const link = require('terminal-link');
module.exports = async () => {
if (!config.updater) return;
const json = await (await fetch('https://api.github.com/repos/eartharoid/DiscordTickets/releases')).json();
const update = json[0];
let notice = [];
if (version !== update.tag_name) {
log.notice(log.f(`There is an update available for Discord Tickets (${version} -> ${update.tag_name})`));
notice.push(`&6You are currently using &c${version}&6, the latest is &a${update.tag_name}&6.`);
notice.push(`&6Download "&f${update.name}&6" from`);
notice.push(link('&6the GitHub releases page', 'https://github.com/eartharoid/DiscordTickets/releases/'));
console.log(
boxen(log.f(notice.join('\n')), {
padding: 1,
margin: 1,
align: 'center',
borderColor: 'yellow',
borderStyle: 'round'
})
);
}
};

View File

@ -1,18 +0,0 @@
/**
*
* @name DiscordTickets
* @author eartharoid <contact@eartharoid.me>
* @license GNU-GPLv3
*
*/
module.exports = {
/**
* @description Appends 's' to a word if plural number
* @param {string} word - singular version of word
* @param {number} num - integer
*/
plural(word, num) {
return num !== 1 ? word + 's' : word;
}
};

View File

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

View File

@ -10,110 +10,48 @@
* Quick Start
* ---------------------
*
* > For detailed instructions, visit the GitHub repository and read the documentation:
* https://github.com/eartharoid/DiscordTickets/wiki
*
* > IMPORTANT: Also edit the TOKEN in 'user/.env'
* > For detailed instructions, visit the documentation: https://eartharoid.github.io/discordtickets
*
* ---------------------
* Support
* ---------------------
*
* > Information: https://github.com/eartharoid/DiscordTickets/#readme
* > Discord Support Server: https://go.eartharoid.me/discord
* > Wiki: https://github.com/eartharoid/DiscordTickets/wiki
* > Discord support server: https://go.eartharoid.me/discord
* > Wiki: https://eartharoid.github.io/discordtickets
*
* ###############################################################################################
*/
module.exports = {
prefix: '-',
name: 'DiscordTickets',
presences: [
{
activity: '%snew',
type: 'PLAYING'
},
{
activity: 'with tickets',
type: 'PLAYING'
},
{
activity: 'for new tickets',
type: 'WATCHING'
}
],
append_presence: ' | %shelp',
colour: '#009999',
err_colour: 'RED',
cooldown: 3,
guild: '', // ID of your guild (REQUIRED)
staff_role: '', // ID of your Support Team role (REQUIRED)
tickets: {
category: '', // ID of your tickets category (REQUIRED)
send_img: true,
ping: 'here',
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! :)`,
pin: false,
max: 3,
default_topic: {
command: 'No topic given',
panel: 'Created via panel'
}
},
commands: {
close: {
confirmation: true,
send_transcripts: true
},
delete: {
confirmation: true
},
new: {
enabled: true
},
closeall: {
enabled: true,
},
},
transcripts: {
text: {
enabled: true,
keep_for: 90,
},
web: {
enabled: false,
server: 'https://tickets.example.com',
},
channel: '' // ID of your archives channel
},
panel: {
title: 'Support Tickets',
description: 'Need help? No problem! React to this panel to create a new support ticket so our Support Team can assist you.',
reaction: '🧾'
},
storage: {
type: 'sqlite'
},
logs: {
files: {
enabled: true,
keep_for: 7
},
discord: {
enabled: false,
channel: '' // ID of your log channel
}
portal: {
enabled: true,
host: 'https://tickets.eartharoid.me'
},
presences: [
{
activity: '%snew | %shelp',
type: 'PLAYING'
},
{
activity: 'with tickets | %shelp',
type: 'PLAYING'
},
{
activity: 'for new tickets | %shelp',
type: 'WATCHING'
}
],
defaults: {
prefix: '-',
colour: '#009999',
},
logs: {
enabled: true,
keep_for: 30
},
debug: false,
updater: true
update_notice: true,
};

57
user/example.config.js Normal file
View File

@ -0,0 +1,57 @@
/**
* ###############################################################################################
* ____ _ _____ _ _
* | _ \ (_) ___ ___ ___ _ __ __| | |_ _| (_) ___ | | __ ___ | |_ ___
* | | | | | | / __| / __| / _ \ | '__| / _` | | | | | / __| | |/ / / _ \ | __| / __|
* | |_| | | | \__ \ | (__ | (_) | | | | (_| | | | | | | (__ | < | __/ | |_ \__ \
* |____/ |_| |___/ \___| \___/ |_| \__,_| |_| |_| \___| |_|\_\ \___| \__| |___/
*
* ---------------------
* Quick Start
* ---------------------
*
* > For detailed instructions, visit the documentation: https://eartharoid.github.io/discordtickets
*
* ---------------------
* Support
* ---------------------
*
* > Discord support server: https://go.eartharoid.me/discord
* > Wiki: https://eartharoid.github.io/discordtickets
*
* ###############################################################################################
*/
module.exports = {
storage: {
type: 'sqlite'
},
portal: {
enabled: false,
host: 'https://tickets.example.com'
},
presences: [
{
activity: '%snew | %shelp',
type: 'PLAYING'
},
{
activity: 'with tickets | %shelp',
type: 'PLAYING'
},
{
activity: 'for new tickets | %shelp',
type: 'WATCHING'
}
],
defaults: {
prefix: '-',
colour: '#009999',
},
logs: {
enabled: true,
keep_for: 30
},
debug: false,
update_notice: true,
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 698 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 577 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB