diff --git a/lib/chat-utils/parse-commands-from-chat.js b/lib/chat-utils/parse-commands-from-chat.js index dfa8ece..4d6be2b 100644 --- a/lib/chat-utils/parse-commands-from-chat.js +++ b/lib/chat-utils/parse-commands-from-chat.js @@ -26,6 +26,15 @@ function formatUnban(user) { return `UNBAN ${JSON.stringify({ data: user })}`; } +function formatPoll(weighted, time, question, options) { + return `STARTPOLL ${JSON.stringify({ + weighted, + time, + question, + options, + })}`; +} + function parseMessage(message) { const parsed = JSON.parse(message.replace('MSG ', '')); return { user: parsed.nick, roles: parsed.features, message: parsed.data }; @@ -59,6 +68,7 @@ module.exports = { formatUnmute, formatBan, formatUnban, + formatPoll, parseWhisper, formatWhisper, }; diff --git a/lib/commands/implementations/breaking-news.js b/lib/commands/implementations/breaking-news.js index fae4e5e..c7cd983 100644 --- a/lib/commands/implementations/breaking-news.js +++ b/lib/commands/implementations/breaking-news.js @@ -53,7 +53,7 @@ function breakingNews(defaultMaxAge) { const formattedMaxAge = formatDuration(moment.duration(maxAge, 'seconds')); const listener = services.messageRelay.startListenerForChatMessages('breakingnews'); - listener.on('message', (data) => { + listener.on('msg', (data) => { const message = data.message.trim().toLowerCase(); if (state !== 'all' && !services.messageMatching.mentionsUser(message, mentionUser)) return; if (!services.messageMatching.hasLink(message)) return; diff --git a/lib/commands/implementations/gulag.js b/lib/commands/implementations/gulag.js index e482c63..9ea43f5 100644 --- a/lib/commands/implementations/gulag.js +++ b/lib/commands/implementations/gulag.js @@ -3,34 +3,32 @@ const Command = require('../command-interface'); const CommandOutput = require('../command-output'); const { makeBan } = require('../../chat-utils/punishment-helpers'); -function isNormalInteger(str) { - const n = Math.floor(Number(str)); - return n !== Infinity && String(n) === str && n >= 0; -} -let gulagStarted = false; function gulag(defaultBanTime) { + const pollDuration = 30000; + + let failSafeTimeout = null; + let gulagActive = false; + return (input, services, rawMessage) => { - if (gulagStarted) { + if (gulagActive) { return new CommandOutput( null, - 'Gulag in progress. Please wait for the current vote to finish.', + 'Gulag in progress. Please wait for the current gulag to finish.', ); } + const { isPermanent, parsedDuration, muteString, users: parsedUsers, } = services.gulag.parseInput(input, defaultBanTime); + if (parsedUsers.length > 5) { return new CommandOutput(null, 'Too many users to be sent to the gulag, max of 5.'); } - gulagStarted = true; - const listener = services.messageRelay.startListenerForChatMessages('gulag'); - if (listener === false) { - return new CommandOutput(null, 'Something went wrong?? uhh. Restart me.'); - } - const votedMap = {}; + + // FIXME: There's a chance that a random user is already in the list, reducing the number of expected users. const users = _.uniq( parsedUsers.map((name) => { if (name.toLowerCase() === 'random') { @@ -39,52 +37,85 @@ function gulag(defaultBanTime) { return name; }), ); - const userVotes = users.map((name) => ({ name, value: 0 })); - listener.on('message', (data) => { - if (votedMap[data.user]) { - return; - } - const message = data.message.trim(); - if (isNormalInteger(message)) { - const int = parseInt(message, 10); - if (int >= 1 && int <= users.length) { - votedMap[data.user] = true; - userVotes[int - 1].value += 1; - } + + const listener = services.messageRelay.startListenerForChatMessages('gulag'); + if (listener === false) { + return new CommandOutput(null, 'Something went wrong?? uhh. Restart me.'); + } + + listener.on('err', (message) => { + const error = JSON.parse(message); + if (error.description === 'activepoll' && !gulagActive) { + clearTimeout(failSafeTimeout); + services.messageRelay.stopRelay('gulag'); + services.messageRelay.sendOutputMessage( + 'Poll in progress. Please wait for the current poll to finish.', + ); } }); - setTimeout(() => { - gulagStarted = false; + + listener.on('pollstart', () => { + gulagActive = true; + services.messageRelay.sendOutputMessage( + `ENTER THE GULAG. Chatters battling it out to not get a ${muteString} ban. Vote KEEP not KICK!? ${users.join( + ' vs ', + )}`, + ); + + // Fail-safe + failSafeTimeout = setTimeout(() => { + gulagActive = false; + services.messageRelay.stopRelay('gulag'); + }, pollDuration + 5000); + }); + + listener.on('pollstop', (message) => { + clearTimeout(failSafeTimeout); + gulagActive = false; services.messageRelay.stopRelay('gulag'); - services.messageRelay.sendOutputMessage('Total votes:'); - userVotes.forEach((user) => { - services.messageRelay.sendOutputMessage(`${user.name} votes: ${user.value}`); + + services.messageRelay.sendOutputMessage('GULAG has ended:'); + + const poll = JSON.parse(message); + const winnerVotes = Math.max(...poll.totals); + const winners = []; + const losers = []; + poll.totals.forEach((votes, index) => { + if (votes === winnerVotes) { + winners.push({ user: poll.options[index], votes }); + } else { + losers.push({ user: poll.options[index], votes }); + } }); - const firstWinner = _.maxBy(userVotes, 'value'); - const winners = userVotes.filter((user) => user.value === firstWinner.value); - const losers = userVotes.filter((user) => user.value !== firstWinner.value); - winners.forEach((user) => { + winners.forEach(({ user, votes }) => { services.messageRelay.sendOutputMessage( - `${user.name} has won the most votes and will be released from the gulag AYAYA`, + `${user} has won the most votes and will be released from the gulag AYAYA . Votes: ${votes}`, ); }); - losers.forEach((user) => { + losers.forEach(({ user, votes }) => { services.punishmentStream.write( makeBan( - user.name, + user, parsedDuration, false, isPermanent, - `${user.name} banned through bot by GULAG battle started by ${rawMessage.user}. Votes: ${user.value}`, + `${user} banned through bot by GULAG battle started by ${rawMessage.user}. Votes: ${votes}`, ), ); }); - }, 30500); - const userOrs = users.join(' or '); - return new CommandOutput( - null, - `/vote ENTER THE GULAG. Chatters battling it out to not get ${muteString} ban. Vote KEEP not KICK!? ${userOrs} 30s`, + }); + + services.messageRelay.emit( + 'poll', + JSON.stringify({ + weighted: false, + time: pollDuration, + question: `ENTER THE GULAG. Chatters battling it out to not get a ${muteString} ban. Vote KEEP not KICK!?`, + options: users, + }), ); + + return new CommandOutput(null, ''); }; } module.exports = { diff --git a/lib/commands/implementations/mutelinks.js b/lib/commands/implementations/mutelinks.js index 0503f4a..613902f 100644 --- a/lib/commands/implementations/mutelinks.js +++ b/lib/commands/implementations/mutelinks.js @@ -52,7 +52,7 @@ function mutelinks(defaultPunishmentDuration) { const listener = services.messageRelay.startListenerForChatMessages('mutelinks'); const formattedDuration = formatDuration(moment.duration(muteDuration, 'seconds')); - listener.on('message', (data) => { + listener.on('msg', (data) => { const message = data.message.trim().toLowerCase(); if (state !== 'all' && !services.messageMatching.mentionsUser(message, mentionUser)) return; if (!services.messageMatching.hasLink(message)) return; diff --git a/lib/commands/implementations/voteban.js b/lib/commands/implementations/voteban.js index b4dc4e9..d57e7ff 100644 --- a/lib/commands/implementations/voteban.js +++ b/lib/commands/implementations/voteban.js @@ -1,84 +1,90 @@ -const _ = require('lodash'); const moment = require('moment'); - +const _ = require('lodash'); const Command = require('../command-interface'); const CommandOutput = require('../command-output'); const { makeBan } = require('../../chat-utils/punishment-helpers'); const basePunishmentHelper = require('../base-punishment-helper'); const formatDuration = require('../../chat-utils/format-duration'); -let voteBanStarted = false; -const weightedTranslateMap = { - flair8: 16, - flair3: 8, - flair1: 4, - flair13: 2, - flair9: 2, -}; function voteBan(ipBan, defaultBanTime, weighted) { + const pollDuration = 30000; + + let failSafeTimeout = null; + let votebanActive = false; + return function ban(input, services, rawMessage) { - if (voteBanStarted) { + if (votebanActive) { return new CommandOutput( null, - 'Vote ban in progress. Please wait for the current vote to finish.', + 'Vote ban in progress. Please wait for the current vote ban to finish.', ); } + const parsedInput = basePunishmentHelper(input, defaultBanTime)[0]; + if (parsedInput === false) { return new CommandOutput( null, 'Could not parse the duration. Usage: "!voteban {amount}{m,h,d,w}OR{perm} {user}" !voteban 1d Destiny', ); } + const { isPermanent, userToPunish, parsedDuration, parsedReason } = parsedInput; + const muteString = isPermanent + ? 'PERMANENTLY' + : formatDuration(moment.duration(parsedDuration, 'seconds')); - voteBanStarted = true; const listener = services.messageRelay.startListenerForChatMessages('voteban'); if (listener === false) { return new CommandOutput(null, 'Something went wrong?? uhh. Restart me.'); } - const muteString = isPermanent - ? 'PERMANENTLY' - : formatDuration(moment.duration(parsedDuration, 'seconds')); - const votedMap = {}; - let yes = 0; - let no = 0; - - listener.on('message', (data) => { - if (votedMap[data.user]) { - return; - } - const message = data.message.trim(); - if (message.trim() === '1' || message.trim() === '2') { - let votes = 1; - if (weighted) { - votes = _.max( - Object.keys(weightedTranslateMap).map((k) => { - const idx = data.roles.indexOf(k); - if (idx === -1) return 1; - return weightedTranslateMap[k]; - }), - ); - } - votedMap[data.user] = true; - // eslint-disable-next-line no-unused-expressions - message.trim() === '1' ? (yes += votes) : (no += votes); + listener.on('err', (message) => { + const error = JSON.parse(message); + if (error.description === 'activepoll' && !votebanActive) { + clearTimeout(failSafeTimeout); + services.messageRelay.stopRelay('voteban'); + services.messageRelay.sendOutputMessage( + 'Poll in progress. Please wait for the current poll to finish.', + ); } }); - setTimeout(() => { - voteBanStarted = false; + listener.on('pollstart', () => { + votebanActive = true; + services.messageRelay.sendOutputMessage( + `Should we ban ${userToPunish} ${ + muteString === 'PERMANENTLY' ? muteString : `for ${muteString}` + } ${parsedReason ? ` Reason: ${parsedReason}` : ''}?`, + ); + + // Fail-safe + failSafeTimeout = setTimeout(() => { + votebanActive = false; + services.messageRelay.stopRelay('voteban'); + }, pollDuration + 5000); + }); + + listener.on('pollstop', (message) => { + clearTimeout(failSafeTimeout); + votebanActive = false; services.messageRelay.stopRelay('voteban'); + services.messageRelay.sendOutputMessage('Total votes:'); - services.messageRelay.sendOutputMessage(`Yes votes: ${yes}`); - services.messageRelay.sendOutputMessage(`No votes: ${no}`); - if (yes <= no) { + + const poll = JSON.parse(message); + const votes = _.zipObject(poll.options, poll.totals); + + services.messageRelay.sendOutputMessage(`Yes votes: ${votes.yes}`); + services.messageRelay.sendOutputMessage(`No votes: ${votes.no}`); + + if (votes.yes <= votes.no) { services.messageRelay.sendOutputMessage( - `No votes win by ${no - yes} votes, ${userToPunish} is safe for now.. AYAYA `, + `No votes win by ${votes.no - votes.yes} votes, ${userToPunish} is safe for now.. AYAYA `, ); return; } + services.punishmentStream.write( makeBan( userToPunish, @@ -87,17 +93,24 @@ function voteBan(ipBan, defaultBanTime, weighted) { isPermanent, `${userToPunish} banned through bot by a VOTE BAN started by ${rawMessage.user}. ${ parsedReason ? `Reason: ${parsedReason}` : '' - } Yes votes: ${yes} No Votes: ${no}`, + } Yes votes: ${votes.yes} No Votes: ${votes.no}`, ), ); - }, 30500); + }); - return new CommandOutput( - null, - `/vote Should we ban ${userToPunish} ${ - muteString === 'PERMANENTLY' ? muteString : `for ${muteString}` - } ${parsedReason ? ` Reason: ${parsedReason}` : ''}? yes or no 30s`, + services.messageRelay.emit( + 'poll', + JSON.stringify({ + weighted, + time: pollDuration, + question: `Should we ban ${userToPunish} ${ + muteString === 'PERMANENTLY' ? muteString : `for ${muteString}` + } ${parsedReason ? ` Reason: ${parsedReason}` : ''}`, + options: ['yes', 'no'], + }), ); + + return new CommandOutput(null, ''); }; } diff --git a/lib/message-routing/chat-service-router.js b/lib/message-routing/chat-service-router.js index 47a11e2..70905ca 100644 --- a/lib/message-routing/chat-service-router.js +++ b/lib/message-routing/chat-service-router.js @@ -43,7 +43,7 @@ class ChatServiceRouter { this.logger.info('Chat socket opened.'); }); - this.bot.on('message', (newMessage) => { + this.bot.on('msg', (newMessage) => { this.messageRouter.routeIncomingMessages(newMessage); }); @@ -95,6 +95,18 @@ class ChatServiceRouter { }); }); + this.bot.on('err', (message) => { + this.messageRelay.relayMessageToListeners('err', message); + }); + + this.bot.on('pollstart', (message) => { + this.messageRelay.relayMessageToListeners('pollstart', message); + }); + + this.bot.on('pollstop', (message) => { + this.messageRelay.relayMessageToListeners('pollstop', message); + }); + this.messageSchedulerStream.on('command', (commandObject) => { this.bot.sendMessage(commandObject.work().output); }); @@ -115,6 +127,13 @@ class ChatServiceRouter { this.bot.sendMessage(message); }); + this.messageRelay.on('poll', (message) => { + const poll = JSON.parse(message); + if (this.chatToConnectTo === 'dgg') { + this.bot.sendPoll(poll.weighted, poll.time, poll.question, poll.options); + } + }); + this.punishmentStream.on('data', (punishmentObject) => { switch (punishmentObject.type) { case 'unmute': diff --git a/lib/message-routing/message-router.js b/lib/message-routing/message-router.js index 50f4e1f..e1990a7 100644 --- a/lib/message-routing/message-router.js +++ b/lib/message-routing/message-router.js @@ -27,7 +27,7 @@ class MessageRouter { const messageContent = newMessage.message; getReporter().incrementCounter(METRIC_NAMES.MESSAGE_RATE, 1); // No api to get roles, we gotta STORE IT - this.messageRelay.relayMessageToListeners(newMessage); + this.messageRelay.relayMessageToListeners('msg', newMessage); this.roleCache.addUserRoles(user, roles); diff --git a/lib/services/destinychat.js b/lib/services/destinychat.js index 4a55a74..4c2ccc2 100644 --- a/lib/services/destinychat.js +++ b/lib/services/destinychat.js @@ -12,6 +12,7 @@ const { formatUnmute, formatBan, formatUnban, + formatPoll, } = require('../chat-utils/parse-commands-from-chat'); class DestinyChat extends EventEmitter { @@ -43,9 +44,6 @@ class DestinyChat extends EventEmitter { this.emit('closed', e); } - // We only care about messages from users, toss everything else. - // If it looks like a command, emit a command event so we can react. - // Otherwise just emit it as a message for listeners of message events. parseMessages(event) { const isMessage = _.startsWith(event.data, 'MSG'); const isWhisper = _.startsWith(event.data, 'PRIVMSG '); @@ -55,7 +53,7 @@ class DestinyChat extends EventEmitter { return; } - if (isMessage) this.emit('message', parsedMessage); + if (isMessage) this.emit('msg', parsedMessage); if (_.startsWith(parsedMessage.message, '!')) { this.emit('command', { @@ -65,6 +63,16 @@ class DestinyChat extends EventEmitter { }); } } + + if (_.startsWith(event.data, 'ERR')) { + this.emit('err', event.data.replace('ERR ', '')); + } + if (_.startsWith(event.data, 'POLLSTART')) { + this.emit('pollstart', event.data.replace('POLLSTART ', '')); + } + if (_.startsWith(event.data, 'POLLSTOP')) { + this.emit('pollstop', event.data.replace('POLLSTOP ', '')); + } } sendMessage(message) { @@ -99,6 +107,10 @@ class DestinyChat extends EventEmitter { this.ws.send(formatUnban(punished.user)); } + sendPoll(weighted, time, question, options) { + this.ws.send(formatPoll(weighted, time, question, options)); + } + sendMultiLine(message) { message.forEach((newLineMessage) => { if (!_.isEmpty(newLineMessage)) { diff --git a/lib/services/message-relay.js b/lib/services/message-relay.js index 5b6b759..19a0763 100644 --- a/lib/services/message-relay.js +++ b/lib/services/message-relay.js @@ -20,13 +20,13 @@ class MessageRelay extends EventEmitter { return this.listeners[listenerKey]; } - relayMessageToListeners(message) { + relayMessageToListeners(type, message) { if (_.keys(this.listeners).length === 0) { return; } _.forEach(this.listeners, (listener) => { - listener.emit('message', message); + listener.emit(type, message); }); } diff --git a/tests/lib/commands/implementations/breaking-news.test.js b/tests/lib/commands/implementations/breaking-news.test.js index 4c9e9d7..a3669f1 100644 --- a/tests/lib/commands/implementations/breaking-news.test.js +++ b/tests/lib/commands/implementations/breaking-news.test.js @@ -41,17 +41,17 @@ describe('breakingNews command Test', () => { cachedAt: now, }; - messageRelay.relayMessageToListeners({ + messageRelay.relayMessageToListeners('msg', { message: 'trump malding OMEGALUL https://twitter.com/realDonaldTrump/status/1325195021339987969', user: 'MrMouton', }); - messageRelay.relayMessageToListeners({ + messageRelay.relayMessageToListeners('msg', { message: 'wow this is a cool tweet https://twitter.com/GazeWithin/status/1301160632838959111', user: 'Jabelonske', }); - messageRelay.relayMessageToListeners({ + messageRelay.relayMessageToListeners('msg', { message: 'did anyone else see this???? https://www.nytimes.com/2020/11/08/us/politics/biden-victory-speech-takeaways.html', user: 'dotted', }); @@ -59,7 +59,7 @@ describe('breakingNews command Test', () => { const output2 = breakingNews.work('off', this.mockServices).output; assert.deepStrictEqual(output2, 'Breaking news mode turned off'); - messageRelay.relayMessageToListeners({ + messageRelay.relayMessageToListeners('msg', { message: 'wow this is really cool indeed https://twitter.com/GazeWithin/status/1301160632838959111', user: 'Dan', }); @@ -82,15 +82,15 @@ describe('breakingNews command Test', () => { 'Breaking news mode (20m) turned on for mentioning Destiny', ); - messageRelay.relayMessageToListeners({ + messageRelay.relayMessageToListeners('msg', { message: 'Destiny click https://twitter.com/realDonaldTrump/status/1325195021339987969', user: 'Jabelonske', }); - messageRelay.relayMessageToListeners({ + messageRelay.relayMessageToListeners('msg', { message: 'Destiny MALARKEY https://www.nytimes.com/2020/11/08/us/politics/biden-victory-speech-takeaways.html', user: 'dotted', }); - messageRelay.relayMessageToListeners({ + messageRelay.relayMessageToListeners('msg', { message: 'trump OMEGALUL https://twitter.com/realDonaldTrump/status/1325195021339987969', user: 'Dan', }); @@ -98,7 +98,7 @@ describe('breakingNews command Test', () => { const output2 = breakingNews.work('off', this.mockServices).output; assert.deepStrictEqual(output2, 'Breaking news mode turned off'); - messageRelay.relayMessageToListeners({ + messageRelay.relayMessageToListeners('msg', { message: 'Destiny click https://twitter.com/realDonaldTrump/status/1325195021339987969', user: 'MrMouton', }); diff --git a/tests/lib/commands/implementations/mutelinks.test.js b/tests/lib/commands/implementations/mutelinks.test.js index 80380e9..d97a8e7 100644 --- a/tests/lib/commands/implementations/mutelinks.test.js +++ b/tests/lib/commands/implementations/mutelinks.test.js @@ -28,15 +28,15 @@ describe('Mutelinks Test', () => { new CommandOutput(null, 'Link muting (10m) turned on for all links'), ); - messageRelay.relayMessageToListeners({ + messageRelay.relayMessageToListeners('msg', { message: 'hey whats up.', user: 'test1', }); - messageRelay.relayMessageToListeners({ + messageRelay.relayMessageToListeners('msg', { message: 'hey cool linkerino .youtube.com', user: 'test2', }); - messageRelay.relayMessageToListeners({ + messageRelay.relayMessageToListeners('msg', { message: 'hey this is my second message with a link https://twitch.tv', user: 'test1', }); @@ -44,7 +44,7 @@ describe('Mutelinks Test', () => { const output2 = mutelinks.work('off', this.mockServices); assert.deepStrictEqual(output2, new CommandOutput(null, 'Link muting turned off')); - messageRelay.relayMessageToListeners({ + messageRelay.relayMessageToListeners('msg', { message: 'cool link http://twitter.com/widinwithbiden', user: 'test3', }); @@ -69,39 +69,39 @@ describe('Mutelinks Test', () => { new CommandOutput(null, 'Link muting (20m) turned on for mentioning deStInY'), ); - messageRelay.relayMessageToListeners({ + messageRelay.relayMessageToListeners('msg', { message: 'hey whats up.', user: 'test1', }); - messageRelay.relayMessageToListeners({ + messageRelay.relayMessageToListeners('msg', { message: 'hey cool linkerino .youtube.com', user: 'test2', }); - messageRelay.relayMessageToListeners({ + messageRelay.relayMessageToListeners('msg', { message: 'hey this is my second message with a link https://twitch.tv', user: 'test1', }); - messageRelay.relayMessageToListeners({ + messageRelay.relayMessageToListeners('msg', { message: 'yo Destiny what is up', user: 'test3', }); - messageRelay.relayMessageToListeners({ + messageRelay.relayMessageToListeners('msg', { message: 'DeStInY click https://twitch.tv', user: 'test4', }); - messageRelay.relayMessageToListeners({ + messageRelay.relayMessageToListeners('msg', { message: 'Destiny click reddit.com', user: 'test5', }); - messageRelay.relayMessageToListeners({ + messageRelay.relayMessageToListeners('msg', { message: 'wow i love this site destiny.gg', user: 'test6', }); - messageRelay.relayMessageToListeners({ + messageRelay.relayMessageToListeners('msg', { message: 'wow i love this site meme.com destiny.gg', user: 'test7', }); - messageRelay.relayMessageToListeners({ + messageRelay.relayMessageToListeners('msg', { message: 'wow i love this site meme.com destiny!', user: 'test8', }); @@ -109,7 +109,7 @@ describe('Mutelinks Test', () => { const output2 = mutelinks.work('off', this.mockServices); assert.deepStrictEqual(output2, new CommandOutput(null, 'Link muting turned off')); - messageRelay.relayMessageToListeners({ + messageRelay.relayMessageToListeners('msg', { message: 'destiny click http://twitter.com/widinwithbiden', user: 'test7', });