diff --git a/src/bot.js b/src/bot.js index 294be5b95..645c74c5d 100644 --- a/src/bot.js +++ b/src/bot.js @@ -26,6 +26,41 @@ const Notifier = require('./notifications/Notifier.js'); * @property {string} codeBlock - String for denoting multi-line code blocks */ + +function checkPrivateRooms(self, shardId) { + self.logger.debug(`Checking private rooms... Shard ${shardId}`); + self.settings.getPrivateRooms() + .then((privateRooms) => { + self.logger.debug(`Private rooms... ${privateRooms.length}`); + privateRooms.forEach((room) => { + if (room && room.voiceChannel && room.textChannel) { + const now = new Date(); + if (((now.getTime() + (now.getTimezoneOffset() * 60000)) - room.createdAt + > self.channelTimeout) + && room.voiceChannel.members.size === 0) { + if (room.textChannel.deletable) { + self.logger.debug(`Deleting text channel... ${room.textChannel.id}`); + room.textChannel.delete() + .then(() => { + if (room.voiceChannel.deletable) { + self.logger.debug(`Deleting text channel... ${room.voiceChannel.id}`); + return room.voiceChannel.delete(); + } + return new Promise(); + }) + .then(() => { + if (room.textChannel.deletable && room.voiceChannel.deletable) { + self.settings + .deletePrivateRoom(room.guild, room.textChannel, room.voiceChannel); + } + }); + } + } + } + }); + }); +} + /** * Class describing Genesis bot */ @@ -66,6 +101,8 @@ class Genesis { */ this.token = discordToken; + this.channelTimeout = 300000; + /** * The logger object * @type {Logger} @@ -207,6 +244,9 @@ class Genesis { this.client.user.setGame(`@${this.client.user.username} help (${this.shardId + 1}/${this.shardCount})`); this.settings.ensureData(this.client); this.readyToExecute = true; + + const self = this; + setInterval(checkPrivateRooms, self.channelTimeout, self, self.shardId); } /** diff --git a/src/commands/Rooms/Create.js b/src/commands/Rooms/Create.js new file mode 100644 index 000000000..71ae7743c --- /dev/null +++ b/src/commands/Rooms/Create.js @@ -0,0 +1,212 @@ +'use strict'; + +const Command = require('../../Command.js'); + +const useable = ['room', 'raid', 'team']; + +/** + * Gets the list of users from the mentions in the call + * @param {Message} message Channel message + * @returns {Array.} Array of users to send message + */ +function getUsersForCall(message) { + const users = []; + if (message.mentions.roles) { + message.mentions.roles.forEach(role => + role.members.forEach(member => + users.push(member.user))); + } + if (message.mentions.users) { + message.mentions.users.forEach((user) => { + if (users.indexOf(user) === -1) { + users.push(user); + } + }); + } + users.push(message.author); + return users; +} +/** + * Create temporary voice/text channels (can be expanded in the future) + */ +class Create extends Command { + /** + * Constructs a callable command + * @param {Genesis} bot The bot object + */ + constructor(bot) { + super(bot, 'rooms.create', 'create', 'Create a temporary room.'); + this.regex = new RegExp(`^${this.call}\\s?(room|raid|team)?(\\w|-)?`, 'i'); + + this.usages = [ + { description: 'Display instructions for creating temporary rooms', parameters: [] }, + { + description: 'Create temporary text and voice channels for the calling user.', + parameters: ['room | raid | team'], + }, + { + description: 'Create temporary text and voice channels for the calling user and any mentioned users/roles.', + parameters: ['room | raid | team', 'users and/or role'], + }, + ]; + + this.allowDM = false; + } + + /** + * Run the command + * @param {Message} message Message with a command to handle, reply to, + * or perform an action based on parameters. + */ + run(message) { + const type = message.strippedContent.match(this.regex)[1]; + const optName = message.strippedContent.match(this.regex)[2]; + this.bot.settings.getChannelSetting(message.channel, 'createPrivateChannel') + .then((createPrivateChannelAllowed) => { + if (createPrivateChannelAllowed && type) { + const roomType = type.trim(); + if (roomType === 'room' || roomType === 'raid' || roomType === 'team') { + const users = getUsersForCall(message); + const name = optName || `${type}-${message.member.displayName}`.toLowerCase(); + if (users.length < 11 && !message.guild.channels.find('name', name)) { + message.guild.createChannel(name.replace(/[^\w|-]/g, ' '), 'text') + .then(textChannel => [message.guild.createChannel(name, 'voice'), textChannel]) + .then((params) => { + const textChannel = params[1]; + // set up listener to delete channels if inactive for more than 5 minutes + return params[0].then((voiceChannel) => { + // set up overwrites + this.setOverwrites(textChannel, voiceChannel, users, message.guild.id); + // add channel to listenedChannels + this.bot.settings.addPrivateRoom(message.guild, textChannel, voiceChannel) + .then(() => {}) + .catch(this.logger.error); + // send users invite link to new rooms + this.sendInvites(voiceChannel, users, message.author); + // set room limits + this.setLimits(voiceChannel, roomType); + this.messageManager.embed(message, { + title: 'Channels created', + fields: [{ + name: '_ _', + value: `Voice Channel: ${voiceChannel.name}\n` + + `Text Channel: ${textChannel.name}`, + }], + }, false, false); + }); + }).catch((error) => { + this.logger.error(error); + if (error.response) { + this.logger.debug(`${error.message}: ${error.status}.\n` + + `Stack trace: ${error.stack}\n` + + `Response: ${error.response.body}`); + } + }); + } else { + let msg = ''; + if (users.length > 10) { + // notify caller that there's too many users if role is more than 10 people. + msg = 'you are trying to send an invite to too many people, please keep the total number under 10'; + } else { + msg = 'that room already exists.'; + } + this.messageManager.reply(message, msg, true, true); + } + } + } else { + this.messageManager.reply(message, '```haskell\n' + + 'Sorry, you need to specify what you want to create. Right now these are available to create:' + + `\n* ${useable.join('\n* ')}\n\`\`\`` + , true, false); + } + }); + } + + /** + * Send channel invites to users who were tagged in message + * @param {VoiceChannel} voiceChannel Voice channel to create invites for + * @param {Array.} users Array of users to send invites to + * @param {User} author Calling user who sends message + */ + sendInvites(voiceChannel, users, author) { + if (voiceChannel.permissionsFor(this.bot.client.user).has('CREATE_INSTANT_INVITE')) { + users.forEach((user) => { + voiceChannel.createInvite({ maxUses: 1 }).then((invite) => { + this.messageManager.sendDirectMessageToUser(user, `Invite for ${voiceChannel.name} from ${author}: ${invite}`, false); + }); + }); + } + } + + /** + * Set up overwrites for the text channel and voice channel + * for the list of users as the team, barring everyone else from joining + * @param {TextChannel} textChannel Text channel to set permissions for + * @param {VoiceChannel} voiceChannel Voice channel to set permissions for + * @param {Array.} users Array of users for whom to allow into channels + * @param {string} everyoneId Snowflake id for the everyone role + */ + setOverwrites(textChannel, voiceChannel, users, everyoneId) { + // create overwrites + const overwritePromises = []; + // create text channel perms + overwritePromises.push(textChannel.overwritePermissions(everyoneId, { + READ_MESSAGES: false, + })); + // create voice channel perms + overwritePromises.push(voiceChannel.overwritePermissions(everyoneId, { + CONNECT: false, + })); + + // allow bot to manage channels + overwritePromises.push(textChannel.overwritePermissions(this.bot.client.user.id, { + READ_MESSAGES: true, + SEND_MESSAGES: true, + })); + overwritePromises.push(voiceChannel.overwritePermissions(this.bot.client.user.id, { + CREATE_INSTANT_INVITE: true, + CONNECT: true, + SPEAK: true, + MUTE_MEMBERS: true, + DEAFEN_MEMBERS: true, + MOVE_MEMBERS: true, + USE_VAD: true, + MANAGE_ROLES: true, + MANAGE_CHANNELS: true, + })); + + // set up overwrites per-user + users.forEach((user) => { + overwritePromises.push(textChannel.overwritePermissions(user.id, { + READ_MESSAGES: true, + SEND_MESSAGES: true, + })); + overwritePromises.push(voiceChannel.overwritePermissions(user.id, { + CONNECT: true, + SPEAK: true, + USE_VAD: true, + })); + }); + + overwritePromises.forEach(promise => promise.catch(this.logger.error)); + } + + setLimits(voiceChannel, type) { + let limit = 99; + switch (type) { + case 'team': + limit = 4; + break; + case 'raid': + limit = 8; + break; + case 'room': + default: + break; + } + voiceChannel.setUserLimit(limit) + .then(vc => this.logger.debug(`User limit set to ${limit} for ${vc.name}`)); + } +} + +module.exports = Create; diff --git a/src/commands/Rooms/RoomCreation.js b/src/commands/Rooms/RoomCreation.js new file mode 100644 index 000000000..9299a3a2a --- /dev/null +++ b/src/commands/Rooms/RoomCreation.js @@ -0,0 +1,44 @@ +'use strict'; + +const Command = require('../../Command.js'); + +class RespondToSettings extends Command { + constructor(bot) { + super(bot, 'settings.allowprivateroom', 'allow private room', 'Set whether or not to allow the bot to create private rooms.'); + this.usages = [ + { description: 'Change if the bot to delete commands and/or responses after responding in this channel', parameters: ['deleting enabled'] }, + ]; + this.regex = new RegExp('^allow\\s?private\\s?rooms?\\s?(.+)?$', 'i'); + this.requiresAuth = true; + this.allowDM = false; + } + + run(message) { + let enable = message.strippedContent.match(this.regex)[1]; + if (!enable) { + const embed = { + title: 'Usage', + type: 'rich', + color: 0x0000ff, + fields: [ + { + name: `${this.bot.prefix}${this.call} `, + value: '_ _', + }, + ], + }; + this.messageManager.embed(message, embed, true, true); + } else { + enable = enable.trim(); + let enableResponse = false; + if (enable === 'enable' || enable === 'yes' || enable === '1' || enable === 'true' || enable === 1) { + enableResponse = true; + } + this.bot.settings.setGuildSetting(message.guild, 'createPrivateChannel', enableResponse) + .then(() => this.messageManager.notifySettingsChange(message, true, true)) + .catch(this.logger.error); + } + } +} + +module.exports = RespondToSettings; diff --git a/src/settings/Database.js b/src/settings/Database.js index 31494548a..297ae4cd0 100644 --- a/src/settings/Database.js +++ b/src/settings/Database.js @@ -40,6 +40,7 @@ class Database { platform: 'pc', language: 'en-us', delete_after_respond: true, + createPrivateChannel: false, }; } @@ -300,7 +301,7 @@ class Database { guild.channels.array().forEach((channel) => { promises.push(this.setChannelSetting(channel, setting, val)); }); - return null; + return Promise.each(promises, () => {}); } /** @@ -841,6 +842,32 @@ class Database { return []; }); } + + addPrivateRoom(guild, textChannel, voiceChannel) { + const query = SQL`INSERT INTO private_channels (guild_id, text_id, voice_id) VALUES (${guild.id}, ${textChannel.id}, ${voiceChannel.id})`; + return this.db.query(query); + } + + deletePrivateRoom(guild, textChannel, voiceChannel) { + const query = SQL`DELETE FROM private_channels WHERE guild_id = ${guild.id} AND text_id = ${textChannel.id} AND voice_id = ${voiceChannel.id}`; + return this.db.query(query); + } + + getPrivateRooms() { + const query = SQL`SELECT guild_id, text_id, voice_id, created_at as crt_sec FROM private_channels WHERE MOD(IFNULL(guild_id, 0) >> 22, ${this.bot.shardCount}) = ${this.bot.shardId}`; + return this.db.query(query) + .then((res) => { + if (res[0]) { + return res[0].map(value => ({ + guild: this.bot.client.guilds.get(value.guild_id), + textChannel: this.bot.client.channels.get(value.text_id), + voiceChannel: this.bot.client.channels.get(value.voice_id), + createdAt: value.crt_sec, + })); + } + return []; + }); + } } module.exports = Database; diff --git a/src/settings/MessageManager.js b/src/settings/MessageManager.js index 0f8eb94f2..4713c8a86 100644 --- a/src/settings/MessageManager.js +++ b/src/settings/MessageManager.js @@ -131,6 +131,20 @@ class MessaageManager { promises.forEach(promise => promise.catch(this.logger.error)); } + /** + * Send a message, with options to delete messages after calling + * @param {TextChannel} user user being sent a message + * @param {string} content String to send to a channel + * @param {boolean} deleteResponse True to delete the sent message after time + */ + sendDirectMessageToUser(user, content, deleteResponse) { + const promises = []; + promises.push(user.send(content).then((msg) => { + this.deleteCallAndResponse(user, msg, false, deleteResponse); + })); + promises.forEach(promise => promise.catch(this.logger.error)); + } + /** * Send a message, with options to delete messages after calling * @param {Message} message original message being responded to @@ -192,18 +206,20 @@ class MessaageManager { * @param {boolean} deleteResponse whether or not to delete the message response */ deleteCallAndResponse(call, response, deleteCall, deleteResponse) { - this.settings.getChannelDeleteAfterResponse(call.channel) - .then((deleteAfterRespond) => { - if (deleteAfterRespond === '1') { - if (deleteCall && call.deletable) { - call.delete(10000).catch(() => `Couldn't delete ${call}`); + if (call.channel) { + this.settings.getChannelDeleteAfterResponse(call.channel) + .then((deleteAfterRespond) => { + if (deleteAfterRespond === '1') { + if (deleteCall && call.deletable) { + call.delete(10000).catch(() => `Couldn't delete ${call}`); + } + if (deleteResponse && response.deletable) { + call.delete(10000).catch(() => `Couldn't delete ${call}`); + } } - if (deleteResponse && response.deletable) { - call.delete(10000).catch(() => `Couldn't delete ${call}`); - } - } - }) - .catch(this.logger.error); + }) + .catch(this.logger.error); + } } webhook(webhookId, embed) { diff --git a/src/settings/schema.js b/src/settings/schema.js index add05ecae..f7896c0a6 100644 --- a/src/settings/schema.js +++ b/src/settings/schema.js @@ -73,4 +73,11 @@ module.exports = [ id_list JSON NOT NULL, PRIMARY KEY (guild_id) );`, + `CREATE TABLE IF NOT EXISTS private_channels ( + guild_id BIGINT UNSIGNED NOT NULL, + text_id BIGINT UNSIGNED NOT NULL, + voice_id BIGINT UNSIGNED NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (guild_id, text_id, voice_id) + );`, ];