diff --git a/index.js b/index.js index 476dee8..ad20e96 100644 --- a/index.js +++ b/index.js @@ -56,8 +56,10 @@ services logger.error('Config property: "chatToConnectTo" not set to one of "dgg" or "twitch"'); process.exit(1); } - if(config.hasOwnProperty('scheduledCommands')){ - config.scheduledCommands.forEach(commandToSchedule => services.fakeScheduler.createMessage(commandToSchedule)); + if (config.hasOwnProperty('scheduledCommands')) { + config.scheduledCommands.forEach((commandToSchedule) => + services.fakeScheduler.createMessage(commandToSchedule), + ); } const chatServiceRouter = new ChatServiceRouter( @@ -66,10 +68,7 @@ services messageRouter, commandRouter, logger, - services.punishmentStream, - services.scheduledCommands, - services.fakeScheduler, - services.messageRelay, + services, ); chatServiceRouter.create(); }) diff --git a/lib/configuration/sample.config.json b/lib/configuration/sample.config.json index dbb7029..5464ec4 100644 --- a/lib/configuration/sample.config.json +++ b/lib/configuration/sample.config.json @@ -8,7 +8,14 @@ "cookieToken": "yourCookieToken", "botNick": "yourBotNick" }, - "scheduledCommands": ["!youtube", "!schedule"], + "destinyLive": { + "url": "ws://localhost:6969", + "origin": "www.obamna.gg" + }, + "scheduledCommands": [ + "!youtube", + "!schedule" + ], "logger": { "level": "debug" }, @@ -134,4 +141,4 @@ "threadFilePath": "", "stateStoreFilePath": "" } -} +} \ No newline at end of file diff --git a/lib/message-routing/chat-service-router.js b/lib/message-routing/chat-service-router.js index 47a11e2..18e6492 100644 --- a/lib/message-routing/chat-service-router.js +++ b/lib/message-routing/chat-service-router.js @@ -1,27 +1,18 @@ const _ = require('lodash'); class ChatServiceRouter { - constructor( - chatToConnectTo, - bot, - messageRouter, - commandRouter, - logger, - punishmentStream, - messageSchedulerStream, - fakeScheduler, - messageRelay, - ) { + constructor(chatToConnectTo, bot, messageRouter, commandRouter, logger, services) { this.messageRouter = messageRouter; this.commandRouter = commandRouter; this.chatToConnectTo = chatToConnectTo; this.logger = logger; this.bot = bot; - this.punishmentStream = punishmentStream; - this.messageSchedulerStream = messageSchedulerStream; - this.fakeScheduler = fakeScheduler; + this.punishmentStream = services.punishmentStream; + this.messageSchedulerStream = services.scheduledCommands; + this.fakeScheduler = services.fakeScheduler; // TODO just refactor other things to use the message relay - this.messageRelay = messageRelay; + this.messageRelay = services.messageRelay; + this.destinyLive = services.destinylive; } create() { @@ -50,49 +41,22 @@ class ChatServiceRouter { this.bot.on('command', (commandObject) => { this.commandRouter .routeIncomingCommandMessage(commandObject) - .then((outputObject) => { - if (_.isEmpty(outputObject.output)) { - return; - } - - if (outputObject.err) { - this.logger.error( - 'Purposeful error thrown by command', - commandObject, - outputObject.err, - ); - if (_.isString(outputObject.output)) { - if (outputObject.isWhisper) { - this.bot.sendWhisper(outputObject.user, outputObject.output); - } else { - this.bot.sendMessage(outputObject.output); - } - } - return; - } - - if ( - this.chatToConnectTo === 'dgg' && - outputObject.isMultiLine && - outputObject.isWhisper === false - ) { - this.bot.sendMultiLine(outputObject.output); - } else if ( - this.chatToConnectTo === 'twitch' && - outputObject.isMultiLine && - outputObject.isWhisper === false - ) { - // There's no good way to do multilines in twitch atm? :C - this.bot.sendMessage(outputObject.output.join(' ')); - } else if (outputObject.isWhisper === true) { - this.bot.sendWhisper(outputObject.user, outputObject.output); - } else { - this.bot.sendMessage(outputObject.output); - } - }) - .catch((err) => { - this.logger.error('Got an error while parsing command: ', err); - }); + .then((outputObject) => this.handleCommandReturn(outputObject, commandObject)) + .catch(this.handleCommandError); + }); + + this.destinyLive.connect(); + this.destinyLive.on('offline', () => { + // Turn mutelinks off. + const commandObject = { + command: '!mutelinks', + input: 'off', + parsedMessage: null, + }; + this.commandRouter + .backgroundRunCommand(commandObject) + .then((outputObject) => this.handleCommandReturn(outputObject, commandObject)) + .catch(this.handleCommandError); }); this.messageSchedulerStream.on('command', (commandObject) => { @@ -142,6 +106,47 @@ class ChatServiceRouter { } }); } + + handleCommandReturn(outputObject, commandObject) { + if (_.isEmpty(outputObject.output)) { + return; + } + + if (outputObject.err) { + this.logger.error('Purposeful error thrown by command', commandObject, outputObject.err); + if (_.isString(outputObject.output)) { + if (outputObject.isWhisper) { + this.bot.sendWhisper(outputObject.user, outputObject.output); + } else { + this.bot.sendMessage(outputObject.output); + } + } + return; + } + + if ( + this.chatToConnectTo === 'dgg' && + outputObject.isMultiLine && + outputObject.isWhisper === false + ) { + this.bot.sendMultiLine(outputObject.output); + } else if ( + this.chatToConnectTo === 'twitch' && + outputObject.isMultiLine && + outputObject.isWhisper === false + ) { + // There's no good way to do multilines in twitch atm? :C + this.bot.sendMessage(outputObject.output.join(' ')); + } else if (outputObject.isWhisper === true) { + this.bot.sendWhisper(outputObject.user, outputObject.output); + } else { + this.bot.sendMessage(outputObject.output); + } + } + + handleCommandError(err) { + this.logger.error('Got an error while parsing command: ', err); + } } module.exports = ChatServiceRouter; diff --git a/lib/message-routing/command-router.js b/lib/message-routing/command-router.js index 321e72d..38218f0 100644 --- a/lib/message-routing/command-router.js +++ b/lib/message-routing/command-router.js @@ -93,6 +93,25 @@ class CommandRouter { return false; } + backgroundRunCommand(commandObject) { + const commandFunction = this.commandRegister.findCommand(commandObject.command); + if (commandFunction === false) return Promise.resolve(false); + + if ( + _.isObject(commandFunction.inputValidator) && + !commandFunction.inputValidator.test(commandObject.input) + ) { + return Promise.resolve(false); + } + + return this.runCommand( + commandFunction, + commandObject.input, + commandObject.parsedMessage, + false, + ); + } + runCommand(command, input, parsedMessage, isWhisper) { return new Promise((accept, reject) => { if (command.isPromiseOutput) { @@ -101,14 +120,14 @@ class CommandRouter { .then((outputObject) => { if (outputObject.err !== null) { return accept({ - user: parsedMessage.user, + user: parsedMessage?.user, isWhisper, err: outputObject.err, output: outputObject.output, }); } return accept({ - user: parsedMessage.user, + user: parsedMessage?.user, isWhisper, output: outputObject.output, isMultiLine: command.multiLineCommand, @@ -125,7 +144,7 @@ class CommandRouter { try { const staticOutputObject = command.work(input, this.services, parsedMessage); return accept({ - user: parsedMessage.user, + user: parsedMessage?.user, output: staticOutputObject.output, isMultiLine: command.multiLineCommand, isWhisper, diff --git a/lib/services/destinylive.js b/lib/services/destinylive.js new file mode 100644 index 0000000..7908d62 --- /dev/null +++ b/lib/services/destinylive.js @@ -0,0 +1,61 @@ +const EventEmitter = require('events'); +const WebSocket = require('ws'); + +class DestinyLive extends EventEmitter { + constructor(config, services) { + super(); + this.logger = services.logger; + this.commandRegistry = services.commandRegistry; + this.url = config.url; + this.origin = config.origin; + this.cache = {}; + } + + connect() { + this.ws = new WebSocket(this.url, { + origin: this.origin, + }); + this.ws.onopen = this.onOpen.bind(this); + this.ws.onclose = this.onClose.bind(this); + this.ws.onmessage = this.parseMessages.bind(this); + this.ws.onerror = this.handleError.bind(this); + } + + onOpen() { + this.logger.info('Destiny Live socket opened.'); + } + + onClose() { + this.logger.info('Destiny Live socket closed. Attempting to reconnect....'); + setTimeout(() => { + this.connect(); + }, 5000); + } + + parseMessages(message) { + try { + const event = JSON.parse(message.data); + + if (event.type === 'dggApi:streamInfo' && this.cache[event.type]) { + const wasLive = this.cache[event.type].some((s) => s?.live); + const isLive = event.data.some((s) => s?.live); + if (wasLive && !isLive) { + this.emit('offline'); + } else if (!wasLive && isLive) { + this.emit('online'); + } + } + + // cache message + this.cache[event.type] = event.data; + } catch (error) { + this.logger.info('Destiny Live socket message parse error: ', error); + } + } + + handleError(error) { + this.logger.info('Destiny Live socket recieved error: ', error.message); + } +} + +module.exports = DestinyLive; diff --git a/lib/services/service-index.js b/lib/services/service-index.js index 4721582..2c99ee5 100644 --- a/lib/services/service-index.js +++ b/lib/services/service-index.js @@ -9,9 +9,9 @@ const SpamDetection = require('./spam-detection'); const ScheduledCommands = require('./message-scheduler'); const gulagService = require('./gulag'); const LastFm = require('./lastfm'); -const YouTube = require('./youtube.js'); -const GoogleCal = require('./schedule.js'); -const RoleCache = require('./role-cache.js'); +const YouTube = require('./youtube'); +const GoogleCal = require('./schedule'); +const RoleCache = require('./role-cache'); const DggApi = require('./dgg-api'); const TwitterApi = require('./twitter-api'); const FakeScheduler = require('./fake-command-scheduler'); @@ -19,6 +19,7 @@ const RedditVote = require('./reddit-vote'); const MessageRelay = require('./message-relay'); const messageMatchingService = require('./message-matching'); const HTMLMetadata = require('./html-metadata'); +const DestinyLive = require('./destinylive'); class Services { constructor(serviceConfigurations, chatConnectedTo) { @@ -42,6 +43,7 @@ class Services { this.schedule = new GoogleCal(serviceConfigurations.googleCalendar); this.fakeScheduler = new FakeScheduler(serviceConfigurations.schedule); this.dggApi = new DggApi(serviceConfigurations.dggApi, this.logger); + this.destinylive = new DestinyLive(serviceConfigurations.destinyLive, this); this.twitterApi = new TwitterApi(serviceConfigurations.twitter, this.logger); this.messageRelay = new MessageRelay(); this.htmlMetadata = new HTMLMetadata();