Add back TwitchBot

This commit is contained in:
Max Goodhart
2025-06-14 16:06:25 +00:00
parent af3a61f8e5
commit d63a5089de
4 changed files with 552 additions and 13 deletions

269
package-lock.json generated
View File

@@ -3860,11 +3860,36 @@
"@types/node": "*"
}
},
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
"integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
"license": "MIT",
"dependencies": {
"@types/ms": "*"
}
},
"node_modules/@types/diff-match-patch": {
"version": "1.0.36",
"resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz",
"integrity": "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg=="
},
"node_modules/@types/duplexify": {
"version": "3.6.4",
"resolved": "https://registry.npmjs.org/@types/duplexify/-/duplexify-3.6.4.tgz",
"integrity": "sha512-2eahVPsd+dy3CL6FugAzJcxoraWhUghZGEQJns1kTKfCXWKJ5iG/VkaB05wRVrDKHfOFKqb0X0kXh91eE99RZg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/ejs": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.5.tgz",
"integrity": "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/electron-squirrel-startup": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@types/electron-squirrel-startup/-/electron-squirrel-startup-1.0.2.tgz",
@@ -3954,6 +3979,12 @@
"dev": true,
"optional": true
},
"node_modules/@types/ms": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
"license": "MIT"
},
"node_modules/@types/mysql": {
"version": "2.15.26",
"resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.26.tgz",
@@ -4661,6 +4692,12 @@
"node": ">= 6"
}
},
"node_modules/async": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
"license": "MIT"
},
"node_modules/at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
@@ -4726,8 +4763,7 @@
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/base-x": {
"version": "5.0.1",
@@ -4841,7 +4877,6 @@
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -5147,7 +5182,6 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
@@ -5429,8 +5463,7 @@
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"node_modules/content-disposition": {
"version": "0.5.4",
@@ -5573,6 +5606,44 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"node_modules/dank-twitch-irc": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/dank-twitch-irc/-/dank-twitch-irc-4.3.0.tgz",
"integrity": "sha512-6ezjPDzwhOhifsiUhllmojlmkrDwMS/TbFVdVOTtq1KcKBzDYPAcpZN8d1jUUf1ICWVu85Dx9Xe66gErbabTYA==",
"deprecated": "I (randers) am no longer supporting or updating dank-twitch-irc. While it may continue to work, I'm no longer making sure it will continue to do so in the future. This package will also not receive any dependency or security updates. For this reason I recommend you to choose a different library in your projects, or to fork this project to continue development on it. If somebody makes a high-quality fork of this project, you can contact me and I can link to your fork in this deprecation notice here. Thank you.",
"license": "MIT",
"dependencies": {
"@types/debug": "^4.1.5",
"@types/duplexify": "^3.6.0",
"debug-logger": "^0.4.1",
"duplexify": "^4.1.1",
"eventemitter3": "^4.0.7",
"lodash.camelcase": "^4.3.0",
"lodash.pickby": "^4.6.0",
"make-error-cause": "^2.3.0",
"ms": "^2.1.3",
"randomstring": "^1.1.5",
"semaphore-async-await": "^1.5.1",
"simple-websocket": "^9.0.0",
"split2": "^3.2.1",
"ts-toolbelt": "^8.0.3"
}
},
"node_modules/dank-twitch-irc/node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"license": "MIT"
},
"node_modules/dank-twitch-irc/node_modules/split2": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz",
"integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==",
"license": "ISC",
"dependencies": {
"readable-stream": "^3.0.0"
}
},
"node_modules/data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
@@ -5649,6 +5720,30 @@
}
}
},
"node_modules/debug-logger": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/debug-logger/-/debug-logger-0.4.1.tgz",
"integrity": "sha512-bLX6pxuWO6KMXBRk6vpLgHqWXiuLbKp8kLL/wNEA4rgU8wsNLaw3UNz0NfOi1bcN0JwjFwSfxhldzU5Bc6P5RQ==",
"license": "MIT",
"dependencies": {
"debug": "^2.1.0"
}
},
"node_modules/debug-logger/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/debug-logger/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
@@ -5934,6 +6029,21 @@
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"dev": true
},
"node_modules/ejs": {
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
"integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
"license": "Apache-2.0",
"dependencies": {
"jake": "^10.8.5"
},
"bin": {
"ejs": "bin/cli.js"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/electron": {
"version": "33.2.1",
"resolved": "https://registry.npmjs.org/electron/-/electron-33.2.1.tgz",
@@ -7560,6 +7670,36 @@
"node": "^10.12.0 || >=12.0.0"
}
},
"node_modules/filelist": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
"integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
"license": "Apache-2.0",
"dependencies": {
"minimatch": "^5.0.1"
}
},
"node_modules/filelist/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/filelist/node_modules/minimatch": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/filename-reserved-regex": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz",
@@ -8235,7 +8375,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"engines": {
"node": ">=8"
}
@@ -9045,6 +9184,24 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/jake": {
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz",
"integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==",
"license": "Apache-2.0",
"dependencies": {
"async": "^3.2.3",
"chalk": "^4.0.2",
"filelist": "^1.0.4",
"minimatch": "^3.1.2"
},
"bin": {
"jake": "bin/cli.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -9341,6 +9498,12 @@
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
},
"node_modules/lodash.camelcase": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
"license": "MIT"
},
"node_modules/lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
@@ -9353,6 +9516,12 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true
},
"node_modules/lodash.pickby": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz",
"integrity": "sha512-AZV+GsS/6ckvPOVQPXSiFFacKvKB4kOQu6ynt9wz0F3LO4R9Ij4K1ddYsIytDpSgLz88JHd9P+oaLeej5/Sl7Q==",
"license": "MIT"
},
"node_modules/log-symbols": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
@@ -9469,8 +9638,16 @@
"node_modules/make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="
},
"node_modules/make-error-cause": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/make-error-cause/-/make-error-cause-2.3.0.tgz",
"integrity": "sha512-etgt+n4LlOkGSJbBTV9VROHA5R7ekIPS4vfh+bCAoJgRrJWdqJCBbpS3osRJ/HrT7R68MzMiY3L3sDJ/Fd8aBg==",
"license": "Apache-2.0",
"dependencies": {
"make-error": "^1.3.5"
}
},
"node_modules/make-fetch-happen": {
"version": "10.2.1",
@@ -9660,7 +9837,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -10929,7 +11105,6 @@
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"dev": true,
"funding": [
{
"type": "github",
@@ -10963,6 +11138,30 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
"license": "MIT",
"dependencies": {
"safe-buffer": "^5.1.0"
}
},
"node_modules/randomstring": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/randomstring/-/randomstring-1.3.1.tgz",
"integrity": "sha512-lgXZa80MUkjWdE7g2+PZ1xDLzc7/RokXVEQOv5NN2UOTChW1I8A9gha5a9xYBOqgaSoI6uJikDmCU8PyRdArRQ==",
"license": "MIT",
"dependencies": {
"randombytes": "2.1.0"
},
"bin": {
"randomstring": "bin/randomstring"
},
"engines": {
"node": "*"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -11633,6 +11832,15 @@
],
"license": "BSD-3-Clause"
},
"node_modules/semaphore-async-await": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/semaphore-async-await/-/semaphore-async-await-1.5.1.tgz",
"integrity": "sha512-b/ptP11hETwYWpeilHXXQiV5UJNJl7ZWWooKRE5eBIYWoom6dZ0SluCIdCtKycsMtZgKWE01/qAw6jblw1YVhg==",
"license": "MIT",
"engines": {
"node": ">=4.1"
}
},
"node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@@ -11901,6 +12109,33 @@
"kolorist": "^1.6.0"
}
},
"node_modules/simple-websocket": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/simple-websocket/-/simple-websocket-9.1.0.tgz",
"integrity": "sha512-8MJPnjRN6A8UCp1I+H/dSFyjwJhp6wta4hsVRhjf8w9qBHRzxYt14RaOcjvQnhD1N4yKOddEjflwMnQM4VtXjQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"debug": "^4.3.1",
"queue-microtask": "^1.2.2",
"randombytes": "^2.1.0",
"readable-stream": "^3.6.0",
"ws": "^7.4.2"
}
},
"node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@@ -12432,7 +12667,6 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},
@@ -12671,6 +12905,12 @@
}
}
},
"node_modules/ts-toolbelt": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-8.4.0.tgz",
"integrity": "sha512-hnGJXIgC49ZuF5g5oDthoge8t4cvT0dYg2pYO5C6yV/HmUUy1koooU2U/5K2N+Uw++hcXQpJAToLRa+GRp28Sg==",
"license": "Apache-2.0"
},
"node_modules/tsconfig-paths": {
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
@@ -14093,7 +14333,7 @@
}
},
"packages/streamwall": {
"version": "2.0.0-pre1",
"version": "2.0.0-pre2",
"license": "MIT",
"dependencies": {
"@iarna/toml": "^2.2.5",
@@ -14102,6 +14342,8 @@
"bufferutil": "^4.0.9",
"chokidar": "^4.0.3",
"color": "^5.0.0",
"dank-twitch-irc": "^4.3.0",
"ejs": "^3.1.10",
"electron-squirrel-startup": "^1.0.1",
"esbuild-register": "^3.6.0",
"hls.js": "^1.5.18",
@@ -14132,6 +14374,7 @@
"@electron/fuses": "^1.8.0",
"@preact/preset-vite": "^2.10.1",
"@types/color": "^4.2.0",
"@types/ejs": "^3.1.5",
"@types/electron-squirrel-startup": "^1.0.2",
"@types/lodash-es": "^4.17.12",
"@types/ws": "^8.5.13",

View File

@@ -25,6 +25,7 @@
"@electron/fuses": "^1.8.0",
"@preact/preset-vite": "^2.10.1",
"@types/color": "^4.2.0",
"@types/ejs": "^3.1.5",
"@types/electron-squirrel-startup": "^1.0.2",
"@types/lodash-es": "^4.17.12",
"@types/ws": "^8.5.13",
@@ -48,6 +49,8 @@
"bufferutil": "^4.0.9",
"chokidar": "^4.0.3",
"color": "^5.0.0",
"dank-twitch-irc": "^4.3.0",
"ejs": "^3.1.10",
"electron-squirrel-startup": "^1.0.1",
"esbuild-register": "^3.6.0",
"hls.js": "^1.5.18",

View File

@@ -0,0 +1,202 @@
import Color from 'color'
import {
ChatClient,
LoginError,
PrivmsgMessage,
SlowModeRateLimiter,
} from 'dank-twitch-irc'
import ejs from 'ejs'
import EventEmitter from 'events'
import { StreamList, StreamwallState } from 'streamwall-shared'
import { matchesState } from 'xstate'
import { StreamwallConfig } from '.'
const VOTE_RE = /^!(\d+)$/
type TwitchBotConfig = StreamwallConfig['twitch'] & {
username: string
token: string
channel: string
}
export default class TwitchBot extends EventEmitter {
config: TwitchBotConfig
announceTemplate: ejs.TemplateFunction
voteTemplate: ejs.TemplateFunction
client: ChatClient
streams: StreamList
listeningURL: string | null
dwellTimeout: NodeJS.Timeout | undefined
announceTimeouts: Map<string, NodeJS.Timeout>
votes: Map<number, number>
constructor(config: TwitchBotConfig) {
super()
const { username, token, vote } = 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 = []
this.listeningURL = null
this.dwellTimeout = undefined
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()
})
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)
}
})
client.on('PRIVMSG', (msg) => {
this.onMsg(msg)
})
}
connect() {
const { client } = this
client.connect()
}
async onReady() {
const { client } = this
const { channel, color: colorText } = this.config
const color = Color(colorText)
await client.setColor({
r: color.red(),
g: color.green(),
b: color.blue(),
})
await client.join(channel)
this.emit('connected')
}
onState({ views, streams }: StreamwallState) {
this.streams = streams
const listeningView = views.find(({ state }) =>
matchesState(state, 'displaying.running.audio.listening'),
)
if (!listeningView) {
return
}
const listeningURL = listeningView.context.content?.url ?? null
if (listeningURL === this.listeningURL) {
return
}
this.listeningURL = listeningURL
this.onListeningURLChange(listeningURL)
}
onListeningURLChange(listeningURL: string | null) {
if (!listeningURL) {
return
}
const { announce } = this.config
clearTimeout(this.dwellTimeout)
this.dwellTimeout = setTimeout(() => {
if (!this.announceTimeouts.has(listeningURL)) {
this.announce()
}
}, announce.delay * 1000)
}
async announce() {
const { client, listeningURL, streams } = this
const { channel, announce } = this.config
if (!client.ready || !listeningURL) {
return
}
const stream = streams.find((s) => s.link === listeningURL)
if (!stream) {
return
}
const msg = this.announceTemplate({ stream })
await client.say(channel, msg)
const timeout = setTimeout(() => {
this.announceTimeouts.delete(listeningURL)
if (this.listeningURL === listeningURL) {
this.announce()
}
}, 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
}
}
if (selectedIdx === null) {
return
}
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: PrivmsgMessage) {
const { 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)
}
}

