diff --git a/.eslintrc b/.eslintrc index a45ac32..b4d557f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -41,6 +41,7 @@ {"properties": false} ], "newline-per-chained-call": "off", + "no-await-in-loop": "off", "no-confusing-arrow": "off", "no-console": "off", "no-continue": "off", diff --git a/commands.js b/commands.js index 788e860..f4bad4c 100644 --- a/commands.js +++ b/commands.js @@ -546,8 +546,11 @@ class Commands { Discord.createTextChannel(`twitch-${streamer}`).then((channel) => { channel.setTopic(`This channel is for ${user}'s Twitch stream. Follow ${user} on Twitch at http://twitch.tv/${streamer}.`).then(() => { channel.setPosition(9999).then(() => { - Discord.sortDiscordChannels(); - resolve(true); + Discord.sortDiscordChannels().then(() => { + resolve(true); + }).catch((err) => { + reject(new Exception("There was a Discord error while sorting channels.", err)); + }); }).catch((err) => { reject(new Exception("There was a Discord error while setting the position of the channel.", err)); }); diff --git a/discord.js b/discord.js index 87d73b3..0e8a8fb 100644 --- a/discord.js +++ b/discord.js @@ -2,10 +2,12 @@ const DiscordJs = require("discord.js"), Commands = require("./commands"), Db = require("./database"), + Exception = require("./exception"), Log = require("./log"), settings = require("./settings"), Tmi = require("./tmi"), Twitch = require("./twitch"), + Warning = require("./warning"), channelDeletionTimeouts = {}, discord = new DiscordJs.Client(settings.discord), @@ -156,9 +158,9 @@ class Discord { } }); - discord.on("message", (message) => { + discord.on("message", async (message) => { if (message.guild && message.guild.name === "Six Gaming" && message.channel.name === "sixbotgg" && message.channel.type === "text") { - Discord.message(message.author, message.content); + await Discord.message(message.author, message.content); } }); @@ -262,25 +264,36 @@ class Discord { /** * Parses a message. * @param {User} user The user who sent the message. - * @param {string} text The text of the message. - * @returns {void} + * @param {string} message The text of the message. + * @returns {Promise} A promise that resolves when the message is parsed. */ - static message(user, text) { - const matches = messageParse.exec(text); + static async message(user, message) { + for (const text of message.split("\n")) { + if (messageParse.test(text)) { + const matches = messageParse.exec(text), + command = matches[1].toLocaleLowerCase(), + args = matches[2]; + + if (Object.getOwnPropertyNames(Commands.prototype).filter((p) => typeof Commands.prototype[p] === "function" && p !== "constructor").indexOf(command) !== -1) { + let success; + try { + await Discord.commands[command](user, args); + } catch (err) { + if (err instanceof Warning) { + Log.warning(`${user}: ${text}\n${err}`); + } else if (err instanceof Exception) { + Log.exception(err.message, err.innerError); + } else { + Log.exception("Unhandled error found.", err); + } + + return; + } - if (matches) { - if (Object.getOwnPropertyNames(Commands.prototype).filter((p) => typeof Commands.prototype[p] === "function" && p !== "constructor").indexOf(matches[1]) !== -1) { - Discord.commands[matches[1]](user, matches[2]).then((success) => { if (success) { Log.log(`${user}: ${text}`); } - }).catch((err) => { - if (err.innerError) { - Log.exception(err.message, err.innerError); - } else { - Log.warning(err); - } - }); + } } } } @@ -711,8 +724,12 @@ class Discord { * @returns {void} */ static markEmptyVoiceChannel(channel) { - channelDeletionTimeouts[channel.id] = setTimeout(() => { - channel.delete(); + channelDeletionTimeouts[channel.id] = setTimeout(async () => { + try { + await channel.delete(); + } catch (err) { + Log.exception(`Couldn't delete empty voice channel ${channel}.`, err); + } delete channelDeletionTimeouts[channel.id]; }, 300000); } @@ -725,24 +742,27 @@ class Discord { // ### ## # ## ### ### ### ## ## # ### ## # # # # # # # # ## ### ### /** * Sorts Discord channels. - * @returns {void} + * @returns {Promise} A promise that resolves when the Discord channels are sorted. */ - static sortDiscordChannels() { + static async sortDiscordChannels() { const channels = Array.from(sixGuild.channels.filter((channel) => channel.name.startsWith("twitch-")).values()).sort((a, b) => a.name.localeCompare(b.name)), - positionChannel = (index) => { + positionChannel = async (index) => { const channel = sixGuild.channels.get(channels[index].id); index++; - channel.edit({position: index}).then(() => { - if (index < channels.length) { - positionChannel(index); - } - }).catch((err) => { + try { + await channel.edit({position: index}); + } catch (err) { Log.exception("Problem repositioning channels.", err); - }); + return; + } + + if (index < channels.length) { + positionChannel(index); + } }; - positionChannel(0); + await positionChannel(0); } // # # # # # # ## # ## @@ -821,7 +841,7 @@ class Discord { * @returns {void} */ static addStreamer(name) { - streamers.push(name.toLowerCase()); + streamers.push(name.toLocaleLowerCase()); } // ## # @@ -855,7 +875,7 @@ class Discord { * @returns {void} */ static addHost(name) { - hosts.push(name.toLowerCase()); + hosts.push(name.toLocaleLowerCase()); } // # # # @@ -888,8 +908,8 @@ class Discord { * @param {User} user The user to add to the role. * @returns {Promise} A promise that resolves when the user has been added to the role. */ - static addStreamersRole(user) { - return sixGuild.member(user).addRole(streamersRole); + static async addStreamersRole(user) { + await sixGuild.member(user).addRole(streamersRole); } // ## # ### ## @@ -903,8 +923,8 @@ class Discord { * @param {User} user The user to remove from the role. * @returns {Promise} A promise that resovles when the user has been removed from the role. */ - static removeStreamersRole(user) { - return sixGuild.member(user).removeRole(streamersRole); + static async removeStreamersRole(user) { + await sixGuild.member(user).removeRole(streamersRole); } // # # ## # # # # # # ### ## @@ -919,8 +939,8 @@ class Discord { * @param {User} user The user to add to the role. * @returns {Promise} A promise that resolves when the user has been added to the role. */ - static addStreamNotifyRole(user) { - return sixGuild.member(user).addRole(streamNotifyRole); + static async addStreamNotifyRole(user) { + await sixGuild.member(user).addRole(streamNotifyRole); } // ## # # # # # # ### ## @@ -935,8 +955,8 @@ class Discord { * @param {User} user The user to remove from the role. * @returns {Promise} A promise that resolves when the user has been removed from the role. */ - static removeStreamNotifyRole(user) { - return sixGuild.member(user).removeRole(streamNotifyRole); + static async removeStreamNotifyRole(user) { + await sixGuild.member(user).removeRole(streamNotifyRole); } // # ### ## @@ -966,8 +986,8 @@ class Discord { * @param {Role} role The role to add the user to. * @returns {Promise} A promise that resolves when the user has been added to the role. */ - static addUserToRole(user, role) { - return sixGuild.member(user).addRole(role); + static async addUserToRole(user, role) { + await sixGuild.member(user).addRole(role); } // # # #### ### ## @@ -982,8 +1002,8 @@ class Discord { * @param {Role} role The role to remove the user to. * @returns {Promise} A promise that resolves when the user has been removed from the role. */ - static removeUserFromRole(user, role) { - return sixGuild.member(user).removeRole(role); + static async removeUserFromRole(user, role) { + await sixGuild.member(user).removeRole(role); } // # ### # ## # ## @@ -995,14 +1015,14 @@ class Discord { /** * Creates a text channel. * @param {string} name The name of the channel to create. - * @returns {Promise} A promise that resolves when the channel has been created. + * @returns {Promise} A promise that resolves with the created channel. */ - static createTextChannel(name) { - return sixGuild.createChannel(name, "text").then((channel) => { - channel.setParent(streamersCategory); + static async createTextChannel(name) { + const channel = await sixGuild.createChannel(name, "text"); - return channel; - }); + await channel.setParent(streamersCategory); + + return channel; } // # # # # ## # ## @@ -1014,15 +1034,15 @@ class Discord { /** * Creates a voice channel. * @param {string} name The name of the channel to create. - * @returns {Promise} A promise that resolves when the channel has been created. + * @returns {Promise} A promise that resolves with the created channel. */ - static createVoiceChannel(name) { - return sixGuild.createChannel(name, "voice").then((channel) => { - channel.edit({bitrate: 64000}); - channel.setParent(voiceCategory); + static async createVoiceChannel(name) { + const channel = await sixGuild.createChannel(name, "voice"); - return channel; - }); + await channel.edit({bitrate: 64000}); + await channel.setParent(voiceCategory); + + return channel; } // # ## # ## # # # diff --git a/index.js b/index.js index 5f0b457..da4f7bb 100644 --- a/index.js +++ b/index.js @@ -13,7 +13,7 @@ const Db = require("./database"), /** * Starts up the application. */ -(function startup() { +(async function startup() { Log.log("Starting up..."); // Set the window title. @@ -24,24 +24,28 @@ const Db = require("./database"), } // Get streamers and hosted channels. - Db.getStreamersAndHosts().then((data) => { - Log.log("Got streamer data."); + let data; + try { + data = await Db.getStreamersAndHosts(); + } catch (err) { + setTimeout(startup, 60000); + Log.exception("There was a database error getting streamers and hosted channels. Retrying in 60 seconds...", err); + return; + } - // Startup tmi - Tmi.startup(); - Tmi.connect(); + Log.log("Got streamer data."); - // Startup Discord - Discord.startup(); - Discord.connect(); + // Startup tmi + Tmi.startup(); + await Tmi.connect(); - // Add streamers and hosts. - data.streamers.forEach((streamer) => Discord.addStreamer(streamer)); - data.hosts.forEach((host) => Discord.addHost(host)); - }).catch((err) => { - setTimeout(startup, 60000); - Log.exception("There was a database error getting streamers and hosted channels.", err); - }); + // Startup Discord + Discord.startup(); + await Discord.connect(); + + // Add streamers and hosts. + data.streamers.forEach((streamer) => Discord.addStreamer(streamer)); + data.hosts.forEach((host) => Discord.addHost(host)); }()); process.on("unhandledRejection", (err) => { diff --git a/tmi.js b/tmi.js index d88f04b..60e192c 100644 --- a/tmi.js +++ b/tmi.js @@ -1,8 +1,10 @@ const TmiJs = require("tmi.js"), Commands = require("./commands"), + Exception = require("./exception"), Log = require("./log"), settings = require("./settings"), + Warning = require("./warning"), autoCommandRotation = [ "facebook", @@ -58,23 +60,27 @@ class Tmi { Log.exception("Disconnected from tmi...", ev); }); - tmi.on("message", (channel, userstate, text, self) => { + tmi.on("message", async (channel, userstate, text, self) => { if (!self && channel === "#sixgaminggg") { - Tmi.message(userstate["display-name"], text); + await Tmi.message(userstate["display-name"], text); } }); tmi.on("names", (channel, users) => { if (channel === "#sixgaminggg") { users.forEach((username) => { - chatters[username] = ""; + if (!chatters[username]) { + chatters[username] = ""; + } }); } }); tmi.on("join", (channel, username, self) => { if (!self && channel === "#sixgaminggg") { - chatters[username] = ""; + if (!chatters[username]) { + chatters[username] = ""; + } } }); @@ -104,15 +110,18 @@ class Tmi { * Connects to tmi. * @returns {void} */ - static connect() { + static async connect() { Log.log("Connecting to tmi..."); - tmi.connect().catch((err) => { + + try { + await tmi.connect(); + } catch (err) { Log.exception("TMI connection failed.", err); - }); + } // Setup IRC command rotation. clearTimeout(commandRotationTimeout); - commandRotationTimeout = setTimeout(() => Tmi.commandRotation(), 600000); + commandRotationTimeout = setTimeout(Tmi.commandRotation, 600000); } // # # ## ### ### ### ### ## @@ -126,24 +135,31 @@ class Tmi { * @param {string} text The text of the message. * @returns {void} */ - static message(user, text) { - const matches = messageParse.exec(text); + static async message(user, text) { + if (messageParse.test(text)) { + const matches = messageParse.exec(text), + command = matches[1].toLocaleLowerCase(), + args = matches[2]; - commandRotationWait--; - - if (matches) { - if (Object.getOwnPropertyNames(Commands.prototype).filter((p) => typeof Commands.prototype[p] === "function" && p !== "constructor").indexOf(matches[1]) !== -1) { - Tmi.commands[matches[1]](user, matches[2]).then((success) => { - if (success) { - Log.log(`${user}: ${text}`); - } - }).catch((err) => { - if (err.innerError) { + if (Object.getOwnPropertyNames(Commands.prototype).filter((p) => typeof Commands.prototype[p] === "function" && p !== "constructor").indexOf(command) !== -1) { + let success; + try { + await Tmi.commands[command](user, args); + } catch (err) { + if (err instanceof Warning) { + Log.warning(`${user}: ${text}\n${err}`); + } else if (err instanceof Exception) { Log.exception(err.message, err.innerError); } else { - Log.warning(err); + Log.exception("Unhandled error found.", err); } - }); + + return; + } + + if (success) { + Log.log(`${user}: ${text}`); + } } } } @@ -178,14 +194,16 @@ class Tmi { * @param {string} hostedChannel The hosted channel. * @return {Promise} A promise that resolves when hosting completes. */ - static host(hostingChannel, hostedChannel) { - return tmi.host(hostingChannel, hostedChannel).catch((err) => { + static async host(hostingChannel, hostedChannel) { + try { + await tmi.host(hostingChannel, hostedChannel); + } catch (err) { if (err === "No response from Twitch.") { Log.log(`Host command from ${hostingChannel} to ${hostedChannel} failed due to no response from Twitch.`); } else { Log.exception(`Host command from ${hostingChannel} to ${hostedChannel} failed.`, err); } - }); + } } // # # @@ -199,14 +217,16 @@ class Tmi { * @param {string} hostingChannel The hosting channel that will end hosting. * @return {Promise} A promise that resolves when unhosting completes. */ - static unhost(hostingChannel) { - return tmi.unhost(hostingChannel).catch((err) => { + static async unhost(hostingChannel) { + try { + await tmi.unhost(hostingChannel); + } catch (err) { if (err === "No response from Twitch.") { Log.log(`Unhost command from ${hostingChannel} failed due to no response from Twitch.`); } else { Log.exception(`Unhost command from ${hostingChannel} failed.`, err); } - }); + } } // # # # # @@ -234,11 +254,11 @@ class Tmi { * Automatically sends a rotating message to chat every 10 minutes. * @returns {void} */ - static commandRotation() { + static async commandRotation() { if (commandRotationWait <= 0) { - Tmi.message("SixBotGG", `!${autoCommandRotation[0]}`); + await Tmi.message("SixBotGG", `!${autoCommandRotation[0]}`); } else { - commandRotationTimeout = setTimeout(() => Tmi.commandRotation(), 600000); + commandRotationTimeout = setTimeout(Tmi.commandRotation, 600000); } } @@ -263,7 +283,7 @@ class Tmi { } clearTimeout(commandRotationTimeout); - commandRotationTimeout = setTimeout(() => Tmi.commandRotation(), 600000); + commandRotationTimeout = setTimeout(Tmi.commandRotation, 600000); } } diff --git a/twitch.js b/twitch.js index 65441ef..b069a38 100644 --- a/twitch.js +++ b/twitch.js @@ -1,6 +1,8 @@ const TwitchApi = require("twitch-api"), + settings = require("./settings"), - twitch = new TwitchApi(settings.twitch); + + twitchApi = new TwitchApi(settings.twitch); // ##### # # # // # # # @@ -36,7 +38,7 @@ class Twitch { } return new Promise((resolve, reject) => { - twitch.getStreams({channel: channels, limit: 100, "stream_type": "live", offset}, (err, results) => { + twitchApi.getStreams({channel: channels, limit: 100, "stream_type": "live", offset}, (err, results) => { if (err || !results || typeof results === "string") { reject(err, results); return; @@ -61,7 +63,6 @@ class Twitch { }); }).then(() => { if (recurse) { - console.log(offset); return Twitch.getStreams(channels, offset + 100, streams); } @@ -83,7 +84,7 @@ class Twitch { */ static getChannelStream(channel) { return new Promise((resolve, reject) => { - twitch.getChannelStream(channel, (err, results) => { + twitchApi.getChannelStream(channel, (err, results) => { if (err) { reject(err); return; diff --git a/warning.js b/warning.js new file mode 100644 index 0000000..0b02b47 --- /dev/null +++ b/warning.js @@ -0,0 +1,15 @@ +// # # # +// # # +// # # ### # ## # ## ## # ## ## # +// # # # # ## # ## # # ## # # # +// # # # #### # # # # # # ## +// ## ## # # # # # # # # # +// # # #### # # # ### # # ### +// # # +// ### +/** + * An error class used to denote a warning rather than an error. + */ +class Warning extends Error {} + +module.exports = Warning;