diff --git a/package-lock.json b/package-lock.json index b3f462a..5979a49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/packages/streamwall/package.json b/packages/streamwall/package.json index c949667..61812b8 100644 --- a/packages/streamwall/package.json +++ b/packages/streamwall/package.json @@ -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", diff --git a/packages/streamwall/src/main/TwitchBot.ts b/packages/streamwall/src/main/TwitchBot.ts new file mode 100644 index 0000000..de9a8ff --- /dev/null +++ b/packages/streamwall/src/main/TwitchBot.ts @@ -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 + votes: Map + + 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) + } +} diff --git a/packages/streamwall/src/main/index.ts b/packages/streamwall/src/main/index.ts index 33fd4fe..ac8cc92 100644 --- a/packages/streamwall/src/main/index.ts +++ b/packages/streamwall/src/main/index.ts @@ -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) { 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)