View File

@@ -24,6 +24,7 @@ import {
} from './data'
import StreamdelayClient from './StreamdelayClient'
import StreamWindow from './StreamWindow'
import TwitchBot from './TwitchBot'
const SENTRY_DSN =
'https://e630a21dcf854d1a9eb2a7a8584cbd0b@o459879.ingest.sentry.io/5459505'
@@ -54,6 +55,21 @@ export interface StreamwallConfig {
control: {
endpoint: string
}
twitch: {
channel: string | null
username: string | null
token: string | null
color: string
announce: {
template: string
interval: number
delay: number
}
vote: {
template: string
interval: number
}
}
telemetry: {
sentry: boolean
}
@@ -157,6 +173,61 @@ function parseArgs(): StreamwallConfig {
describe: 'URL of control server endpoint',
default: null,
})
.group(
[
'twitch.channel',
'twitch.username',
'twitch.token',
'twitch.color',
'twitch.announce.template',
'twitch.announce.interval',
'twitch.vote.template',
'twitch.vote.interval',
],
'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.token', {
describe: 'Password of Twitch bot account',
default: null,
})
.option('twitch.color', {
describe: 'Color of Twitch bot username',
default: '#ff0000',
})
.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,
})
.option('twitch.announce.delay', {
describe: 'Time to dwell on a stream before its details are announced',
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(['telemetry.sentry'], 'Telemetry')
.option('telemetry.sentry', {
describe: 'Enable error reporting to Sentry',
@@ -402,6 +473,26 @@ async function main(argv: ReturnType<typeof parseArgs>) {
streamdelayClient.connect()
}
const {
username: twitchUsername,
token: twitchToken,
channel: twitchChannel,
} = argv.twitch
if (twitchUsername && twitchToken && twitchChannel) {
console.debug('Setting up Twitch bot...')
const twitchBot = new TwitchBot({
...argv.twitch,
username: twitchUsername,
token: twitchToken,
channel: twitchChannel,
})
twitchBot.on('setListeningView', (idx) => {
streamWindow.setListeningView(idx)
})
stateEmitter.on('state', () => twitchBot.onState(clientState))
twitchBot.connect()
}
const dataSources = [
...argv.data['json-url'].map((url) => {
console.debug('Setting data source from json-url:', url)