├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── bot.js ├── commands ├── about.js ├── add.js ├── done.js ├── help.js ├── ping.js ├── set.js └── ticket.js ├── config.js ├── events ├── guildCreate.js ├── guildDelete.js ├── message.js ├── messageDelete.js └── ready.js ├── modules ├── assistance.js ├── command.js ├── database.js ├── invite.js ├── moderator.js ├── prefix.js ├── sequence.js └── uptime.js ├── package.json ├── strings-en.json └── strings.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | extends: 'airbnb' 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 BitQuote 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tech support queue bot 2 | 3 | ## Functionality 4 | 5 | - Users can create tickets 6 | - Bot posts ticket info in a channel 7 | - Mods can resolve tickets 8 | - Everything saved to Rethink 9 | 10 | ## Setup 11 | 12 | 1. Install [RethinkDB](https://www.rethinkdb.com/docs/install/) and start the server 13 | 3. Fill out the info in *config.js* 14 | 4. Run `npm install` to fetch dependencies 15 | 5. Start the bot with `npm start` or use something like [PM2](http://pm2.keymetrics.io/) 16 | 17 | ## Technical bits 18 | 19 | - ESLint rules: [Airbnb](https://github.com/airbnb/javascript) 20 | - RethinkDB driver: official 21 | - Ticket document fields: 22 | - `id` (rethink key) 23 | - `message_id` (snowflake) 24 | - `user_id` (snowflake) 25 | - `timestamp` 26 | - `description` (text) 27 | - Bot messages in *strings.json* 28 | 29 | ## Misc 30 | 31 | This bot is not official supported. Do not complain about how horribly it is written or ask me for support. Updates/fixes are not guaranteed and will only be provided if I am in a good mood and I have some spare time. 32 | 33 | Licensed under [MIT](https://opensource.org/licenses/MIT). Copyright © 2017 BitQuote. 34 | -------------------------------------------------------------------------------- /bot.js: -------------------------------------------------------------------------------- 1 | /* 2 | Entry point for tech support queue bot 3 | 4 | License: MIT 5 | Copyright 2017 BitQuote 6 | */ 7 | 8 | // Package imports 9 | const Discord = require('discord.js'); 10 | const winston = require('winston'); 11 | const requireDir = require('require-dir'); 12 | 13 | // Module imports 14 | const Database = require('./modules/database.js'); 15 | const config = require('./config'); 16 | 17 | // Event handlers 18 | const events = requireDir('./events'); 19 | 20 | // Initialize client 21 | const bot = new Discord.Client(); 22 | const token = config.token; 23 | 24 | // Attempt DB setup 25 | const db = new Database(); 26 | db.init() 27 | .then(() => { 28 | // Log into Discord 29 | bot.login(token); 30 | }) 31 | .catch(winston.error); 32 | 33 | // Define event handlers 34 | bot.on('ready', () => events.ready(db)); 35 | bot.on('message', message => events.message(message, db)); 36 | bot.on('messageDelete', message => events.messageDelete(message, db)); 37 | bot.on('guildCreate', guild => events.guildCreate(guild, db)); 38 | -------------------------------------------------------------------------------- /commands/about.js: -------------------------------------------------------------------------------- 1 | /* 2 | about command 3 | */ 4 | 5 | // Import response 6 | const response = require('./../strings.json').commands.about.responses.default; 7 | module.exports = msg => msg.channel.send(response); 8 | -------------------------------------------------------------------------------- /commands/add.js: -------------------------------------------------------------------------------- 1 | /* 2 | add command 3 | */ 4 | 5 | // String format for templating, moment for pretty dates, and winston for logging 6 | const format = require('string-format'); 7 | const moment = require('moment'); 8 | const winston = require('winston'); 9 | 10 | // Import responses and date format 11 | const responses = require('./../strings.json').commands.add.responses; 12 | const ticketTemplate = require('./../strings.json').ticket; 13 | const dateFormat = require('./../config.js').dateFormat; 14 | 15 | module.exports = (msg, suffix, db) => { 16 | // Make sure description is provided 17 | if (!suffix) { 18 | msg.reply(responses.args_error); 19 | } else { 20 | // Save creation timestamp 21 | const timestamp = Date.now(); 22 | 23 | // Add ticket to DB 24 | db.createTicket(msg.guild.id, msg.author.id, timestamp, suffix) 25 | .then((res) => { 26 | // Save ticket ID 27 | const ticketId = res.generated_keys[0]; 28 | winston.info(`Created ticket ${ticketId} for @${msg.author.username} in ${msg.guild.name}`); 29 | 30 | // Success response 31 | msg.channel.send(format(responses.success, ticketId)); 32 | 33 | // Get guild's log channel 34 | db.autoCreateGuild(msg.guild.id) 35 | .then(() => { 36 | db.getGuildChannel(msg.guild.id) 37 | .then((logChannelId) => { 38 | // Check if log channel is specified 39 | if (logChannelId) { 40 | const logChannel = msg.guild.channels.get(logChannelId); 41 | if (logChannel) { 42 | // Post ticket as a message 43 | logChannel.send(format(ticketTemplate, ticketId, msg.author.tag, 44 | moment(timestamp).format(dateFormat), suffix)) 45 | .then(ticketMessage => db.setTicketMessage(ticketId, ticketMessage.id) 46 | .catch(winston.error)) 47 | .catch(winston.error); 48 | } 49 | } 50 | }) 51 | .catch(winston.error); 52 | }) 53 | .catch(winston.error); 54 | }) 55 | .catch((err) => { 56 | winston.error(err); 57 | msg.channel.send(responses.db_error); 58 | }); 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /commands/done.js: -------------------------------------------------------------------------------- 1 | /* 2 | done command 3 | */ 4 | 5 | // Winston for logging 6 | const winston = require('winston'); 7 | 8 | // Import responses and mod checker 9 | const responses = require('./../strings.json').commands.done.responses; 10 | const moderator = require('./../modules/moderator'); 11 | 12 | module.exports = (msg, suffix, db) => { 13 | // Allow only mods to use this 14 | moderator(msg.member, msg.guild, db) 15 | .then((isVerified) => { 16 | if (isVerified) { 17 | // Make sure ID is provided 18 | if (!suffix) { 19 | msg.reply(responses.args_error); 20 | } else { 21 | // Database error response 22 | const dbError = (err) => { 23 | winston.error(err); 24 | msg.channel.send(responses.db_error); 25 | }; 26 | 27 | // Get ticket's corresponding log message 28 | db.getTicketMessage(suffix) 29 | .then((msgId) => { 30 | // Check if log message exists 31 | if (msgId) { 32 | // Get guild's log channel 33 | db.getGuildChannel(msg.guild.id) 34 | .then((logChannelId) => { 35 | // Check if log channel exists 36 | if (logChannelId) { 37 | const logChannel = msg.guild.channels.get(logChannelId); 38 | if (logChannel) { 39 | // Fetch and delete the message 40 | logChannel.fetchMessage(msgId) 41 | .then(logMessage => logMessage.delete()) 42 | .catch(winston.error); 43 | } 44 | } 45 | }) 46 | .catch(winston.error); 47 | } 48 | }) 49 | .catch(winston.error); 50 | 51 | // Delete ticket in DB 52 | db.deleteTicket(suffix) 53 | .then((res) => { 54 | if (res.deleted === 1) { 55 | winston.info(`Deleted ticket ${suffix}`); 56 | msg.channel.send(responses.success); 57 | } else { 58 | msg.channel.send(responses.id_error); 59 | } 60 | }) 61 | .catch(dbError); 62 | } 63 | } 64 | }); 65 | }; 66 | -------------------------------------------------------------------------------- /commands/help.js: -------------------------------------------------------------------------------- 1 | /* 2 | help command 3 | */ 4 | 5 | // Import responses and assistance module 6 | const responses = require('./../strings.json').commands.help.responses; 7 | const assistance = require('./../modules/assistance'); 8 | 9 | module.exports = (msg, suffix) => msg.channel.send(responses.heading + 10 | (assistance(suffix) || responses.error)); 11 | -------------------------------------------------------------------------------- /commands/ping.js: -------------------------------------------------------------------------------- 1 | /* 2 | ping command 3 | */ 4 | 5 | // String format for templating 6 | const format = require('string-format'); 7 | 8 | // Import response and uptime 9 | const uptime = require('./../modules/uptime'); 10 | const responseTemplate = require('./../strings.json').commands.ping.responses.default; 11 | 12 | module.exports = msg => msg.channel.send(format( 13 | responseTemplate, uptime(), msg.client.guilds.size)); 14 | -------------------------------------------------------------------------------- /commands/set.js: -------------------------------------------------------------------------------- 1 | /* 2 | set command 3 | */ 4 | 5 | // String format for templating and winston for logging 6 | const format = require('string-format'); 7 | const winston = require('winston'); 8 | 9 | // Import responses and mod checker 10 | const responses = require('./../strings.json').commands.set.responses; 11 | const moderator = require('./../modules/moderator'); 12 | 13 | module.exports = (msg, suffix, db) => { 14 | // Allow only mods to use this 15 | moderator(msg.member, msg.guild, db) 16 | .then((isVerified) => { 17 | if (isVerified) { 18 | // Parse arguments 19 | const args = suffix.split(' '); 20 | 21 | // Database error response 22 | const dbError = (err) => { 23 | winston.error(err); 24 | msg.channel.send(responses.db_error); 25 | }; 26 | 27 | // Args error response 28 | const argsError = () => msg.reply(responses.args_error); 29 | 30 | // Set log channel 31 | if (args[0] === responses.args.log_channel[0]) { 32 | // Validate channel ID 33 | const channelId = args[1]; 34 | 35 | if (channelId) { 36 | const channel = msg.guild.channels.get(channelId); 37 | 38 | if (channel && channel.type === 'text') { 39 | // Set log channel in DB 40 | db.setGuildChannel(msg.guild.id, channelId) 41 | .then(() => msg.channel.send(format(responses.success, responses.args.log_channel[1], 42 | channel.toString()))) 43 | .catch(dbError); 44 | } else { 45 | msg.channel.send(format(responses.id_error, responses.args.log_channel[1])); 46 | } 47 | } else { 48 | argsError(); 49 | } 50 | // Set mod role 51 | } else if (args[0] === responses.args.mod_role[0]) { 52 | // Validate role ID 53 | const roleId = args[1]; 54 | 55 | if (roleId) { 56 | const role = msg.guild.roles.get(roleId); 57 | 58 | if (role && role.id !== msg.guild.id) { 59 | // Set mod role in DB 60 | db.setGuildRole(msg.guild.id, roleId) 61 | .then(() => msg.channel.send(format(responses.success, responses.args.mod_role[1], 62 | role.name))) 63 | .catch(dbError); 64 | } else { 65 | msg.channel.send(format(responses.id_error, responses.args.mod_role[1])); 66 | } 67 | } else { 68 | argsError(); 69 | } 70 | // Invalid arguments 71 | } else { 72 | argsError(); 73 | } 74 | } 75 | }); 76 | }; 77 | -------------------------------------------------------------------------------- /commands/ticket.js: -------------------------------------------------------------------------------- 1 | /* 2 | ticket command 3 | */ 4 | 5 | // String format for templating, moment for pretty dates, and winston for logging 6 | const format = require('string-format'); 7 | const moment = require('moment'); 8 | const winston = require('winston'); 9 | 10 | // Import responses, date format, and sequential send 11 | const responses = require('./../strings.json').commands.ticket.responses; 12 | const ticketTemplate = require('./../strings.json').ticket; 13 | const dateFormat = require('./../config.js').dateFormat; 14 | const sendArray = require('./../modules/sequence'); 15 | 16 | module.exports = (msg, suffix, db) => { 17 | // Database error response 18 | const dbError = (err) => { 19 | winston.error(err); 20 | msg.channel.send(responses.db_error); 21 | }; 22 | 23 | // Format ticket info 24 | const formatTicket = (ticket) => { 25 | const { id, user_id: userId, timestamp, description } = ticket; 26 | 27 | // Try getting the ticket creator 28 | const member = msg.guild.members.get(userId); 29 | 30 | // Show ticket info 31 | return format(ticketTemplate, id, member ? member.user.tag : userId, 32 | moment(timestamp).format(dateFormat), description); 33 | }; 34 | 35 | if (!suffix) { 36 | // Count support tickets in DB 37 | db.countGuildTickets(msg.guild.id) 38 | .then((count) => { 39 | let info = format(responses.default, count); 40 | 41 | // Determine if log channel exists 42 | db.autoCreateGuild(msg.guild.id) 43 | .then(() => { 44 | db.getGuildChannel(msg.guild.id) 45 | .then((logChannelId) => { 46 | // Check if log channel is specified 47 | if (logChannelId) { 48 | const logChannel = msg.guild.channels.get(logChannelId); 49 | 50 | // If so, mention it in the summary 51 | if (logChannel) { 52 | info += ` ${format(responses.log_channel, logChannel.toString())}`; 53 | } 54 | } 55 | 56 | // Send response 57 | msg.channel.send(info); 58 | }) 59 | .catch(dbError); 60 | }) 61 | .catch(dbError); 62 | }) 63 | .catch(dbError); 64 | } else if (suffix === 'list') { 65 | db.getRecentTickets(5) 66 | .then((tickets) => { 67 | sendArray(tickets.map(formatTicket), msg.channel); 68 | }) 69 | .catch(dbError); 70 | } else { 71 | // Fetch ticket from DB 72 | db.getTicket(suffix) 73 | .then((ticket) => { 74 | if (ticket) { 75 | msg.channel.send(formatTicket(ticket)); 76 | } else { 77 | msg.channel.send(responses.id_error); 78 | } 79 | }) 80 | .catch(dbError); 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | /* 2 | Bot config vars 3 | */ 4 | 5 | module.exports = { 6 | // Bot user token, found on Discord Devs website: 7 | token: '', 8 | // The bot app's client ID: 9 | clientId: '', 10 | // Commands should be preceded by this string: 11 | prefix: '@mention', 12 | // Moment.js date format to use: 13 | dateFormat: 'YY-MM-DD HH:mm', 14 | }; 15 | -------------------------------------------------------------------------------- /events/guildCreate.js: -------------------------------------------------------------------------------- 1 | /* 2 | New guild handler 3 | */ 4 | 5 | // Winston for logging 6 | const winston = require('winston'); 7 | 8 | // Add guild to DB 9 | module.exports = (guild, db) => db.createGuild(guild.id).then(() => winston.info(`Joined guild ${guild.id}`)).catch(winston.error); 10 | -------------------------------------------------------------------------------- /events/guildDelete.js: -------------------------------------------------------------------------------- 1 | /* 2 | Removed guild handler 3 | */ 4 | 5 | // Winston for logging 6 | const winston = require('winston'); 7 | 8 | // Add guild to DB 9 | module.exports = (guild, db) => db.deleteGuild(guild.id).then(() => winston.info(`Left guild ${guild.id}`)).catch(winston.error); 10 | -------------------------------------------------------------------------------- /events/message.js: -------------------------------------------------------------------------------- 1 | /* 2 | New message handler 3 | */ 4 | 5 | // String format for templating, winston for logging, and require-dir to import command files 6 | const format = require('string-format'); 7 | const winston = require('winston'); 8 | const requireDir = require('require-dir'); 9 | 10 | // Get all commands from directory 11 | const commands = requireDir('./../commands'); 12 | const commandList = Object.keys(commands); 13 | 14 | // Function to validate commands 15 | const getCommand = require('./../modules/command'); 16 | 17 | // Import response and invite link 18 | const inviteLink = require('./../modules/invite'); 19 | const pmResponseTemplate = require('./../strings.json').pm; 20 | 21 | module.exports = (msg, db) => { 22 | // Ignore self and system messages 23 | if (msg.author.id !== msg.client.user.id && !msg.system) { 24 | // Public messages 25 | if (msg.guild) { 26 | // Get command info (might be invalid) 27 | const cmdInfo = getCommand(msg.content, commandList, msg.client); 28 | 29 | // Run command if it's all good 30 | if (cmdInfo.isCommand && cmdInfo.isValid) { 31 | winston.info(`'${msg.cleanContent}' run by @${msg.author.username}`); 32 | commands[cmdInfo.name](msg, cmdInfo.suffix, db); 33 | } 34 | // Private messages 35 | } else { 36 | // Reply with invite link 37 | msg.reply(format(pmResponseTemplate, inviteLink)); 38 | } 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /events/messageDelete.js: -------------------------------------------------------------------------------- 1 | /* 2 | Deleted message handler 3 | */ 4 | 5 | // Winston for logging 6 | const winston = require('winston'); 7 | 8 | module.exports = (msg, db) => { 9 | // Check if author is the bot 10 | if (msg.author.id === msg.client.user.id) { 11 | // Get guild's log channel 12 | db.getGuildChannel(msg.guild.id) 13 | .then((logChannelId) => { 14 | // Check if message is in the log channel 15 | if (logChannelId && msg.channel.id === logChannelId) { 16 | // Delete corresponding support ticket 17 | db.deleteTicketByMessage(msg.id) 18 | .then(res => res.deleted === 1 && winston.info(`Deleted ticket for message ${msg.id}`)) 19 | .catch(winston.error); 20 | } 21 | }) 22 | .catch(winston.error); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /events/ready.js: -------------------------------------------------------------------------------- 1 | /* 2 | Connection ready handler 3 | */ 4 | 5 | // Import winston for logging 6 | const winston = require('winston'); 7 | 8 | module.exports = () => winston.info('Connected to Discord!'); 9 | -------------------------------------------------------------------------------- /modules/assistance.js: -------------------------------------------------------------------------------- 1 | /* 2 | Provides assistance with using the bot 3 | */ 4 | 5 | // Import command info 6 | const commandInfo = require('./../strings.json').commands; 7 | 8 | // Format the info for a single command 9 | function formatCommandInfo(command) { 10 | // Pull out the description and usage 11 | const { description, usage } = commandInfo[command]; 12 | 13 | // Default command info 14 | let str = `**${command}**\n\t${description}`; 15 | 16 | // Add usage info if necessary 17 | if (usage) { 18 | str += `\n\t\`${usage}\``; 19 | } 20 | 21 | return str; 22 | } 23 | 24 | // Generate list of commands 25 | const commandList = Object.keys(commandInfo).sort(); 26 | 27 | // Format and combine info for individual commands 28 | const cumulativeInfo = commandList.map(formatCommandInfo).join('\n'); 29 | 30 | module.exports = (command) => { 31 | if (command) { 32 | if (commandInfo[command]) { 33 | return formatCommandInfo(command); 34 | } 35 | return null; 36 | } 37 | return cumulativeInfo; 38 | }; 39 | -------------------------------------------------------------------------------- /modules/command.js: -------------------------------------------------------------------------------- 1 | /* 2 | Identify and parse bot commands 3 | */ 4 | 5 | const prefix = require('./prefix'); 6 | 7 | // Verify command 8 | function verifyCommand(name, cmds) { 9 | return cmds.includes(name); 10 | } 11 | 12 | module.exports = (content, cmds, bot) => { 13 | // Remove excess spaces from string 14 | const str = content.trim(); 15 | 16 | // Define data structure 17 | const data = { 18 | isCommand: false, 19 | }; 20 | 21 | // What a command should start with 22 | const beginning = prefix(bot); 23 | 24 | // Check if it's a command 25 | if (str.startsWith(beginning) && str.length > beginning.length) { 26 | // Split the string by spaces 27 | const cmdArgs = str.split(' '); 28 | 29 | // Set data values 30 | data.isCommand = true; 31 | data.name = cmdArgs[1].trim(); 32 | data.isValid = verifyCommand(data.name, cmds); 33 | data.suffix = cmdArgs.slice(2).join(' ').trim(); 34 | } 35 | 36 | return data; 37 | }; 38 | -------------------------------------------------------------------------------- /modules/database.js: -------------------------------------------------------------------------------- 1 | /* 2 | Interact with RethinkDB store 3 | */ 4 | 5 | // Import RethinkDB driver, bluebird for promises, and winston for logging 6 | const driver = require('rethinkdbdash'); 7 | const Promise = require('bluebird'); 8 | const winston = require('winston'); 9 | 10 | module.exports = class { 11 | // Connect to DB 12 | constructor() { 13 | this.r = driver({ db: 'tsqb' }); 14 | } 15 | 16 | // Default DB tables (guilds and tickets) 17 | init() { 18 | return this.r.tableCreate('guilds').run() 19 | .then(() => winston.info('Created guilds table')) 20 | .catch(() => winston.warn('Guilds table already exists')) 21 | .finally(() => this.r.tableCreate('tickets').run()) 22 | .then(() => winston.info('Created tickets table')) 23 | .catch(() => winston.warn('Tickets table already exists')); 24 | } 25 | 26 | // Insert data for a new guild 27 | createGuild(id) { 28 | return this.r.table('guilds').insert({ id, log_channel_id: null, mod_role_id: null }).run(); 29 | } 30 | 31 | // Call createGuild() if necessary 32 | autoCreateGuild(id) { 33 | return new Promise((resolve, reject) => { 34 | this.r.table('guilds').get(id).run() 35 | .then((document) => { 36 | if (document) { 37 | resolve(); 38 | } else { 39 | this.createGuild(id) 40 | .then(() => resolve()) 41 | .catch(reject); 42 | } 43 | }) 44 | .catch(reject); 45 | }); 46 | } 47 | 48 | // Fetch guild's ticket log channel 49 | getGuildChannel(id) { 50 | return this.r.table('guilds').get(id).getField('log_channel_id').run(); 51 | } 52 | 53 | // Set guild's ticket log channel 54 | setGuildChannel(id, channelId) { 55 | return this.r.table('guilds').get(id).update({ log_channel_id: channelId }).run(); 56 | } 57 | 58 | // Fetch guild's moderator role 59 | getGuildRole(id) { 60 | return this.r.table('guilds').get(id).getField('mod_role_id').run(); 61 | } 62 | 63 | // Set guild's moderator role 64 | setGuildRole(id, roleId) { 65 | return this.r.table('guilds').get(id).update({ mod_role_id: roleId }).run(); 66 | } 67 | 68 | // Delete data for a guild 69 | deleteGuild(id) { 70 | return this.r.table('guilds').get(id).delete().run(); 71 | } 72 | 73 | // Count guild's tickets 74 | countGuildTickets(id) { 75 | return this.r.table('tickets').filter({ guild_id: id }).count().run(); 76 | } 77 | 78 | // Fetch ticket info 79 | getTicket(id) { 80 | return this.r.table('tickets').get(id).run(); 81 | } 82 | 83 | // Fetch n most recent tickets 84 | getRecentTickets(n) { 85 | return this.r.table('tickets').orderBy('timestamp').limit(n).run(); 86 | } 87 | 88 | // Create user support ticket 89 | createTicket(guildId, userId, timestamp, description) { 90 | return this.r.table('tickets').insert({ guild_id: guildId, user_id: userId, timestamp, description, message_id: null }).run(); 91 | } 92 | 93 | // Get ticket's corresponding log message 94 | getTicketMessage(id) { 95 | return this.r.table('tickets').get(id).getField('message_id').run(); 96 | } 97 | 98 | // Set ticket's corresponding log message 99 | setTicketMessage(id, msgId) { 100 | return this.r.table('tickets').get(id).update({ message_id: msgId }).run(); 101 | } 102 | 103 | // Delete ticket (it has been closed) 104 | deleteTicket(id) { 105 | return this.r.table('tickets').get(id).delete().run(); 106 | } 107 | 108 | // Delete ticket by message ID 109 | deleteTicketByMessage(msgId) { 110 | return this.r.table('tickets').filter(this.r.row('message_id').eq(msgId)).delete().run(); 111 | } 112 | }; 113 | -------------------------------------------------------------------------------- /modules/invite.js: -------------------------------------------------------------------------------- 1 | /* 2 | Generate OAuth2 add-bot link 3 | */ 4 | 5 | // Import client ID from configs 6 | const clientId = require('./../config').clientId; 7 | 8 | // Calculate permissions constant 9 | const permissions = 0x400 | 0x800; // eslint-disable-line no-bitwise 10 | 11 | // Invite url 12 | const url = `https://discordapp.com/api/oauth2/authorize?client_id=${clientId}&scope=bot&permissions=${permissions}`; 13 | module.exports = url; 14 | -------------------------------------------------------------------------------- /modules/moderator.js: -------------------------------------------------------------------------------- 1 | /* 2 | Verify a member as a mod 3 | */ 4 | 5 | // Import bluebird for promises and winston for logging 6 | const Promise = require('bluebird'); 7 | const winston = require('winston'); 8 | 9 | module.exports = (member, guild, db) => new Promise((resolve) => { 10 | if (member.id === guild.ownerID) { 11 | resolve(true); 12 | } else { 13 | // Resolve false if there's an error 14 | const handleError = (err) => { 15 | winston.error(err); 16 | resolve(false); 17 | }; 18 | 19 | // Check if user has mod role 20 | db.autoCreateGuild(guild.id) 21 | .then(() => { 22 | db.getGuildRole(guild.id) 23 | .then((modRoleId) => { 24 | if (modRoleId && member.roles.has(modRoleId)) { 25 | resolve(true); 26 | } else { 27 | resolve(false); 28 | } 29 | }) 30 | .catch(handleError); 31 | }) 32 | .catch(handleError); 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /modules/prefix.js: -------------------------------------------------------------------------------- 1 | /* 2 | Print command prefix 3 | */ 4 | 5 | // Get command prefix setting from config 6 | let prefix = require('./../config').prefix; 7 | 8 | module.exports = (bot) => { 9 | if (prefix === '@mention') { 10 | prefix = `${bot.user.toString()} `; 11 | } 12 | return prefix; 13 | }; 14 | -------------------------------------------------------------------------------- /modules/sequence.js: -------------------------------------------------------------------------------- 1 | /* 2 | Sends messages sequentially 3 | */ 4 | 5 | // Winston for logging 6 | const winston = require('winston'); 7 | 8 | // Recursive function to send array element 9 | function sendMessage(arr, channel, i) { 10 | if (i < arr.length) { 11 | channel.send(arr[i]) 12 | .catch(winston.error) 13 | .then(() => sendMessage(arr, channel, i + 1)); 14 | } 15 | } 16 | 17 | module.exports = (arr, channel) => sendMessage(arr, channel, 0); 18 | -------------------------------------------------------------------------------- /modules/uptime.js: -------------------------------------------------------------------------------- 1 | /* 2 | Gets pretty-printed process uptime 3 | */ 4 | 5 | // Import moment for time parsing 6 | const moment = require('moment'); 7 | 8 | // Humanize duration in seconds 9 | module.exports = () => moment.duration(process.uptime(), 's').humanize(); 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tech-support-queue-bot", 3 | "version": "0.0.1", 4 | "description": "A simple Discord bot to manage support tickets", 5 | "main": "bot.js", 6 | "scripts": { 7 | "start": "node bot.js" 8 | }, 9 | "keywords": [ 10 | "discord", 11 | "support", 12 | "queue", 13 | "bot" 14 | ], 15 | "author": "BitQuote", 16 | "license": "MIT", 17 | "dependencies": { 18 | "bluebird": "^3.5.0", 19 | "discord.js": "^11.1.0", 20 | "moment": "^2.18.1", 21 | "require-dir": "^0.3.1", 22 | "rethinkdbdash": "^2.3.28", 23 | "string-format": "^0.5.0", 24 | "winston": "^2.3.1" 25 | }, 26 | "devDependencies": { 27 | "eslint-config-airbnb": "^15.0.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /strings-en.json: -------------------------------------------------------------------------------- 1 | { 2 | "pm": "Hey there! 😀 Add me to a server to get started: <{}> 👈", 3 | "ticket": "🔖 **ID:** `{0}`\n👤 **User:** {1}\n⏳ **Created:** {2}\n💬 **Description:**```{3}```", 4 | "commands": { 5 | "about": { 6 | "description": "Tells you about this bot", 7 | "responses": { 8 | "default": "__TSQB__ (**T**ech **S**upport **Q**ueue **B**ot) is a simple robot built by BitQuote to manage user-created tickets for community support on Discord servers." 9 | } 10 | }, 11 | "add": { 12 | "description": "Opens a new user support ticket", 13 | "usage": "", 14 | "responses": { 15 | "args_error": "Include a brief description of what you need help with please. ☝️", 16 | "db_error": "Uh-oh. 😳 Something went wrong. Try again later.", 17 | "success": "📨 Opened a support ticket. ID: `{}`" 18 | } 19 | }, 20 | "done": { 21 | "description": "Removes a ticket after it has been addressed", 22 | "usage": "", 23 | "responses": { 24 | "args_error": "Please provide the ticket's ID. 🏷 It was shown when the ticket was created and should be available in the log channel.", 25 | "id_error": "🤔 I couldn't find a ticket with that ID. Check again.", 26 | "db_error": "An error occurred. 😩 I'm sorry.", 27 | "success": "✅ All done with that!" 28 | } 29 | }, 30 | "help": { 31 | "description": "How to use TSQB", 32 | "responses": { 33 | "heading": "__Command Help__\n", 34 | "error": "That command doesn't exist. ☹️" 35 | } 36 | }, 37 | "ping": { 38 | "description": "Is the bot alive?", 39 | "responses": { 40 | "default": "👋 TSQB is alive, running for {0}. 🤖 On {1} servers." 41 | } 42 | }, 43 | "set": { 44 | "description": "Edits server config (mod-only)", 45 | "usage": "<'log_channel' or 'mod_role'> ", 46 | "responses": { 47 | "args": { 48 | "log_channel": [ 49 | "log_channel", 50 | "ticket log channel" 51 | ], 52 | "mod_role": [ 53 | "mod_role", 54 | "moderator role" 55 | ] 56 | }, 57 | "args_error": "Please tell me what you want to set (`log_channel` or `mod_role`) and the ID of the channel/role you want to use. 🚦 You can get that info from 'Copy ID' in Discord's developer mode context menu.", 58 | "id_error": "🙅 That ID isn't valid for a {}.", 59 | "db_error": "I tried, but it didn't work. 😦 Contact the bot maintainer.", 60 | "success": "👍 Alright, set the `{0}` to **{1}**" 61 | } 62 | }, 63 | "ticket": { 64 | "description": "Shows the status of ticket(s)", 65 | "usage": "[<'list' or ID>]", 66 | "responses": { 67 | "id_error": "🤔 I couldn't find a ticket with that ID. Check again.", 68 | "db_error": "🛑 I'm not working right now.", 69 | "default": "There are currently **{}** tickets in the queue. 🗄", 70 | "log_channel": "A full list is available in {}. 🙂" 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /strings.json: -------------------------------------------------------------------------------- 1 | strings-en.json --------------------------------------------------------------------------------