mirror of
https://github.com/streamwall/streamwall.git
synced 2026-01-25 14:42:48 -05:00
Add Twitch stream focus announcement bot support
This commit is contained in:
104
src/node/TwitchBot.js
Normal file
104
src/node/TwitchBot.js
Normal file
@@ -0,0 +1,104 @@
|
||||
import EventEmitter from 'events'
|
||||
|
||||
import ejs from 'ejs'
|
||||
import { State } from 'xstate'
|
||||
import { ChatClient, SlowModeRateLimiter, LoginError } from 'dank-twitch-irc'
|
||||
|
||||
export default class TwitchBot extends EventEmitter {
|
||||
constructor(config) {
|
||||
super()
|
||||
const { username, token } = config
|
||||
this.config = config
|
||||
this.announceTemplate = ejs.compile(config.announce.template)
|
||||
const client = new ChatClient({
|
||||
username,
|
||||
password: `oauth:${token}`,
|
||||
rateLimits: 'default',
|
||||
})
|
||||
client.use(new SlowModeRateLimiter(client, 0))
|
||||
this.client = client
|
||||
|
||||
this.streams = null
|
||||
this.listeningURL = null
|
||||
this.announceTimeouts = new Map()
|
||||
|
||||
client.on('ready', () => {
|
||||
this.onReady()
|
||||
})
|
||||
client.on('error', (err) => {
|
||||
console.error('Twitch connection error:', err)
|
||||
if (err instanceof LoginError) {
|
||||
client.close()
|
||||
}
|
||||
})
|
||||
client.on('close', (err) => {
|
||||
console.log('Twitch bot disconnected.')
|
||||
if (err != null) {
|
||||
console.error('Twitch bot disconnected due to error:', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
connect() {
|
||||
const { client } = this
|
||||
client.connect()
|
||||
}
|
||||
|
||||
async onReady() {
|
||||
const { client } = this
|
||||
const { channel, color } = this.config
|
||||
await client.setColor(color)
|
||||
await client.join(channel)
|
||||
this.emit('connected')
|
||||
}
|
||||
|
||||
onState({ views, streams }) {
|
||||
this.streams = streams
|
||||
|
||||
const listeningView = views.find(({ state, context }) =>
|
||||
State.from(state, context).matches('displaying.running.audio.listening'),
|
||||
)
|
||||
if (!listeningView) {
|
||||
return
|
||||
}
|
||||
|
||||
const listeningURL = listeningView.context.content.url
|
||||
if (listeningURL === this.listeningURL) {
|
||||
return
|
||||
}
|
||||
this.listeningURL = listeningURL
|
||||
this.onListeningURLChange(listeningURL)
|
||||
}
|
||||
|
||||
async onListeningURLChange(listeningURL) {
|
||||
if (!this.announceTimeouts.has(listeningURL)) {
|
||||
await this.announce()
|
||||
}
|
||||
}
|
||||
|
||||
async announce() {
|
||||
const { client, listeningURL, streams } = this
|
||||
const { channel, announce } = this.config
|
||||
|
||||
if (!client.ready) {
|
||||
return
|
||||
}
|
||||
|
||||
const stream = streams.find((s) => s.link === listeningURL)
|
||||
if (!stream) {
|
||||
return
|
||||
}
|
||||
|
||||
const msg = this.announceTemplate({ stream })
|
||||
await client.say(channel, msg)
|
||||
this.emit('sent', msg)
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
this.announceTimeouts.delete(listeningURL)
|
||||
if (this.listeningURL === listeningURL) {
|
||||
this.announce()
|
||||
}
|
||||
}, announce.interval * 1000)
|
||||
this.announceTimeouts.set(listeningURL, timeout)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from 'fs'
|
||||
import yargs from 'yargs'
|
||||
import TOML from '@iarna/toml'
|
||||
import Color from 'color'
|
||||
import { Repeater } from '@repeaterjs/repeater'
|
||||
import { app, shell, session, BrowserWindow } from 'electron'
|
||||
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
combineDataSources,
|
||||
} from './data'
|
||||
import StreamWindow from './StreamWindow'
|
||||
import TwitchBot from './TwitchBot'
|
||||
import StreamdelayClient from './StreamdelayClient'
|
||||
import initWebServer from './server'
|
||||
|
||||
@@ -71,6 +73,45 @@ function parseArgs() {
|
||||
array: true,
|
||||
default: [],
|
||||
})
|
||||
.group(
|
||||
[
|
||||
'twitch.channel',
|
||||
'twitch.username',
|
||||
'twitch.password',
|
||||
'twitch.color',
|
||||
'twitch.announce.template',
|
||||
'twitch.announce.interval-seconds',
|
||||
],
|
||||
'Twitch Chat',
|
||||
)
|
||||
.option('twitch.channel', {
|
||||
describe: 'Name of Twitch channel',
|
||||
default: null,
|
||||
})
|
||||
.option('twitch.username', {
|
||||
describe: 'Username of Twitch bot account',
|
||||
default: null,
|
||||
})
|
||||
.option('twitch.password', {
|
||||
describe: 'Password of Twitch bot account',
|
||||
default: null,
|
||||
})
|
||||
.option('twitch.color', {
|
||||
describe: 'Color of Twitch bot username',
|
||||
coerce: (text) => Color(text).object(),
|
||||
default: { r: 255, g: 0, b: 0 },
|
||||
})
|
||||
.option('twitch.announce.template', {
|
||||
describe: 'Message template for stream announcements',
|
||||
default:
|
||||
'SingsMic <%- stream.source %> <%- stream.city && stream.state ? `(${stream.city} ${stream.state})` : `` %> <%- stream.link %>',
|
||||
})
|
||||
.option('twitch.announce.interval', {
|
||||
describe:
|
||||
'Minimum time interval (in seconds) between re-announcing the same stream',
|
||||
number: true,
|
||||
default: 60,
|
||||
})
|
||||
.group(
|
||||
[
|
||||
'control.username',
|
||||
@@ -163,6 +204,7 @@ async function main() {
|
||||
streamWindow.init()
|
||||
|
||||
let browseWindow = null
|
||||
let twitchBot = null
|
||||
let streamdelayClient = null
|
||||
|
||||
const clientState = {
|
||||
@@ -250,10 +292,18 @@ async function main() {
|
||||
streamdelayClient.connect()
|
||||
}
|
||||
|
||||
if (argv.twitch.token) {
|
||||
twitchBot = new TwitchBot(argv.twitch)
|
||||
twitchBot.connect()
|
||||
}
|
||||
|
||||
streamWindow.on('state', (viewStates) => {
|
||||
clientState.views = viewStates
|
||||
streamWindow.send('state', clientState)
|
||||
broadcastState(clientState)
|
||||
if (twitchBot) {
|
||||
twitchBot.onState(clientState)
|
||||
}
|
||||
})
|
||||
|
||||
const dataSources = [
|
||||
|
||||
Reference in New Issue
Block a user