From d32e8a0f41bc1e0cd707b1c38b7f1c4c689db3a7 Mon Sep 17 00:00:00 2001 From: Max Goodhart Date: Tue, 3 Nov 2020 13:26:10 -0800 Subject: [PATCH] Add experimental Twitch plays functionality --- example.config.toml | 4 +++ src/node/TwitchBot.js | 59 ++++++++++++++++++++++++++++++++++++++++++- src/node/index.js | 14 ++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) diff --git a/example.config.toml b/example.config.toml index 666cecc..5361b11 100644 --- a/example.config.toml +++ b/example.config.toml @@ -54,6 +54,10 @@ json-url = ["https://woke.net/api/streams.json"] #interval = 60 #delay = 30 + [twitch.vote] + #template = "Switching to #<%- selectedIdx %> (with <%- voteCount %> votes)" + #interval = 15 + [cert] # SSL certificates (optional) # If you specify an https:// URL for the "webserver" option, a certificate will be automatically generated and signed by Let's Encrypt. diff --git a/src/node/TwitchBot.js b/src/node/TwitchBot.js index 79c8ef9..bee5082 100644 --- a/src/node/TwitchBot.js +++ b/src/node/TwitchBot.js @@ -4,10 +4,12 @@ import ejs from 'ejs' import { State } from 'xstate' import { ChatClient, SlowModeRateLimiter, LoginError } from 'dank-twitch-irc' +const VOTE_RE = /^!(\d+)$/ + export default class TwitchBot extends EventEmitter { constructor(config) { super() - const { username, token } = config + const { username, token, vote } = config this.config = config this.announceTemplate = ejs.compile(config.announce.template) const client = new ChatClient({ @@ -23,6 +25,12 @@ export default class TwitchBot extends EventEmitter { this.dwellTimeout = null this.announceTimeouts = new Map() + if (vote.interval) { + this.voteTemplate = ejs.compile(config.vote.template) + this.votes = new Map() + setInterval(this.tallyVotes.bind(this), vote.interval * 1000) + } + client.on('ready', () => { this.onReady() }) @@ -38,6 +46,9 @@ export default class TwitchBot extends EventEmitter { console.error('Twitch bot disconnected due to error:', err) } }) + client.on('PRIVMSG', (msg) => { + this.onMsg(msg) + }) } connect() { @@ -105,4 +116,50 @@ export default class TwitchBot extends EventEmitter { }, announce.interval * 1000) this.announceTimeouts.set(listeningURL, timeout) } + + async tallyVotes() { + const { client } = this + const { channel } = this.config + if (this.votes.size === 0) { + return + } + + let voteCount = 0 + let selectedIdx = null + for (const [idx, value] of this.votes) { + if (value > voteCount) { + voteCount = value + selectedIdx = idx + } + } + + const msg = this.voteTemplate({ selectedIdx, voteCount }) + await client.say(channel, msg) + + // Index spaces starting with 1 + this.emit('setListeningView', selectedIdx - 1) + + this.votes = new Map() + } + + onMsg(msg) { + const { grid, vote } = this.config + if (!vote.interval) { + return + } + + const match = msg.messageText.match(VOTE_RE) + if (!match) { + return + } + + let idx + try { + idx = Number(match[1]) + } catch (err) { + return + } + + this.votes.set(idx, (this.votes.get(idx) || 0) + 1) + } } diff --git a/src/node/index.js b/src/node/index.js index bb80c33..fa8a89a 100644 --- a/src/node/index.js +++ b/src/node/index.js @@ -98,6 +98,8 @@ function parseArgs() { 'twitch.color', 'twitch.announce.template', 'twitch.announce.interval', + 'twitch.vote.template', + 'twitch.vote.interval', ], 'Twitch Chat', ) @@ -133,6 +135,15 @@ function parseArgs() { number: true, default: 30, }) + .option('twitch.vote.template', { + describe: 'Message template for vote result announcements', + default: 'Switching to #<%- selectedIdx %> (with <%- voteCount %> votes)', + }) + .option('twitch.vote.interval', { + describe: 'Time interval (in seconds) between votes (0 to disable)', + number: true, + default: 0, + }) .group( [ 'control.username', @@ -382,6 +393,9 @@ async function main() { if (argv.twitch.token) { twitchBot = new TwitchBot(argv.twitch) + twitchBot.on('setListeningView', (idx) => { + streamWindow.setListeningView(idx) + }) twitchBot.connect() }