diff --git a/.github/release-template.md b/.github/release-template.md index f99481176..7c28b9639 100644 --- a/.github/release-template.md +++ b/.github/release-template.md @@ -25,6 +25,9 @@ ## Linux Install - Download Firebot-v{0}-linux-x64.tar.gz -- ?? will need further instructions +- Unpack Firebot-v{0}-linux-x64.tar.gz +- Change into the directory where you unpacked the archive +- Run the `Firebot v5` executable. + - This must either be done via a terminal window, or you will need to create a shortcut that includes the correct path for the unpacked archived as the "working directory". **Note**: Linux does not receive auto-updates \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 725fbfb1d..fd35be933 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,23 +1,23 @@ { "name": "firebotv5", - "version": "5.61.2", + "version": "5.62.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "firebotv5", - "version": "5.61.2", + "version": "5.62.0", "license": "GPL-3.0", "dependencies": { "@aws-sdk/client-polly": "^3.26.0", "@crowbartools/firebot-custom-scripts-types": "^5.53.2-6", "@nut-tree/nut-js": "^3.1.1", "@seald-io/nedb": "^4.0.4", - "@twurple/api": "^7.0.6", - "@twurple/auth": "^7.0.6", - "@twurple/chat": "^7.0.6", - "@twurple/eventsub-ws": "^7.0.6", - "@twurple/pubsub": "^7.0.6", + "@twurple/api": "^7.1.0", + "@twurple/auth": "^7.1.0", + "@twurple/chat": "^7.1.0", + "@twurple/eventsub-ws": "^7.1.0", + "@twurple/pubsub": "^7.1.0", "@zunderscore/elgato-light-control": "^1.1.2", "angular": "^1.8.0", "angular-animate": "^1.7.8", @@ -79,7 +79,7 @@ "node-hue-api": "^4.0.11", "node-json-db": "^1.4.1", "node-xlsx": "^0.20.0", - "obs-websocket-js": "^5.0.3", + "obs-websocket-js": "^5.0.5", "request": "^2.85.0", "roll": "^1.2.0", "sanitize-filename": "^1.6.3", @@ -1118,9 +1118,9 @@ "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==" }, "node_modules/@d-fischer/connection": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/@d-fischer/connection/-/connection-8.0.5.tgz", - "integrity": "sha512-F/rMmwVTE9/Rq2BzEU8CRoEVRvpUiSvazt56XgRp15oAbT9GC4D4CIfd4YlPxD5j63ueUjs0b+RDEN7BULrpRQ==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@d-fischer/connection/-/connection-9.0.0.tgz", + "integrity": "sha512-Mljp/EbaE+eYWfsFXUOk+RfpbHgrWGL/60JkAvjYixw6KREfi5r17XdUiXe54ByAQox6jwgdN2vebdmW1BT+nQ==", "dependencies": { "@d-fischer/isomorphic-ws": "^7.0.0", "@d-fischer/logger": "^4.2.1", @@ -1161,17 +1161,17 @@ } }, "node_modules/@d-fischer/isomorphic-ws": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@d-fischer/isomorphic-ws/-/isomorphic-ws-7.0.0.tgz", - "integrity": "sha512-bydCy1tKvPKvyF0KeDvN1aiAZA4CzQVa2gHifNQczW9Czl89vZ4QHnJMjUcTboWKecbnz5mGiM9PjKA1Xx2Dyg==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@d-fischer/isomorphic-ws/-/isomorphic-ws-7.0.2.tgz", + "integrity": "sha512-xK+qIJUF0ne3dsjq5Y3BviQ4M+gx9dzkN+dPP7abBMje4YRfow+X9jBgeEoTe5e+Q6+8hI9R0b37Okkk8Vf0hQ==", "peerDependencies": { "ws": "^8.2.0" } }, "node_modules/@d-fischer/logger": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@d-fischer/logger/-/logger-4.2.1.tgz", - "integrity": "sha512-D/QHXhdz1nt80SYPTC9VsnCBb9kfKUWUnxqvSbwWXuWSt2JNDUQZNwvRuhuxHSNYPd1IxD68/Ex8O5gkzLT14w==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@d-fischer/logger/-/logger-4.2.3.tgz", + "integrity": "sha512-mJUx9OgjrNVLQa4od/+bqnmD164VTCKnK5B4WOW8TX5y/3w2i58p+PMRE45gUuFjk2BVtOZUg55JQM3d619fdw==", "dependencies": { "@d-fischer/detect-node": "^3.0.1", "@d-fischer/shared-utils": "^3.2.0", @@ -1212,22 +1212,18 @@ } }, "node_modules/@d-fischer/rate-limiter": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@d-fischer/rate-limiter/-/rate-limiter-0.7.2.tgz", - "integrity": "sha512-x6XDquQjyJFtKW6oMjQ8vv8Z2S+i+fHWxgYLSZXObDF4JGFaz13hqQtrDEyAMzVgcO8SmmR5zzrNpAZzb/4u1Q==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@d-fischer/rate-limiter/-/rate-limiter-1.0.1.tgz", + "integrity": "sha512-Mq+0pAJsx92hP83cjmsrXQZVQJ+/+u1JFT6fjH8pj3yfUrbT3eDBsA+6J63eat+QaC+Mci78HdiBfpsdBkdwog==", "dependencies": { - "@d-fischer/logger": "^4.2.1", - "@d-fischer/promise.allsettled": "^2.0.2", - "@d-fischer/shared-utils": "^3.2.0", - "@types/node": "^12.12.5", - "tslib": "^2.0.3" + "@d-fischer/logger": "^4.2.3", + "@d-fischer/shared-utils": "^3.6.3", + "tslib": "^2.6.2" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" } }, - "node_modules/@d-fischer/rate-limiter/node_modules/@types/node": { - "version": "12.20.55", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", - "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" - }, "node_modules/@d-fischer/shared-utils": { "version": "3.6.3", "resolved": "https://registry.npmjs.org/@d-fischer/shared-utils/-/shared-utils-3.6.3.tgz", @@ -2554,19 +2550,19 @@ "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" }, "node_modules/@twurple/api": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@twurple/api/-/api-7.0.6.tgz", - "integrity": "sha512-Fe8haADUI+m4juCuNxtkWBX2HkCU6FA2+2Biq2/KRTp50FVRCeOBdDQpkdM4r+T2KE0NaiTpiH2rtaRodkR/gQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@twurple/api/-/api-7.1.0.tgz", + "integrity": "sha512-cDVVY+vecMFNEOyp7UobQn4ARydIDf04NZy1YCKIKpJHBuOV/pkTjNGluRZ0nR9/t9hBFfOyHAH4JswRZpZbnw==", "dependencies": { - "@d-fischer/cache-decorators": "^3.0.0", + "@d-fischer/cache-decorators": "^4.0.0", "@d-fischer/cross-fetch": "^5.0.1", "@d-fischer/detect-node": "^3.0.1", "@d-fischer/logger": "^4.2.1", - "@d-fischer/rate-limiter": "^0.7.2", + "@d-fischer/rate-limiter": "^1.0.0", "@d-fischer/shared-utils": "^3.6.1", "@d-fischer/typed-event-emitter": "^3.3.1", - "@twurple/api-call": "7.0.6", - "@twurple/common": "7.0.6", + "@twurple/api-call": "7.1.0", + "@twurple/common": "7.1.0", "retry": "^0.13.1", "tslib": "^2.0.3" }, @@ -2574,7 +2570,7 @@ "url": "https://github.com/sponsors/d-fischer" }, "peerDependencies": { - "@twurple/auth": "7.0.6" + "@twurple/auth": "7.1.0" } }, "node_modules/@twurple/api-call": { @@ -2593,23 +2589,32 @@ "url": "https://github.com/sponsors/d-fischer" } }, + "node_modules/@twurple/api/node_modules/@d-fischer/cache-decorators": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@d-fischer/cache-decorators/-/cache-decorators-4.0.1.tgz", + "integrity": "sha512-HNYLBLWs/t28GFZZeqdIBqq8f37mqDIFO6xNPof94VjpKvuP6ROqCZGafx88dk5zZUlBfViV9jD8iNNlXfc4CA==", + "dependencies": { + "@d-fischer/shared-utils": "^3.6.3", + "tslib": "^2.6.2" + } + }, "node_modules/@twurple/api/node_modules/@d-fischer/cross-fetch": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@d-fischer/cross-fetch/-/cross-fetch-5.0.3.tgz", - "integrity": "sha512-PAxxY2MJff3DUZP6uYWAo0gvp7lGry8SjZ07H661RBnJviy91o+NWR5C7E67dMvrSGvfA1kZC0xrwk+v4eTcMA==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@d-fischer/cross-fetch/-/cross-fetch-5.0.5.tgz", + "integrity": "sha512-symjDUPInTrkfIsZc2n2mo9hiAJLcTJsZkNICjZajEWnWpJ3s3zn50/FY8xpNUAf5w3eFuQii2wxztTGpvG1Xg==", "dependencies": { "node-fetch": "^2.6.12" } }, "node_modules/@twurple/api/node_modules/@twurple/api-call": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@twurple/api-call/-/api-call-7.0.6.tgz", - "integrity": "sha512-3E9IAJRyRLji9bjmZzicWv/wP9aGVdIDkzaX4WaK3sTRC9EkZ3nWIxSj0WmJ977O2lDsUkpgxzwmskuDqdaTrQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@twurple/api-call/-/api-call-7.1.0.tgz", + "integrity": "sha512-aiyV492StnILyFzU/Eqgn+BA8fz125sB/0QJVlCJotMolrZxBkA4NsFEGDOcR3rOJLL7zOKPYMhWI8zY0gfzPA==", "dependencies": { "@d-fischer/cross-fetch": "^5.0.1", "@d-fischer/qs": "^7.0.2", "@d-fischer/shared-utils": "^3.6.1", - "@twurple/common": "7.0.6", + "@twurple/common": "7.1.0", "tslib": "^2.0.3" }, "funding": { @@ -2617,9 +2622,9 @@ } }, "node_modules/@twurple/api/node_modules/@twurple/common": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@twurple/common/-/common-7.0.6.tgz", - "integrity": "sha512-38cufsx5k1ruUNrorXsiyTVBnFCXPFgr+MMkpMoFnh3T5G9drT3RkmE6Ss7oeaK6Glr/ihQNIPL6NzTJkGmXhA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@twurple/common/-/common-7.1.0.tgz", + "integrity": "sha512-kz3J9J116+aOdyhCzRQwaxFC5eAewwQ9Iv2UmPHXYqRfbgKay6TsL27vk+Q2HygBCvx/8OWpX3pdSo3V/VPmoA==", "dependencies": { "@d-fischer/shared-utils": "^3.6.1", "klona": "^2.0.4", @@ -2657,15 +2662,15 @@ } }, "node_modules/@twurple/auth": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@twurple/auth/-/auth-7.0.6.tgz", - "integrity": "sha512-kgjSdLRW9NKk9LD8dySkgjXDxzJQFAiNIBZFbQNgTqSn7A2ZwaxmapIxy7FcHyGV0MdRtimScnRbKK9Nm089Xw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@twurple/auth/-/auth-7.1.0.tgz", + "integrity": "sha512-OT7XtoXeYA8yLvCKdIZ76x71D/RfxPZQqufpimy5ZSL4+TpxY1CJNFp8YWstC1KEfyGVwyr7ZoV49u95k0JJmw==", "dependencies": { "@d-fischer/logger": "^4.2.1", "@d-fischer/shared-utils": "^3.6.1", "@d-fischer/typed-event-emitter": "^3.3.1", - "@twurple/api-call": "7.0.6", - "@twurple/common": "7.0.6", + "@twurple/api-call": "7.1.0", + "@twurple/common": "7.1.0", "tslib": "^2.0.3" }, "funding": { @@ -2673,22 +2678,22 @@ } }, "node_modules/@twurple/auth/node_modules/@d-fischer/cross-fetch": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@d-fischer/cross-fetch/-/cross-fetch-5.0.3.tgz", - "integrity": "sha512-PAxxY2MJff3DUZP6uYWAo0gvp7lGry8SjZ07H661RBnJviy91o+NWR5C7E67dMvrSGvfA1kZC0xrwk+v4eTcMA==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@d-fischer/cross-fetch/-/cross-fetch-5.0.5.tgz", + "integrity": "sha512-symjDUPInTrkfIsZc2n2mo9hiAJLcTJsZkNICjZajEWnWpJ3s3zn50/FY8xpNUAf5w3eFuQii2wxztTGpvG1Xg==", "dependencies": { "node-fetch": "^2.6.12" } }, "node_modules/@twurple/auth/node_modules/@twurple/api-call": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@twurple/api-call/-/api-call-7.0.6.tgz", - "integrity": "sha512-3E9IAJRyRLji9bjmZzicWv/wP9aGVdIDkzaX4WaK3sTRC9EkZ3nWIxSj0WmJ977O2lDsUkpgxzwmskuDqdaTrQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@twurple/api-call/-/api-call-7.1.0.tgz", + "integrity": "sha512-aiyV492StnILyFzU/Eqgn+BA8fz125sB/0QJVlCJotMolrZxBkA4NsFEGDOcR3rOJLL7zOKPYMhWI8zY0gfzPA==", "dependencies": { "@d-fischer/cross-fetch": "^5.0.1", "@d-fischer/qs": "^7.0.2", "@d-fischer/shared-utils": "^3.6.1", - "@twurple/common": "7.0.6", + "@twurple/common": "7.1.0", "tslib": "^2.0.3" }, "funding": { @@ -2696,9 +2701,9 @@ } }, "node_modules/@twurple/auth/node_modules/@twurple/common": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@twurple/common/-/common-7.0.6.tgz", - "integrity": "sha512-38cufsx5k1ruUNrorXsiyTVBnFCXPFgr+MMkpMoFnh3T5G9drT3RkmE6Ss7oeaK6Glr/ihQNIPL6NzTJkGmXhA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@twurple/common/-/common-7.1.0.tgz", + "integrity": "sha512-kz3J9J116+aOdyhCzRQwaxFC5eAewwQ9Iv2UmPHXYqRfbgKay6TsL27vk+Q2HygBCvx/8OWpX3pdSo3V/VPmoA==", "dependencies": { "@d-fischer/shared-utils": "^3.6.1", "klona": "^2.0.4", @@ -2728,31 +2733,40 @@ } }, "node_modules/@twurple/chat": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@twurple/chat/-/chat-7.0.6.tgz", - "integrity": "sha512-sHfJ6oYb8+++qX8KGlKOlkkJnXbCHmslRwv5xMr88eze/4x/URcx+nHGTcRpzgk0qtSDdIDfCFcH2t/TWE5lBw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@twurple/chat/-/chat-7.1.0.tgz", + "integrity": "sha512-AzLtq+xqbyYpqPZau5jvX3Dov+C7MW1YTunYZZ5TqyQlEb/leUD6LdwdhXhsVxQUfpVD1FhU1NSlNd7VEhv0Rg==", "dependencies": { - "@d-fischer/cache-decorators": "^3.0.0", + "@d-fischer/cache-decorators": "^4.0.0", "@d-fischer/deprecate": "^2.0.2", "@d-fischer/logger": "^4.2.1", - "@d-fischer/rate-limiter": "^0.7.2", + "@d-fischer/rate-limiter": "^1.0.0", "@d-fischer/shared-utils": "^3.6.1", "@d-fischer/typed-event-emitter": "^3.3.0", - "@twurple/common": "7.0.6", - "ircv3": "^0.32.3", + "@twurple/common": "7.1.0", + "ircv3": "^0.33.0", "tslib": "^2.0.3" }, "funding": { "url": "https://github.com/sponsors/d-fischer" }, "peerDependencies": { - "@twurple/auth": "7.0.6" + "@twurple/auth": "7.1.0" + } + }, + "node_modules/@twurple/chat/node_modules/@d-fischer/cache-decorators": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@d-fischer/cache-decorators/-/cache-decorators-4.0.1.tgz", + "integrity": "sha512-HNYLBLWs/t28GFZZeqdIBqq8f37mqDIFO6xNPof94VjpKvuP6ROqCZGafx88dk5zZUlBfViV9jD8iNNlXfc4CA==", + "dependencies": { + "@d-fischer/shared-utils": "^3.6.3", + "tslib": "^2.6.2" } }, "node_modules/@twurple/chat/node_modules/@twurple/common": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@twurple/common/-/common-7.0.6.tgz", - "integrity": "sha512-38cufsx5k1ruUNrorXsiyTVBnFCXPFgr+MMkpMoFnh3T5G9drT3RkmE6Ss7oeaK6Glr/ihQNIPL6NzTJkGmXhA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@twurple/common/-/common-7.1.0.tgz", + "integrity": "sha512-kz3J9J116+aOdyhCzRQwaxFC5eAewwQ9Iv2UmPHXYqRfbgKay6TsL27vk+Q2HygBCvx/8OWpX3pdSo3V/VPmoA==", "dependencies": { "@d-fischer/shared-utils": "^3.6.1", "klona": "^2.0.4", @@ -2776,16 +2790,16 @@ } }, "node_modules/@twurple/eventsub-base": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@twurple/eventsub-base/-/eventsub-base-7.0.6.tgz", - "integrity": "sha512-itUQo5c1mZdXpukVC51ZOoWQo+0pBjydbjSVxZ+BBD95a7WmOAnp4FNKl8caqfSlzQaqA7GvmTJCiPreXin3qw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@twurple/eventsub-base/-/eventsub-base-7.1.0.tgz", + "integrity": "sha512-3FNmSwhf09yWYQwhkc+EjmEngbMLbmMPJvJ4m30X9duuhFqvZcd0XnnRvHWSd4qpiolJb0BPerqP2EGXlGjElA==", "dependencies": { "@d-fischer/logger": "^4.2.1", "@d-fischer/shared-utils": "^3.6.1", "@d-fischer/typed-event-emitter": "^3.3.0", - "@twurple/api": "7.0.6", - "@twurple/auth": "7.0.6", - "@twurple/common": "7.0.6", + "@twurple/api": "7.1.0", + "@twurple/auth": "7.1.0", + "@twurple/common": "7.1.0", "tslib": "^2.0.3" }, "funding": { @@ -2793,9 +2807,9 @@ } }, "node_modules/@twurple/eventsub-base/node_modules/@twurple/common": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@twurple/common/-/common-7.0.6.tgz", - "integrity": "sha512-38cufsx5k1ruUNrorXsiyTVBnFCXPFgr+MMkpMoFnh3T5G9drT3RkmE6Ss7oeaK6Glr/ihQNIPL6NzTJkGmXhA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@twurple/common/-/common-7.1.0.tgz", + "integrity": "sha512-kz3J9J116+aOdyhCzRQwaxFC5eAewwQ9Iv2UmPHXYqRfbgKay6TsL27vk+Q2HygBCvx/8OWpX3pdSo3V/VPmoA==", "dependencies": { "@d-fischer/shared-utils": "^3.6.1", "klona": "^2.0.4", @@ -2806,30 +2820,30 @@ } }, "node_modules/@twurple/eventsub-ws": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@twurple/eventsub-ws/-/eventsub-ws-7.0.6.tgz", - "integrity": "sha512-n0JsJ7gTEadvd8saHbXHBO77nmvCBROjVIm3itIFELhLgtn3437wxJ+Ne+yuEKwln8WmZn0JWmVCEHoTWGQehw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@twurple/eventsub-ws/-/eventsub-ws-7.1.0.tgz", + "integrity": "sha512-0ZOPAGvStqjBTT2Vjtz6euxgtcb8U4cQ23TOaUssgUYJ6hy34ebmLxt6Ghj/5tJt11sduYAxHRvw+XKTSjGIoA==", "dependencies": { - "@d-fischer/connection": "^8.0.5", + "@d-fischer/connection": "^9.0.0", "@d-fischer/logger": "^4.2.1", "@d-fischer/shared-utils": "^3.6.1", "@d-fischer/typed-event-emitter": "^3.3.0", - "@twurple/auth": "7.0.6", - "@twurple/common": "7.0.6", - "@twurple/eventsub-base": "7.0.6", + "@twurple/auth": "7.1.0", + "@twurple/common": "7.1.0", + "@twurple/eventsub-base": "7.1.0", "tslib": "^2.0.3" }, "funding": { "url": "https://github.com/sponsors/d-fischer" }, "peerDependencies": { - "@twurple/api": "7.0.6" + "@twurple/api": "7.1.0" } }, "node_modules/@twurple/eventsub-ws/node_modules/@twurple/common": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@twurple/common/-/common-7.0.6.tgz", - "integrity": "sha512-38cufsx5k1ruUNrorXsiyTVBnFCXPFgr+MMkpMoFnh3T5G9drT3RkmE6Ss7oeaK6Glr/ihQNIPL6NzTJkGmXhA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@twurple/common/-/common-7.1.0.tgz", + "integrity": "sha512-kz3J9J116+aOdyhCzRQwaxFC5eAewwQ9Iv2UmPHXYqRfbgKay6TsL27vk+Q2HygBCvx/8OWpX3pdSo3V/VPmoA==", "dependencies": { "@d-fischer/shared-utils": "^3.6.1", "klona": "^2.0.4", @@ -2840,28 +2854,28 @@ } }, "node_modules/@twurple/pubsub": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@twurple/pubsub/-/pubsub-7.0.6.tgz", - "integrity": "sha512-RM6t3pFpyHcF5c9RtPP3MC3IkhqYiiag8mHIDMVnhmBRfjTeANuDP/Rex6xzREx/tQlTYQ9vM8K1pbrKrFgPCg==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@twurple/pubsub/-/pubsub-7.1.0.tgz", + "integrity": "sha512-2YMiktQbHPiPqCNzdlQ2OOMLVHNiy5tjRImkVBHNjw0tqCsbrCUhmFwgKjiIiDQUTTzOcNdFhDNhlcgy5Mz65A==", "dependencies": { - "@d-fischer/connection": "^8.0.5", + "@d-fischer/connection": "^9.0.0", "@d-fischer/logger": "^4.2.1", "@d-fischer/shared-utils": "^3.6.1", "@d-fischer/typed-event-emitter": "^3.3.0", - "@twurple/common": "7.0.6", + "@twurple/common": "7.1.0", "tslib": "^2.0.3" }, "funding": { "url": "https://github.com/sponsors/d-fischer" }, "peerDependencies": { - "@twurple/auth": "7.0.6" + "@twurple/auth": "7.1.0" } }, "node_modules/@twurple/pubsub/node_modules/@twurple/common": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@twurple/common/-/common-7.0.6.tgz", - "integrity": "sha512-38cufsx5k1ruUNrorXsiyTVBnFCXPFgr+MMkpMoFnh3T5G9drT3RkmE6Ss7oeaK6Glr/ihQNIPL6NzTJkGmXhA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@twurple/common/-/common-7.1.0.tgz", + "integrity": "sha512-kz3J9J116+aOdyhCzRQwaxFC5eAewwQ9Iv2UmPHXYqRfbgKay6TsL27vk+Q2HygBCvx/8OWpX3pdSo3V/VPmoA==", "dependencies": { "@d-fischer/shared-utils": "^3.6.1", "klona": "^2.0.4", @@ -3056,9 +3070,9 @@ "integrity": "sha512-7axfYN8SW9pWg78NgenHasSproWQee5rzyPVLC9HpaQSDgNArsnKJD88EaMfi4Pl48AyciO3agYCFqpHS1gLpg==" }, "node_modules/@types/ws": { - "version": "8.5.5", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz", - "integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==", + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", "dependencies": { "@types/node": "*" } @@ -8569,11 +8583,11 @@ } }, "node_modules/ircv3": { - "version": "0.32.3", - "resolved": "https://registry.npmjs.org/ircv3/-/ircv3-0.32.3.tgz", - "integrity": "sha512-H0ejwbIPzJO73PPGJGrdEDrqRxxK0f+6/7kNZ7yY/ukTDZ8zy7w7mN4wMbmOFrnPYO78ZKnbeqmkBRgcVJ552Q==", + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/ircv3/-/ircv3-0.33.0.tgz", + "integrity": "sha512-7rK1Aial3LBiFycE8w3MHiBBFb41/2GG2Ll/fR2IJj1vx0pLpn1s+78K+z/I4PZTqCCSp/Sb4QgKMh3NMhx0Kg==", "dependencies": { - "@d-fischer/connection": "^8.0.5", + "@d-fischer/connection": "^9.0.0", "@d-fischer/escape-string-regexp": "^5.0.0", "@d-fischer/logger": "^4.2.1", "@d-fischer/shared-utils": "^3.5.0", @@ -10154,9 +10168,9 @@ } }, "node_modules/obs-websocket-js": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/obs-websocket-js/-/obs-websocket-js-5.0.3.tgz", - "integrity": "sha512-lEsDKVlSgXQ7p0nLuL8DER9SzOBrpqOo7fUO1m0zVRjdUT8pCSCEPb9Xn0I6XH3xoe51fWGybFYo6bN9cO51Pw==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/obs-websocket-js/-/obs-websocket-js-5.0.5.tgz", + "integrity": "sha512-mSMqLXJ4z28jgwy7Ecv8CtpYh/xdbcn524kq0n6wT3kN6xkgWU/Zc6OtiVZo+gyyylC0anjehMLEVF+CDSwccw==", "dependencies": { "@msgpack/msgpack": "^2.7.1", "crypto-js": "^4.1.1", @@ -12386,9 +12400,9 @@ "integrity": "sha512-e4g0EJtAjk64xgnFPD6kTBUtpnMVzDrMb12N1YZV0VvSlhnVT3SGxiYTLdGy8Q5cYHOIC/FAHmZ10eGrAguicQ==" }, "node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tsutils": { "version": "3.21.0", diff --git a/package.json b/package.json index 476fc5149..cb909c8de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebotv5", - "version": "5.61.2", + "version": "5.62.0", "description": "Powerful all-in-one bot for Twitch streamers.", "main": "build/main.js", "scripts": { @@ -46,11 +46,11 @@ "@crowbartools/firebot-custom-scripts-types": "^5.53.2-6", "@nut-tree/nut-js": "^3.1.1", "@seald-io/nedb": "^4.0.4", - "@twurple/api": "^7.0.6", - "@twurple/auth": "^7.0.6", - "@twurple/chat": "^7.0.6", - "@twurple/eventsub-ws": "^7.0.6", - "@twurple/pubsub": "^7.0.6", + "@twurple/api": "^7.1.0", + "@twurple/auth": "^7.1.0", + "@twurple/chat": "^7.1.0", + "@twurple/eventsub-ws": "^7.1.0", + "@twurple/pubsub": "^7.1.0", "@zunderscore/elgato-light-control": "^1.1.2", "angular": "^1.8.0", "angular-animate": "^1.7.8", @@ -112,7 +112,7 @@ "node-hue-api": "^4.0.11", "node-json-db": "^1.4.1", "node-xlsx": "^0.20.0", - "obs-websocket-js": "^5.0.3", + "obs-websocket-js": "^5.0.5", "request": "^2.85.0", "roll": "^1.2.0", "sanitize-filename": "^1.6.3", diff --git a/src/backend/app-management/electron/events/when-ready.js b/src/backend/app-management/electron/events/when-ready.js index 8fd2d8c7b..30ce5f8fd 100644 --- a/src/backend/app-management/electron/events/when-ready.js +++ b/src/backend/app-management/electron/events/when-ready.js @@ -123,12 +123,19 @@ exports.whenReady = async () => { windowManagement.updateSplashScreenStatus("Loading custom roles..."); const customRolesManager = require("../../../roles/custom-roles-manager"); - customRolesManager.loadCustomRoles(); + await customRolesManager.loadCustomRoles(); - windowManagement.updateSplashScreenStatus("Loading known bot list..."); const chatRolesManager = require("../../../roles/chat-roles-manager"); + + windowManagement.updateSplashScreenStatus("Loading known bot list..."); await chatRolesManager.cacheViewerListBots(); + windowManagement.updateSplashScreenStatus("Loading channel moderators..."); + await chatRolesManager.loadModerators(); + + windowManagement.updateSplashScreenStatus("Loading channel VIPs..."); + await chatRolesManager.loadVips(); + windowManagement.updateSplashScreenStatus("Loading effect queues..."); const effectQueueManager = require("../../../effects/queues/effect-queue-manager"); effectQueueManager.loadItems(); diff --git a/src/backend/app-management/electron/events/windows-all-closed.js b/src/backend/app-management/electron/events/windows-all-closed.js index de7ead106..154e7b96b 100644 --- a/src/backend/app-management/electron/events/windows-all-closed.js +++ b/src/backend/app-management/electron/events/windows-all-closed.js @@ -14,6 +14,10 @@ exports.windowsAllClosed = async () => { const scheduledTaskManager = require("../../../timers/scheduled-task-manager"); scheduledTaskManager.stop(); + // Stop all custom scripts so they can clean up + const customScriptRunner = require("../../../common/handlers/custom-scripts/custom-script-runner"); + await customScriptRunner.stopAllScripts(); + // Unregister all shortcuts. const hotkeyManager = require("../../../hotkeys/hotkey-manager"); hotkeyManager.unregisterAllHotkeys(); diff --git a/src/backend/app-management/electron/window-management.js b/src/backend/app-management/electron/window-management.js index b208f13ae..f08199b45 100644 --- a/src/backend/app-management/electron/window-management.js +++ b/src/backend/app-management/electron/window-management.js @@ -15,6 +15,13 @@ const { settings } = require("../../common/settings-access"); setupTitlebar(); + +/** + * The variable inspector window. + *@type {Electron.BrowserWindow} + */ +let variableInspectorWindow = null; + /** * The stream preview popout window. * Keeps a global reference of the window object, if you don't, the window will @@ -83,8 +90,8 @@ function createStreamPreviewWindow() { streamPreviewWindowState.manage(streamPreview); streamPreview.on("close", () => { - if (!view.isDestroyed()) { - view.destroy(); + if (!view.webContents.isDestroyed()) { + view.webContents.destroy(); } }); } @@ -428,7 +435,7 @@ async function createMainWindow() { ); // wait for the main window's content to load, then show it - mainWindow.webContents.on("did-finish-load", () => { + mainWindow.webContents.on("did-finish-load", async () => { createTray(mainWindow); @@ -442,7 +449,7 @@ async function createMainWindow() { } const startupScriptsManager = require("../../common/handlers/custom-scripts/startup-scripts-manager"); - startupScriptsManager.runStartupScripts(); + await startupScriptsManager.runStartupScripts(); const eventManager = require("../../events/EventManager"); eventManager.triggerEvent("firebot", "firebot-started", { @@ -470,8 +477,24 @@ async function createMainWindow() { }).then(({response}) => { if (response === 0) { mainWindow.destroy(); + global.renderWindow = null; } }).catch(() => console.log("Error with close app confirmation")); + } else { + mainWindow.destroy(); + global.renderWindow = null; + } + }); + + mainWindow.on("closed", () => { + if (variableInspectorWindow?.isDestroyed() === false) { + logger.debug("Closing variable inspector window"); + variableInspectorWindow.destroy(); + } + + if (streamPreview?.isDestroyed() === false) { + logger.debug("Closing stream preview window"); + streamPreview.destroy(); } }); } @@ -525,12 +548,6 @@ function updateSplashScreenStatus(newStatus) { splashscreenWindow.webContents.send("update-splash-screen-status", newStatus); } -/** - * The variable inspector window. - *@type {Electron.BrowserWindow} - */ -let variableInspectorWindow = null; - async function createVariableInspectorWindow() { if (variableInspectorWindow != null && !variableInspectorWindow.isDestroyed()) { diff --git a/src/backend/auth/twitch-auth.ts b/src/backend/auth/twitch-auth.ts index 9615c8579..66f376001 100644 --- a/src/backend/auth/twitch-auth.ts +++ b/src/backend/auth/twitch-auth.ts @@ -66,21 +66,27 @@ class TwitchAuthProviders { 'moderator:manage:chat_settings', 'moderator:manage:shield_mode', 'moderator:manage:shoutouts', + 'moderator:manage:unban_requests', 'moderator:read:automod_settings', 'moderator:read:blocked_terms', 'moderator:read:chat_settings', 'moderator:read:chatters', 'moderator:read:followers', + 'moderator:read:moderators', 'moderator:read:shield_mode', 'moderator:read:shoutouts', + 'moderator:read:unban_requests', + 'moderator:read:vips', 'user:edit:broadcast', 'user:manage:blocked_users', 'user:manage:whispers', 'user:read:blocked_users', 'user:read:broadcast', 'user:read:chat', + 'user:read:emotes', 'user:read:follows', 'user:read:subscriptions', + 'user:write:chat', 'whispers:edit', 'whispers:read' ] @@ -105,6 +111,8 @@ class TwitchAuthProviders { 'moderator:manage:announcements', 'user:manage:whispers', 'user:read:chat', + 'user:read:emotes', + 'user:write:chat', 'whispers:edit', 'whispers:read' ] @@ -141,7 +149,7 @@ async function getUserCurrent(accessToken: string) { return null; } -authManager.on("auth-success", async authData => { +authManager.on("auth-success", async (authData) => { const { providerId, tokenData } = authData; if (providerId === twitchAuthProviders.streamerAccountProviderId diff --git a/src/backend/channel-rewards/channel-reward-manager.ts b/src/backend/channel-rewards/channel-reward-manager.ts index efb9eb7e7..f1bee5012 100644 --- a/src/backend/channel-rewards/channel-reward-manager.ts +++ b/src/backend/channel-rewards/channel-reward-manager.ts @@ -4,13 +4,69 @@ import accountAccess from "../common/account-access"; import profileManager from "../common/profile-manager"; import frontendCommunicator from "../common/frontend-communicator"; import twitchApi from "../twitch-api/api"; -import { CustomReward } from "../twitch-api/resource/channel-rewards"; +import { CustomReward, RewardRedemption, RewardRedemptionsApprovalRequest } from "../twitch-api/resource/channel-rewards"; import { EffectTrigger } from "../../shared/effect-constants"; import { RewardRedemptionMetadata, SavedChannelReward } from "../../types/channel-rewards"; - +import { TriggerType } from "../common/EffectType"; class ChannelRewardManager { channelRewards: Record = {}; + private _channelRewardRedemptions: Record = {}; + + constructor() { + frontendCommunicator.onAsync("get-channel-reward-count", + twitchApi.channelRewards.getTotalChannelRewardCount); + + frontendCommunicator.onAsync("get-channel-rewards", async () => Object.values(this.channelRewards)); + + frontendCommunicator.onAsync("save-channel-reward", + (channelReward: SavedChannelReward) => this.saveChannelReward(channelReward)); + + frontendCommunicator.onAsync("save-all-channel-rewards", + async (data: { channelRewards: SavedChannelReward[]; updateTwitch: boolean}) => + await this.saveAllChannelRewards(data.channelRewards, data.updateTwitch)); + + frontendCommunicator.onAsync("sync-channel-rewards", async (): Promise => { + await this.loadChannelRewards(); + return Object.values(this.channelRewards); + }); + + frontendCommunicator.onAsync("delete-channel-reward", async (channelRewardId: string) => { + await this.deleteChannelReward(channelRewardId); + }); + + frontendCommunicator.on("manually-trigger-reward", (channelRewardId: string) => { + const savedReward = this.channelRewards[channelRewardId]; + + if (savedReward == null) { + return; + } + + const accountAccess = require("../common/account-access"); + + this.triggerChannelReward(channelRewardId, { + messageText: "Testing reward", + redemptionId: "test-redemption-id", + rewardId: savedReward.id, + rewardCost: savedReward.twitchData.cost, + rewardImage: savedReward.twitchData.image ? savedReward.twitchData.image.url4x : savedReward.twitchData.defaultImage.url4x, + rewardName: savedReward.twitchData.title, + username: accountAccess.getAccounts().streamer.displayName + }, true); + }); + + frontendCommunicator.onAsync("refresh-channel-reward-redemptions", async () => { + await this.refreshChannelRewardRedemptions(); + }); + + frontendCommunicator.onAsync("approve-reject-channel-reward-redemptions", async (request: RewardRedemptionsApprovalRequest) => { + await this.approveOrRejectChannelRewardRedemptions(request); + }); + + frontendCommunicator.onAsync("approve-reject-channel-all-redemptions-for-rewards", async (request: { rewardIds: string[], approve?: boolean }) => { + await this.approveOrRejectAllRedemptionsForChannelRewards(request.rewardIds, request.approve); + }); + } getChannelRewardsDb(): JsonDB { return profileManager @@ -40,7 +96,7 @@ class ChannelRewardManager { // Determine new manageable rewards const newManageableChannelRewards = twitchManageableRewards .filter(nr => rewards.every(r => r.id !== nr.id)) - .map(nr => { + .map((nr) => { return { id: nr.id, manageable: true, @@ -59,7 +115,7 @@ class ChannelRewardManager { // Determine new unmanageable rewards const newTwitchUnmanageableRewards: SavedChannelReward[] = twitchUnmanageableRewards .filter(ur => rewards.every(r => r.id !== ur.id)) - .map(ur => { + .map((ur) => { return { id: ur.id, manageable: false, @@ -68,7 +124,7 @@ class ChannelRewardManager { }); // Sync current reward Twitch data/manageability status, remove deleted rewards, then add new rewards - const syncedRewards: Record = rewards.map(r => { + const syncedRewards: Record = rewards.map((r) => { const rewardTwitchData = twitchManageableRewards.find(tc => tc.id === r.id); // If we have a match, this is a manageable reward @@ -209,6 +265,67 @@ class ChannelRewardManager { return; } + const restrictionData = savedReward.restrictionData; + if (restrictionData) { + logger.debug("Reward has restrictions...checking them."); + const restrictionsManager = require("../restrictions/restriction-manager"); + const triggerData = { + type: TriggerType.CHANNEL_REWARD, + metadata + }; + + const shouldAutoApproveOrReject = savedReward.manageable && + !savedReward.twitchData.shouldRedemptionsSkipRequestQueue && + savedReward.autoApproveRedemptions; + + try { + await restrictionsManager.runRestrictionPredicates(triggerData, savedReward.restrictionData); + logger.debug("Restrictions passed!"); + if (shouldAutoApproveOrReject) { + logger.debug("auto accepting redemption"); + this.approveOrRejectChannelRewardRedemptions({ + rewardId, + redemptionIds: [metadata.redemptionId], + approve: true + }); + } + } catch (restrictionReason) { + let reason; + if (Array.isArray(restrictionReason)) { + reason = restrictionReason.join(", "); + } else { + reason = restrictionReason; + } + + logger.debug(`${metadata.username} could not use Reward '${savedReward.twitchData.title}' because: ${reason}`); + if (restrictionData.sendFailMessage || restrictionData.sendFailMessage == null) { + + const restrictionMessage = restrictionData.useCustomFailMessage ? + restrictionData.failMessage : + "Sorry @{user}, you cannot use this channel reward because: {reason}"; + + const twitchChat = require("../chat/twitch-chat"); + await twitchChat.sendChatMessage( + restrictionMessage + .replace("{user}", metadata.username) + .replace("{reason}", reason) + ); + } + + if (shouldAutoApproveOrReject) { + logger.debug("auto rejecting redemption"); + this.approveOrRejectChannelRewardRedemptions({ + rewardId, + redemptionIds: [metadata.redemptionId], + approve: false + }); + } + + return false; + } + } + + const effectRunner = require("../common/effect-runner"); const processEffectsRequest = { @@ -225,49 +342,38 @@ class ChannelRewardManager { console.log(`error when running effects: ${reason}`); } } -} -const channelRewardManager = new ChannelRewardManager(); - -frontendCommunicator.onAsync("getChannelRewardCount", - twitchApi.channelRewards.getTotalChannelRewardCount); + async refreshChannelRewardRedemptions(): Promise { + if (accountAccess.getAccounts().streamer.broadcasterType === "") { + return; + } -frontendCommunicator.onAsync("getChannelRewards", async () => Object.values(channelRewardManager.channelRewards)); + this._channelRewardRedemptions = await twitchApi.channelRewards.getOpenChannelRewardRedemptions(); -frontendCommunicator.onAsync("saveChannelReward", - (channelReward: SavedChannelReward) => channelRewardManager.saveChannelReward(channelReward)); + frontendCommunicator.send("channel-reward-redemptions-updated", this.getChannelRewardRedemptions()); + } -frontendCommunicator.onAsync("saveAllChannelRewards", - async (data: { channelRewards: SavedChannelReward[]; updateTwitch: boolean}) => - await channelRewardManager.saveAllChannelRewards(data.channelRewards, data.updateTwitch)); + getChannelRewardRedemptions(): Record { + return this._channelRewardRedemptions ?? {}; + } -frontendCommunicator.onAsync("syncChannelRewards", async (): Promise => { - await channelRewardManager.loadChannelRewards(); - return Object.values(channelRewardManager.channelRewards); -}); + async approveOrRejectChannelRewardRedemptions(request: RewardRedemptionsApprovalRequest): Promise { + const successful = await twitchApi.channelRewards.approveOrRejectChannelRewardRedemption(request); -frontendCommunicator.onAsync("deleteChannelReward", async (channelRewardId: string) => { - await channelRewardManager.deleteChannelReward(channelRewardId); -}); + if (successful) { + await this.refreshChannelRewardRedemptions(); + } + } -frontendCommunicator.on("manuallyTriggerReward", (channelRewardId: string) => { - const savedReward = channelRewardManager.channelRewards[channelRewardId]; + async approveOrRejectAllRedemptionsForChannelRewards(rewardIds: string[], approve = true): Promise { + const successful = await twitchApi.channelRewards.approveOrRejectAllRedemptionsForChannelRewards(rewardIds, approve); - if (savedReward == null) { - return; + if (successful) { + await this.refreshChannelRewardRedemptions(); + } } +} - const accountAccess = require("../common/account-access"); - - channelRewardManager.triggerChannelReward(channelRewardId, { - messageText: "Testing reward", - redemptionId: "test-redemption-id", - rewardId: savedReward.id, - rewardCost: savedReward.twitchData.cost, - rewardImage: savedReward.twitchData.image ? savedReward.twitchData.image.url4x : savedReward.twitchData.defaultImage.url4x, - rewardName: savedReward.twitchData.title, - username: accountAccess.getAccounts().streamer.displayName - }, true); -}); +const channelRewardManager = new ChannelRewardManager(); export = channelRewardManager; \ No newline at end of file diff --git a/src/backend/chat/chat-helpers.ts b/src/backend/chat/chat-helpers.ts index 6019d8c75..84f6449da 100644 --- a/src/backend/chat/chat-helpers.ts +++ b/src/backend/chat/chat-helpers.ts @@ -193,8 +193,8 @@ class FirebotChatHelpers { return parts.flatMap((p) => { if (p.type === "text" && p.text != null) { - if (firebotChatMessage.username !== streamer.displayName && - (!bot.loggedIn || firebotChatMessage.username !== bot.displayName)) { + if (firebotChatMessage.username !== streamer.username && + (!bot.loggedIn || firebotChatMessage.username !== bot.username)) { if (!firebotChatMessage.whisper && !firebotChatMessage.tagged && streamer.loggedIn && @@ -346,9 +346,8 @@ class FirebotChatHelpers { buildBasicFirebotChatMessage(msgText: string, username: string): FirebotChatMessage { return { id: null, - userIdName: null, - userId: null, username: username, + userId: null, rawText: msgText, whisper: false, action: false, @@ -362,9 +361,9 @@ class FirebotChatHelpers { async buildFirebotChatMessage(msg: ChatMessage, msgText: string, whisper = false, action = false) { const firebotChatMessage: FirebotChatMessage = { id: msg.tags.get("id"), - username: msg.userInfo.displayName, - userIdName: msg.userInfo.userName, + username: msg.userInfo.userName, userId: msg.userInfo.userId, + userDisplayName: msg.userInfo.displayName, customRewardId: msg.tags.get("custom-reward-id") || undefined, isHighlighted: msg.tags.get("msg-id") === "highlighted-message", isAnnouncement: false, @@ -442,12 +441,12 @@ class FirebotChatHelpers { firebotChatMessage.isSubscriber = msg.userInfo.isSubscriber; firebotChatMessage.isVip = msg.userInfo.isVip; - if (streamer.loggedIn && firebotChatMessage.username === streamer.displayName) { + if (streamer.loggedIn && firebotChatMessage.username === streamer.username) { firebotChatMessage.isBroadcaster = true; firebotChatMessage.roles.push("broadcaster"); } - if (bot.loggedIn && firebotChatMessage.username === bot.displayName) { + if (bot.loggedIn && firebotChatMessage.username === bot.username) { firebotChatMessage.isBot = true; firebotChatMessage.roles.push("bot"); } @@ -478,8 +477,8 @@ class FirebotChatHelpers { const firebotChatMessage: FirebotChatMessage = { id: id, username: extensionName, - userIdName: extensionName, userId: extensionName, + userDisplayName: extensionName, rawText: text, profilePicUrl: extensionIconUrl, whisper: false, @@ -513,9 +512,9 @@ class FirebotChatHelpers { const viewerFirebotChatMessage: FirebotChatMessage = { id: msg.messageId, - username: msg.senderDisplayName, - userIdName: msg.senderName, + username: msg.senderName, userId: msg.senderId, + userDisplayName: msg.senderDisplayName, rawText: msg.messageContent, profilePicUrl: profilePicUrl, whisper: false, diff --git a/src/backend/chat/chat-listeners/active-user-handler.js b/src/backend/chat/chat-listeners/active-user-handler.js index 464e73a72..168f11298 100644 --- a/src/backend/chat/chat-listeners/active-user-handler.js +++ b/src/backend/chat/chat-listeners/active-user-handler.js @@ -13,13 +13,13 @@ const ONLINE_TIMEOUT = 450; // 7.50 mins /** * Simple User * @typedef {Object} User - * @property {id} id + * @property {string} id * @property {string} username */ /** * @typedef {Object} UserDetails - * @property {number} id + * @property {string} id * @property {string} username * @property {string} displayName * @property {string} profilePicUrl @@ -156,7 +156,7 @@ async function updateUserOnlineStatus(userDetails, updateDb = false) { frontendCommunicator.send("twitch:chat:user-joined", { id: userDetails.id, - username: userDetails.displayName, + username: userDetails.username, displayName: userDetails.displayName, roles: roles, profilePicUrl: userDetails.profilePicUrl, diff --git a/src/backend/chat/chat-listeners/twitch-chat-listeners.js b/src/backend/chat/chat-listeners/twitch-chat-listeners.js index b06e5d9e2..22607a9e7 100644 --- a/src/backend/chat/chat-listeners/twitch-chat-listeners.js +++ b/src/backend/chat/chat-listeners/twitch-chat-listeners.js @@ -40,9 +40,9 @@ exports.setupChatListeners = (streamerChatClient, botChatClient) => { frontendCommunicator.send("twitch:chat:message", firebotChatMessage); twitchEventsHandler.announcement.triggerAnnouncement( - firebotChatMessage.userIdName, - firebotChatMessage.userId, firebotChatMessage.username, + firebotChatMessage.userId, + firebotChatMessage.userDisplayName, firebotChatMessage.roles, firebotChatMessage.rawText ); @@ -54,9 +54,13 @@ exports.setupChatListeners = (streamerChatClient, botChatClient) => { await chatModerationManager.moderateMessage(firebotChatMessage); if (firebotChatMessage.isVip === true) { - chatRolesManager.addVipToVipList(firebotChatMessage.username); + chatRolesManager.addVipToVipList({ + id: msg.userInfo.userId, + username: msg.userInfo.userName, + displayName: msg.userInfo.displayName + }); } else { - chatRolesManager.removeVipFromVipList(firebotChatMessage.username); + chatRolesManager.removeVipFromVipList(msg.userInfo.userId); } // send to the frontend @@ -67,7 +71,8 @@ exports.setupChatListeners = (streamerChatClient, botChatClient) => { messageText: firebotChatMessage.rawText, user: { id: firebotChatMessage.userId, - username: firebotChatMessage.username + username: firebotChatMessage.username, + displayName: firebotChatMessage.userDisplayName }, reward: { id: HIGHLIGHT_MESSAGE_REWARD_ID, @@ -85,9 +90,9 @@ exports.setupChatListeners = (streamerChatClient, botChatClient) => { await activeUserHandler.addActiveUser(msg.userInfo, true); twitchEventsHandler.viewerArrived.triggerViewerArrived( - msg.userInfo.displayName, msg.userInfo.userName, msg.userInfo.userId, + msg.userInfo.displayName, messageText, firebotChatMessage ); @@ -102,7 +107,10 @@ exports.setupChatListeners = (streamerChatClient, botChatClient) => { if (firebotChatMessage.isFirstChat) { twitchEventsHandler.chatMessage.triggerFirstTimeChat(firebotChatMessage); } - await raidMessageChecker.sendMessageToCache(firebotChatMessage); + await raidMessageChecker.sendMessageToCache({ + rawText: firebotChatMessage.rawText, + userId: firebotChatMessage.userId + }); }); const whisperHandler = async (_user, messageText, msg, accountType) => { @@ -114,6 +122,8 @@ exports.setupChatListeners = (streamerChatClient, botChatClient) => { twitchEventsHandler.whisper.triggerWhisper( msg.userInfo.userName, + msg.userInfo.userId, + msg.userInfo.displayName, messageText, accountType ); @@ -126,9 +136,13 @@ exports.setupChatListeners = (streamerChatClient, botChatClient) => { const firebotChatMessage = await chatHelpers.buildFirebotChatMessage(msg, messageText, false, true); if (firebotChatMessage.isVip === true) { - chatRolesManager.addVipToVipList(firebotChatMessage.username); + chatRolesManager.addVipToVipList({ + id: msg.userInfo.userId, + username: msg.userInfo.userName, + displayName: msg.userInfo.displayName + }); } else { - chatRolesManager.removeVipFromVipList(firebotChatMessage.username); + chatRolesManager.removeVipFromVipList(msg.userInfo.userId); } frontendCommunicator.send("twitch:chat:message", firebotChatMessage); @@ -141,9 +155,9 @@ exports.setupChatListeners = (streamerChatClient, botChatClient) => { } twitchEventsHandler.viewerArrived.triggerViewerArrived( - msg.userInfo.displayName, msg.userInfo.userName, msg.userInfo.userId, + msg.userInfo.displayName, messageText, firebotChatMessage ); @@ -210,8 +224,8 @@ exports.setupChatListeners = (streamerChatClient, botChatClient) => { streamerChatClient.onGiftPaidUpgrade((_channel, _user, subInfo, msg) => { twitchEventsHandler.giftSub.triggerSubGiftUpgrade( msg.userInfo.userName, - subInfo.displayName, subInfo.userId, + subInfo.displayName, subInfo.gifterDisplayName, subInfo.plan ); @@ -220,8 +234,8 @@ exports.setupChatListeners = (streamerChatClient, botChatClient) => { streamerChatClient.onPrimePaidUpgrade((_channel, _user, subInfo, msg) => { twitchEventsHandler.sub.triggerPrimeUpgrade( msg.userInfo.userName, - subInfo.displayName, subInfo.userId, + subInfo.displayName, subInfo.plan ); }); diff --git a/src/backend/chat/commands/builtin/custom-role-management.ts b/src/backend/chat/commands/builtin/custom-role-management.ts index 4ce85dc45..490db4aed 100644 --- a/src/backend/chat/commands/builtin/custom-role-management.ts +++ b/src/backend/chat/commands/builtin/custom-role-management.ts @@ -1,6 +1,7 @@ import { SystemCommand } from "../../../../types/commands"; import customRoleManager from "../../../roles/custom-roles-manager"; import chat from "../../twitch-chat"; +import twitchApi from "../../../twitch-api/api"; /** * The `!role` command @@ -61,37 +62,56 @@ export const CustomRoleManagementSystemCommand: SystemCommand = { switch (triggeredArg) { case "add": { - const roleName = args.slice(2); + const roleName = args.slice(2)[0]; const role = customRoleManager.getRoleByName(roleName); if (role == null) { await chat.sendChatMessage("Can't find a role by that name."); } else { const username = args[1].replace("@", ""); - customRoleManager.addViewerToRole(role.id, username); - await chat.sendChatMessage(`Added role ${role.name} to ${username}`); + const user = await twitchApi.users.getUserByName(username); + if (user == null) { + await chat.sendChatMessage(`Could not add role ${role.name} to ${username}. User does not exist.`); + } else { + customRoleManager.addViewerToRole(role.id, { + id: user.id, + username: user.name, + displayName: user.displayName + }); + await chat.sendChatMessage(`Added role ${role.name} to ${username}`); + } } break; } case "remove": { - const roleName = args.slice(2); + const roleName = args.slice(2)[0]; const role = customRoleManager.getRoleByName(roleName); if (role == null) { await chat.sendChatMessage("Can't find a role by that name."); } else { const username = args[1].replace("@", ""); - customRoleManager.removeViewerFromRole(role.id, username); - await chat.sendChatMessage(`Removed role ${role.name} from ${username}`); + const user = await twitchApi.users.getUserByName(username); + if (user == null) { + await chat.sendChatMessage(`Could not remove role ${role.name} from ${username}. User does not exist.`); + } else { + customRoleManager.removeViewerFromRole(role.id, user.id); + await chat.sendChatMessage(`Removed role ${role.name} from ${username}`); + } } break; } case "list": { if (args.length > 1) { const username = args[1].replace("@", ""); - const roleNames = customRoleManager.getAllCustomRolesForViewer(username).map((r) => r.name); - if (roleNames.length < 1) { - await chat.sendChatMessage(`${username} has no custom roles assigned.`); + const user = await twitchApi.users.getUserByName(username); + if (user == null) { + await chat.sendChatMessage(`Could not get roles for ${username}. User does not exist.`); } else { - await chat.sendChatMessage(`${username}'s custom roles: ${roleNames.join(", ")}`); + const roleNames = customRoleManager.getAllCustomRolesForViewer(user.id).map((r) => r.name); + if (roleNames.length < 1) { + await chat.sendChatMessage(`${username} has no custom roles assigned.`); + } else { + await chat.sendChatMessage(`${username}'s custom roles: ${roleNames.join(", ")}`); + } } } else { diff --git a/src/backend/chat/commands/chat-command-handler.ts b/src/backend/chat/commands/chat-command-handler.ts index d4d028e7b..01a1ec4ac 100644 --- a/src/backend/chat/commands/chat-command-handler.ts +++ b/src/backend/chat/commands/chat-command-handler.ts @@ -123,13 +123,13 @@ class CommandHandler { const { streamer, bot } = accountAccess.getAccounts(); // check if chat came from the streamer and if we should ignore it. - if (command.ignoreStreamer && firebotChatMessage.username === streamer.displayName) { + if (command.ignoreStreamer && firebotChatMessage.username === streamer.username) { logger.debug("Message came from streamer and this command is set to ignore it"); return false; } // check if chat came from the bot and if we should ignore it. - if (command.ignoreBot && firebotChatMessage.username === bot.displayName) { + if (command.ignoreBot && firebotChatMessage.username === bot.username) { logger.debug("Message came from bot and this command is set to ignore it"); return false; } @@ -212,6 +212,7 @@ class CommandHandler { metadata: { username: commandSender, userId: firebotChatMessage.userId, + userDisplayName: firebotChatMessage.userDisplayName, userTwitchRoles: firebotChatMessage.roles, command: command, userCommand: userCmd, diff --git a/src/backend/chat/commands/command-manager.ts b/src/backend/chat/commands/command-manager.ts index c64faf531..ea11e2030 100644 --- a/src/backend/chat/commands/command-manager.ts +++ b/src/backend/chat/commands/command-manager.ts @@ -324,6 +324,8 @@ class CommandManager extends EventEmitter { command.count = 0; } + command.type = "custom"; + const commandDb = this.getCommandsDb(); try { diff --git a/src/backend/chat/commands/command-runner.ts b/src/backend/chat/commands/command-runner.ts index 4b25cded1..11f330a76 100644 --- a/src/backend/chat/commands/command-runner.ts +++ b/src/backend/chat/commands/command-runner.ts @@ -117,7 +117,7 @@ class CommandRunner { metadata: { username: userCommand.commandSender, userId: undefined, - userIdName: undefined, + userDisplayName: userCommand.commandSender, command: command, userCommand: userCommand, chatMessage: firebotChatMessage @@ -128,7 +128,7 @@ class CommandRunner { if (firebotChatMessage != null) { processEffectsRequest.trigger.metadata.userId = firebotChatMessage.userId; - processEffectsRequest.trigger.metadata.userIdName = firebotChatMessage.userIdName; + processEffectsRequest.trigger.metadata.userDisplayName = firebotChatMessage.userDisplayName; } return effectRunner.processEffects(processEffectsRequest).catch((reason) => { diff --git a/src/backend/chat/moderation/chat-moderation-manager.js b/src/backend/chat/moderation/chat-moderation-manager.js index 476dedd2d..be2bb9715 100644 --- a/src/backend/chat/moderation/chat-moderation-manager.js +++ b/src/backend/chat/moderation/chat-moderation-manager.js @@ -174,7 +174,7 @@ async function moderateMessage(chatMessage) { return; } - const userExemptGlobally = rolesManager.userIsInRole(chatMessage.username, chatMessage.roles, + const userExemptGlobally = rolesManager.userIsInRole(chatMessage.userId, chatMessage.roles, chatModerationSettings.exemptRoles); if (userExemptGlobally) { @@ -184,7 +184,7 @@ async function moderateMessage(chatMessage) { const twitchApi = require("../../twitch-api/api"); const chat = require("../twitch-chat"); - const userExemptForEmoteLimit = rolesManager.userIsInRole(chatMessage.username, chatMessage.roles, chatModerationSettings.emoteLimit.exemptRoles); + const userExemptForEmoteLimit = rolesManager.userIsInRole(chatMessage.userId, chatMessage.roles, chatModerationSettings.emoteLimit.exemptRoles); if (chatModerationSettings.emoteLimit.enabled && !!chatModerationSettings.emoteLimit.max && !userExemptForEmoteLimit) { const emoteCount = chatMessage.parts.filter(p => p.type === "emote").length; const emojiCount = chatMessage.parts @@ -203,7 +203,7 @@ async function moderateMessage(chatMessage) { } } - const userExemptForUrlModeration = rolesManager.userIsInRole(chatMessage.username, chatMessage.roles, chatModerationSettings.urlModeration.exemptRoles); + const userExemptForUrlModeration = rolesManager.userIsInRole(chatMessage.userId, chatMessage.roles, chatModerationSettings.urlModeration.exemptRoles); if (chatModerationSettings.urlModeration.enabled && !userExemptForUrlModeration && !permitCommand.hasTemporaryPermission(chatMessage.username)) { let shouldDeleteMessage = false; const message = chatMessage.rawText; @@ -280,7 +280,7 @@ async function moderateMessage(chatMessage) { messageId: messageId, username: username, scanForBannedWords: chatModerationSettings.bannedWordList.enabled, - isExempt: rolesManager.userIsInRole(chatMessage.username, chatMessage.roles, chatModerationSettings.bannedWordList.exemptRoles), + isExempt: rolesManager.userIsInRole(chatMessage.userId, chatMessage.roles, chatModerationSettings.bannedWordList.exemptRoles), maxEmotes: null } ); diff --git a/src/backend/chat/moderation/raid-message-checker.ts b/src/backend/chat/moderation/raid-message-checker.ts index 7f87f2de4..d27a5f9cc 100644 --- a/src/backend/chat/moderation/raid-message-checker.ts +++ b/src/backend/chat/moderation/raid-message-checker.ts @@ -1,10 +1,14 @@ -import { FirebotChatMessage } from "../../../types/chat"; import logger from "../../logwrapper"; import twitchApi from "../../twitch-api/api"; +interface RaidMessage { + rawText: string; + userId: string; +} + class RaidMessageChecker { private readonly _chatCacheLimit = 50; - private _messageCache: FirebotChatMessage[] = []; + private _messageCache: RaidMessage[] = []; private _raidMessage = ""; private _checkerEnabled = false; private _settings = { @@ -12,7 +16,7 @@ class RaidMessageChecker { shouldBlock: false }; - private async handleRaider(message: FirebotChatMessage): Promise { + private async handleRaider(message: RaidMessage): Promise { if (this._settings.shouldBan) { await twitchApi.moderation.banUser(message.userId); } @@ -48,19 +52,19 @@ class RaidMessageChecker { } } - async sendMessageToCache(firebotChatMessage: FirebotChatMessage): Promise { + async sendMessageToCache(raidMessage: RaidMessage): Promise { if (this._messageCache.length >= this._chatCacheLimit) { this._messageCache.shift(); } - if (firebotChatMessage.rawText.length > 10) { - firebotChatMessage.rawText = firebotChatMessage.rawText.substr(10); + if (raidMessage.rawText.length > 10) { + raidMessage.rawText = raidMessage.rawText.substr(10); } - this._messageCache.push(firebotChatMessage); + this._messageCache.push(raidMessage); - if (firebotChatMessage && this._checkerEnabled && firebotChatMessage.rawText === this._raidMessage) { - await this.handleRaider(firebotChatMessage); + if (raidMessage && this._checkerEnabled && raidMessage.rawText === this._raidMessage) { + await this.handleRaider(raidMessage); } } diff --git a/src/backend/chat/twitch-chat.ts b/src/backend/chat/twitch-chat.ts index f605240e1..23004ef99 100644 --- a/src/backend/chat/twitch-chat.ts +++ b/src/backend/chat/twitch-chat.ts @@ -37,15 +37,13 @@ interface UserVipRequest { } class TwitchChat extends EventEmitter { - private _streamerIncomingChatClient: ChatClient; - private _streamerOutgoingingChatClient: ChatClient; + private _streamerChatClient: ChatClient; private _botChatClient: ChatClient; constructor() { super(); - this._streamerIncomingChatClient = null; - this._streamerOutgoingingChatClient = null; + this._streamerChatClient = null; this._botChatClient = null; } @@ -54,8 +52,7 @@ class TwitchChat extends EventEmitter { */ get chatIsConnected(): boolean { return ( - this._streamerIncomingChatClient?.irc?.isConnected === true && - this._streamerOutgoingingChatClient?.irc?.isConnected === true + this._streamerChatClient?.irc?.isConnected === true ); } @@ -63,13 +60,9 @@ class TwitchChat extends EventEmitter { * Disconnects the streamer and bot from chat */ async disconnect(emitDisconnectEvent = true): Promise { - if (this._streamerIncomingChatClient != null) { - this._streamerIncomingChatClient.quit(); - this._streamerIncomingChatClient = null; - } - if (this._streamerOutgoingingChatClient != null) { - this._streamerOutgoingingChatClient.quit(); - this._streamerOutgoingingChatClient = null; + if (this._streamerChatClient != null) { + this._streamerChatClient.quit(); + this._streamerChatClient = null; } if (this._botChatClient != null && this._botChatClient?.irc?.isConnected === true) { this._botChatClient.quit(); @@ -102,24 +95,17 @@ class TwitchChat extends EventEmitter { await this.disconnect(false); try { - this._streamerIncomingChatClient = new ChatClient({ - authProvider: streamerAuthProvider, - requestMembershipEvents: true - }); - this._streamerOutgoingingChatClient = new ChatClient({ + this._streamerChatClient = new ChatClient({ authProvider: streamerAuthProvider, requestMembershipEvents: true }); - this._streamerIncomingChatClient.irc.onRegister(() => { - this._streamerIncomingChatClient.join(streamer.username); + this._streamerChatClient.irc.onRegister(() => { + this._streamerChatClient.join(streamer.username); frontendCommunicator.send("twitch:chat:autodisconnected", false); }); - this._streamerOutgoingingChatClient.irc.onRegister(() => { - this._streamerOutgoingingChatClient.join(streamer.username); - }); - this._streamerIncomingChatClient.irc.onPasswordError((event) => { + this._streamerChatClient.irc.onPasswordError((event) => { logger.error("Failed to connect to chat", event); frontendCommunicator.send( "error", @@ -128,26 +114,18 @@ class TwitchChat extends EventEmitter { this.disconnect(true); }); - this._streamerIncomingChatClient.irc.onConnect(() => { + this._streamerChatClient.irc.onConnect(() => { this.emit("connected"); }); - this._streamerIncomingChatClient.irc.onDisconnect((manual, reason) => { + this._streamerChatClient.irc.onDisconnect((manual, reason) => { if (!manual) { - logger.error("Chat disconnected unexpectedly", reason); + logger.error("Incoming Chat disconnected unexpectedly", reason); frontendCommunicator.send("twitch:chat:autodisconnected", true); } }); - this._streamerOutgoingingChatClient.irc.onDisconnect((manual, reason) => { - if (!manual) { - logger.error("Chat disconnected unexpectedly", reason); - frontendCommunicator.send("twitch:chat:autodisconnected", true); - } - }); - - this._streamerIncomingChatClient.connect(); - this._streamerOutgoingingChatClient.connect(); + this._streamerChatClient.connect(); await chatHelpers.handleChatConnect(); @@ -156,10 +134,10 @@ class TwitchChat extends EventEmitter { chatterPoll.startChatterPoll(); - const vips = await twitchApi.channels.getVips(); - if (vips) { - chatRolesManager.loadUsersInVipRole(vips); - } + // Refresh these once we connect to Twitch + // While connected, we can just react to changes via chat messages/EventSub events + await chatRolesManager.loadVips(); + await chatRolesManager.loadModerators(); } catch (error) { logger.error("Chat connect error", error); await this.disconnect(); @@ -187,7 +165,7 @@ class TwitchChat extends EventEmitter { } try { - twitchChatListeners.setupChatListeners(this._streamerIncomingChatClient, this._botChatClient); + twitchChatListeners.setupChatListeners(this._streamerChatClient, this._botChatClient); } catch (error) { logger.error("Error setting up chat listeners", error); } @@ -199,12 +177,10 @@ class TwitchChat extends EventEmitter { * @param {string} accountType The type of account to whisper with ('streamer' or 'bot') */ async _say(message: string, accountType: string, replyToId?: string): Promise { - const chatClient = accountType === "bot" ? this._botChatClient : this._streamerOutgoingingChatClient; try { logger.debug(`Sending message as ${accountType}.`); - const streamer = accountAccess.getAccounts().streamer; - chatClient.say(streamer.username, message, replyToId ? { replyTo: replyToId } : undefined); + await twitchApi.chat.sendChatMessage(message, replyToId ?? undefined, accountType === "bot"); } catch (error) { logger.error(`Error attempting to send message with ${accountType}`, error); } @@ -370,10 +346,14 @@ frontendCommunicator.onAsync("update-user-vip-status", async (data: UserVipReque if (shouldBeVip) { await twitchApi.moderation.addChannelVip(user.id); - chatRolesManager.addVipToVipList(username); + chatRolesManager.addVipToVipList({ + id: user.id, + username: user.name, + displayName: user.displayName + }); } else { await twitchApi.moderation.removeChannelVip(user.id); - chatRolesManager.removeVipFromVipList(username); + chatRolesManager.removeVipFromVipList(user.id); } }); diff --git a/src/backend/chat/twitch-commands/moderation-handlers.ts b/src/backend/chat/twitch-commands/moderation-handlers.ts index 5a7bd8763..a011f1a8b 100644 --- a/src/backend/chat/twitch-commands/moderation-handlers.ts +++ b/src/backend/chat/twitch-commands/moderation-handlers.ts @@ -115,15 +115,19 @@ export const vipHandler: TwitchSlashCommandHandler<[string]> = { }; }, handle: async ([targetUsername]) => { - const targetUserId = (await twitchApi.users.getUserByName(targetUsername))?.id; + const targetUser = await twitchApi.users.getUserByName(targetUsername); - if (targetUserId == null) { + if (targetUser == null) { return false; } - const result = await twitchApi.moderation.addChannelVip(targetUserId); + const result = await twitchApi.moderation.addChannelVip(targetUser.id); if (result === true) { - chatRolesManager.addVipToVipList(targetUsername); + chatRolesManager.addVipToVipList({ + id: targetUser.id, + username: targetUser.name, + displayName: targetUser.displayName + }); } return result; } @@ -155,7 +159,7 @@ export const unvipHandler: TwitchSlashCommandHandler<[string]> = { const result = await twitchApi.moderation.removeChannelVip(targetUserId); if (result === true) { - chatRolesManager.removeVipFromVipList(targetUsername); + chatRolesManager.removeVipFromVipList(targetUserId); } return result; } diff --git a/src/backend/common/account-access.js b/src/backend/common/account-access.js index 4c8ecfa3a..bd86b34a0 100644 --- a/src/backend/common/account-access.js +++ b/src/backend/common/account-access.js @@ -228,6 +228,11 @@ function setAccountTokenIssue(accountType) { } else { throw new Error("invalid account type"); } + + frontendCommunicator.send("invalidate-accounts", { + streamer: streamerTokenIssue, + bot: botTokenIssue + }); } exports.events = accountEvents; diff --git a/src/backend/common/connection-manager.js b/src/backend/common/connection-manager.js index a2493ec9e..8c413caf2 100644 --- a/src/backend/common/connection-manager.js +++ b/src/backend/common/connection-manager.js @@ -54,14 +54,18 @@ function emitServiceConnectionUpdateEvents(serviceId, connectionState) { } // Chat listeners -twitchChat.on("connected", () => emitServiceConnectionUpdateEvents("chat", ConnectionState.Connected)); +twitchChat.on("connected", () => { + emitServiceConnectionUpdateEvents("chat", ConnectionState.Connected); + const rewardsManager = require("../channel-rewards/channel-reward-manager"); + rewardsManager.refreshChannelRewardRedemptions(); +}); twitchChat.on("disconnected", () => emitServiceConnectionUpdateEvents("chat", ConnectionState.Disconnected)); twitchChat.on("connecting", () => emitServiceConnectionUpdateEvents("chat", ConnectionState.Connecting)); twitchChat.on("reconnecting", () => emitServiceConnectionUpdateEvents("chat", ConnectionState.Reconnecting)); // Integrations listener -integrationManager.on("integration-connected", (id) => emitServiceConnectionUpdateEvents(`integration.${id}`, ConnectionState.Connected)); -integrationManager.on("integration-disconnected", (id) => emitServiceConnectionUpdateEvents(`integration.${id}`, ConnectionState.Disconnected)); +integrationManager.on("integration-connected", id => emitServiceConnectionUpdateEvents(`integration.${id}`, ConnectionState.Connected)); +integrationManager.on("integration-disconnected", id => emitServiceConnectionUpdateEvents(`integration.${id}`, ConnectionState.Disconnected)); let connectionUpdateInProgress = false; @@ -153,8 +157,10 @@ class ConnectionManager extends EventEmitter { } else if (accountAccess.streamerTokenIssue()) { const botTokenIssue = accountAccess.getAccounts().bot.loggedIn && accountAccess.botTokenIssue(); - const message = `There is an issue with the Streamer ${botTokenIssue ? ' and Bot' : ""} Twitch account${botTokenIssue ? 's' : ""}. Please re-sign into the account${botTokenIssue ? 's' : ""} and try again.`; - renderWindow.webContents.send("error", message); + frontendCommunicator.send("invalidate-accounts", { + streamer: true, + bot: botTokenIssue + }); } else { const waitForServiceConnectDisconnect = (serviceId, action = true) => { const shouldToggle = action === "toggle"; @@ -165,7 +171,7 @@ class ConnectionManager extends EventEmitter { return Promise.resolve(); } - const promise = new Promise(resolve => { + const promise = new Promise((resolve) => { currentlyWaitingService = { serviceId: serviceId, callback: () => resolve() diff --git a/src/backend/common/frontend-communicator.ts b/src/backend/common/frontend-communicator.ts index 87be9a122..aa586acd6 100644 --- a/src/backend/common/frontend-communicator.ts +++ b/src/backend/common/frontend-communicator.ts @@ -23,7 +23,7 @@ class FrontendCommunicator implements FrontendCommunicatorModule { } send(eventName: string, data?: unknown): void { - if (globalThis.renderWindow != null) { + if (globalThis.renderWindow?.webContents?.isDestroyed() === false) { globalThis.renderWindow.webContents.send(eventName, data); } } diff --git a/src/backend/common/handlers/custom-scripts/custom-script-runner.js b/src/backend/common/handlers/custom-scripts/custom-script-runner.js index 74e714979..aa772347e 100644 --- a/src/backend/common/handlers/custom-scripts/custom-script-runner.js +++ b/src/backend/common/handlers/custom-scripts/custom-script-runner.js @@ -122,7 +122,7 @@ async function executeScript(scriptData, trigger, isStartupScript = false) { effectsObj = { id: uuid(), list: effects - .filter((e) => e.type != null && e.type !== "") + .filter(e => e.type != null && e.type !== "") .map((e) => { e = mapV4EffectToV5(e); if (e.id == null) { @@ -240,6 +240,20 @@ function runScript(effect, trigger) { return executeScript(effect, trigger); } +async function stopAllScripts() { + logger.info("Stopping all custom scripts..."); + for (const activeScript of Object.values(activeCustomScripts)) { + if (activeScript.stop != null) { + try { + await Promise.resolve(activeScript.stop()); + } catch (error) { + logger.error(`Error when attempting to stop custom script`, error); + } + } + } + logger.info("Stopped all custom scripts"); +} + ipcMain.on("openScriptsFolder", function () { shell.openPath(profileManager.getPathInProfile("/scripts")); }); @@ -248,3 +262,4 @@ exports.runScript = runScript; exports.runStartUpScript = runStartUpScript; exports.startUpScriptSaved = startUpScriptSaved; exports.startUpScriptDeleted = startUpScriptDeleted; +exports.stopAllScripts = stopAllScripts; diff --git a/src/backend/common/profile-manager.js b/src/backend/common/profile-manager.js index 4943007e6..e64cf59bd 100644 --- a/src/backend/common/profile-manager.js +++ b/src/backend/common/profile-manager.js @@ -149,9 +149,14 @@ function deleteProfile() { } const getPathInProfile = function(filepath) { - const profilePath = - `${dataAccess.getUserDataPath()}/profiles/${getLoggedInProfile()}`; - return path.join(profilePath, filepath); + return path.join(dataAccess.getUserDataPath(), + "profiles", + getLoggedInProfile(), + filepath); +}; + +const getPathInProfileRelativeToUserData = function(filepath) { + return path.join("profiles", getLoggedInProfile(), filepath); }; /** @@ -178,11 +183,15 @@ const getJsonDbInProfile = function(filepath, humanReadable = true) { }; const profileDataPathExistsSync = function(filePath) { - const profilePath = `/profiles/${getLoggedInProfile()}`, - joinedPath = path.join(profilePath, filePath); + const joinedPath = getPathInProfileRelativeToUserData(filePath); return dataAccess.userDataPathExistsSync(joinedPath); }; +const deletePathInProfile = function(filePath) { + const joinedPath = getPathInProfileRelativeToUserData(filePath); + return dataAccess.deletePathInUserData(joinedPath); +}; + exports.getLoggedInProfile = getLoggedInProfile; exports.createNewProfile = createNewProfile; exports.getPathInProfile = getPathInProfile; @@ -193,3 +202,4 @@ exports.logInProfile = logInProfile; exports.renameProfile = renameProfile; exports.getNewProfileName = () => profileToRename; exports.hasProfileRename = () => profileToRename != null; +exports.deletePathInProfile = deletePathInProfile; diff --git a/src/backend/common/settings-access.js b/src/backend/common/settings-access.js index 08756d34c..832830055 100644 --- a/src/backend/common/settings-access.js +++ b/src/backend/common/settings-access.js @@ -57,13 +57,16 @@ function handleCorruptSettingsFile() { })); } -function getDataFromFile(path, forceCacheUpdate = false) { +function getDataFromFile(path, forceCacheUpdate = false, defaultValue = undefined) { try { if (settingsCache[path] == null || forceCacheUpdate) { const data = getSettingsFile().getData(path); - settingsCache[path] = data; + settingsCache[path] = data ?? defaultValue; } } catch (err) { + if (defaultValue !== undefined) { + settingsCache[path] = defaultValue; + } if (err.name !== "DataError") { logger.warn(err); if ( @@ -161,6 +164,15 @@ settings.setOverlayInstances = function(ois) { pushDataToFile("/settings/overlayInstances", ois); }; +settings.getForceOverlayEffectsToContinueOnRefresh = function() { + const forceOverlayEffectsToContinueOnRefresh = getDataFromFile("/settings/forceOverlayEffectsToContinueOnRefresh", false, true); + return forceOverlayEffectsToContinueOnRefresh === true; +}; + +settings.setForceOverlayEffectsToContinueOnRefresh = function(value) { + pushDataToFile("/settings/forceOverlayEffectsToContinueOnRefresh", value); +}; + settings.backupKeepAll = function() { const backupKeepAll = getDataFromFile("/settings/backupKeepAll"); return backupKeepAll != null ? backupKeepAll : false; @@ -310,4 +322,13 @@ settings.setWebOnlineCheckin = (value) => { pushDataToFile("/settings/webOnlineCheckin", value); }; +settings.getTriggerUpcomingAdBreakMinutes = function() { + const value = getDataFromFile("/settings/triggerUpcomingAdBreakMinutes", false, 0); + return value ?? 0; +}; + +settings.setTriggerUpcomingAdBreakMinutes = function(value) { + pushDataToFile("/settings/triggerUpcomingAdBreakMinutes", value); +}; + exports.settings = settings; diff --git a/src/backend/common/user-access.js b/src/backend/common/user-access.js index a200624dc..04aaada47 100644 --- a/src/backend/common/user-access.js +++ b/src/backend/common/user-access.js @@ -13,27 +13,43 @@ const teamRolesManager = require("../roles/team-roles-manager"); const followCache = new NodeCache({ stdTTL: 10, checkperiod: 10 }); -async function userFollowsChannels(username, channelNames) { +async function userFollowsChannels(username, channelNames, durationInSeconds = 0) { let userfollowsAllChannels = true; for (const channelName of channelNames) { - let userFollowsChannel = false; + /** + * @type {import('@twurple/api').HelixChannelFollower | boolean} + */ + let userFollow; // check cache first const cachedFollow = followCache.get(`${username}:${channelName}`); if (cachedFollow !== undefined) { - userFollowsChannel = cachedFollow; + userFollow = cachedFollow; } else { - userFollowsChannel = await twitchApi.users.doesUserFollowChannel(username, channelName); + userFollow = await twitchApi.users.getUserChannelFollow(username, channelName); // set cache value - followCache.set(`${username}:${channelName}`, userFollowsChannel); + followCache.set(`${username}:${channelName}`, userFollow); } - if (!userFollowsChannel) { + if (!userFollow) { userfollowsAllChannels = false; break; } + + if (userFollow === true) { // streamer follow + continue; + } + + if (durationInSeconds > 0) { + const followTime = Math.round(userFollow.followDate.getTime() / 1000); + const currentTime = Math.round(new Date().getTime() / 1000); + if ((currentTime - followTime) < durationInSeconds) { + userfollowsAllChannels = false; + break; + } + } } return userfollowsAllChannels; @@ -81,7 +97,8 @@ async function getUserDetails(userId) { frontendCommunicator.send("twitch:chat:user-updated", { id: firebotUserData._id, - username: firebotUserData.displayName, + username: firebotUserData.username, + displayName: firebotUserData.displayName, roles: userRoles, profilePicUrl: firebotUserData.profilePicUrl, active: activeUserHandler.userIsActive(firebotUserData._id) diff --git a/src/backend/currency/currency-manager.ts b/src/backend/currency/currency-manager.ts index d82216ffd..b2519e3d3 100644 --- a/src/backend/currency/currency-manager.ts +++ b/src/backend/currency/currency-manager.ts @@ -380,8 +380,8 @@ class CurrencyManager { const teamRoles: Record> = {}; for (const viewer of onlineViewers) { - teamRoles[viewer.username] = await teamRolesManager - .getAllTeamRolesForViewer(viewer.username); + teamRoles[viewer._id] = await teamRolesManager + .getAllTeamRolesForViewer(viewer._id); } const userIdsInRoles = onlineViewers @@ -390,9 +390,9 @@ class CurrencyManager { const allRoles = [ ...twitchRoles.map(tr => twitchRolesManager.mapTwitchRole(tr)), - ...customRolesManager.getAllCustomRolesForViewer(u.username), - ...teamRoles[u.username], - ...firebotRolesManager.getAllFirebotRolesForViewer(u.username) + ...customRolesManager.getAllCustomRolesForViewer(u._id), + ...teamRoles[u._id], + ...firebotRolesManager.getAllFirebotRolesForViewer(u._id) ]; return { diff --git a/src/backend/effects/builtin-effect-loader.js b/src/backend/effects/builtin-effect-loader.js index aed5e4f6b..a180f723d 100644 --- a/src/backend/effects/builtin-effect-loader.js +++ b/src/backend/effects/builtin-effect-loader.js @@ -70,6 +70,7 @@ exports.loadEffects = () => { 'twitch/raid', 'twitch/set-chat-mode', 'twitch/shoutout', + 'twitch/snooze-ad-break', 'twitch/stream-title', 'twitch/stream-game', @@ -80,7 +81,7 @@ exports.loadEffects = () => { 'twitch/create-prediction', 'twitch/lock-prediction', 'twitch/resolve-prediction' - ].forEach(filename => { + ].forEach((filename) => { const definition = require(`./builtin/${filename}`); effectManager.registerEffect(definition); }); diff --git a/src/backend/effects/builtin/chat.js b/src/backend/effects/builtin/chat.js index 13fad9d1f..119dd95a3 100644 --- a/src/backend/effects/builtin/chat.js +++ b/src/backend/effects/builtin/chat.js @@ -16,31 +16,50 @@ const effect = { - +
Chat messages cannot be longer than 500 characters. This message will get automatically chunked into multiple messages if it's too long after all replace variables have been populated.
- -
-
- To - -
+ + +
+
-

ProTip: To whisper the associated user, put $user in the whisper field.

-
- +

ProTip: To whisper the associated user, put $user in the whisper field.

+
+
`, - optionsController: () => {}, + optionsController: ($scope) => { + $scope.showWhisperInput = $scope.effect.whisper != null && $scope.effect.whisper !== '' + }, optionsValidator: effect => { const errors = []; if (effect.message == null || effect.message === "") { @@ -56,6 +75,10 @@ const effect = { messageId = trigger.metadata.eventData?.chatMessage?.id; } + if (effect.me) { + effect.message = `/me ${effect.message}`; + } + await twitchChat.sendChatMessage(effect.message, effect.whisper, effect.chatter, !effect.whisper && effect.sendAsReply ? messageId : undefined); return true; diff --git a/src/backend/effects/builtin/clips.js b/src/backend/effects/builtin/clips.js index ae8f4e7d3..bb43771da 100644 --- a/src/backend/effects/builtin/clips.js +++ b/src/backend/effects/builtin/clips.js @@ -57,7 +57,7 @@ const clip = {
-
diff --git a/src/gui/app/directives/chat/feed items/chat-message.js b/src/gui/app/directives/chat/feed items/chat-message.js index e1cb2c443..f784d3d74 100644 --- a/src/gui/app/directives/chat/feed items/chat-message.js +++ b/src/gui/app/directives/chat/feed items/chat-message.js @@ -119,7 +119,13 @@ ng-click="$root.openLinkExternally('https://pronouns.alejo.io/')" ng-show="$ctrl.showPronoun && $ctrl.pronouns.pronounCache[$ctrl.message.username] != null" >{{$ctrl.pronouns.pronounCache[$ctrl.message.username]}} - {{$ctrl.message.username}} + {{$ctrl.message.userDisplayName != null ? $ctrl.message.userDisplayName : $ctrl.message.username}} +  ({{$ctrl.message.username}}) - ${message.username} + ${message.userDisplayName}${message.username && message.username.toLowerCase() !== message.userDisplayName.toLowerCase() ? ` (${message.username})` : ""} `, enabled: false }, - ...actions.map(a => { + ...actions.map((a) => { let html = ""; if (a.name === "Remove VIP") { html = ` @@ -387,62 +393,62 @@ return { html: html, click: () => { - $ctrl.messageActionSelected(a.name, message.username, message.userId, message.id, message.rawText); + $ctrl.messageActionSelected(a.name, message.username, message.userId, message.displayName, message.id, message.rawText); } }; })]; }; - $ctrl.messageActionSelected = (action, userName, userId, msgId, rawText) => { + $ctrl.messageActionSelected = (action, username, userId, displayName, msgId, rawText) => { switch (action.toLowerCase()) { case "delete message": chatMessagesService.deleteMessage(msgId); break; case "timeout": - updateChatField(`/timeout @${userName} 300`); + updateChatField(`/timeout @${username} 300`); break; case "ban": utilityService .showConfirmationModal({ title: "Ban User", - question: `Are you sure you want to ban ${userName}?`, + question: `Are you sure you want to ban ${username}?`, confirmLabel: "Ban", confirmBtnType: "btn-danger" }) - .then(confirmed => { + .then((confirmed) => { if (confirmed) { - backendCommunicator.fireEvent("update-user-banned-status", { username: userName, shouldBeBanned: true }); + backendCommunicator.fireEvent("update-user-banned-status", { username: username, shouldBeBanned: true }); } }); break; case "mod": - chatMessagesService.changeModStatus(userName, true); + chatMessagesService.changeModStatus(username, true); break; case "unmod": utilityService .showConfirmationModal({ title: "Mod User", - question: `Are you sure you want to unmod ${userName}?`, + question: `Are you sure you want to unmod ${username}?`, confirmLabel: "Unmod", confirmBtnType: "btn-danger" }) - .then(confirmed => { + .then((confirmed) => { if (confirmed) { - chatMessagesService.changeModStatus(userName, false); + chatMessagesService.changeModStatus(username, false); } }); break; case "add as vip": - backendCommunicator.fireEvent("update-user-vip-status", { username: userName, shouldBeVip: true }); + backendCommunicator.fireEvent("update-user-vip-status", { username: username, shouldBeVip: true }); break; case "remove vip": - backendCommunicator.fireEvent("update-user-vip-status", { username: userName, shouldBeVip: false }); + backendCommunicator.fireEvent("update-user-vip-status", { username: username, shouldBeVip: false }); break; case "whisper": - updateChatField(`/w @${userName} `); + updateChatField(`/w @${username} `); break; case "mention": - updateChatField(`@${userName} `); + updateChatField(`@${username} `); break; case "reply to message": $ctrl.onReplyClicked({ @@ -450,13 +456,13 @@ }); break; case "quote message": - updateChatField(`!quote add @${userName} ${rawText}`); + updateChatField(`!quote add @${username} ${rawText}`); break; case "spotlight message": - chatMessagesService.highlightMessage(userName, rawText); + chatMessagesService.highlightMessage(username, userId, displayName, rawText); break; case "shoutout": - updateChatField(`!so @${userName}`); + updateChatField(`!so @${username}`); break; case "details": { $ctrl.showUserDetailsModal(userId); diff --git a/src/gui/app/directives/chat/feed items/reward-redemption.js b/src/gui/app/directives/chat/feed items/reward-redemption.js index b95748a5b..42a728f62 100644 --- a/src/gui/app/directives/chat/feed items/reward-redemption.js +++ b/src/gui/app/directives/chat/feed items/reward-redemption.js @@ -8,8 +8,8 @@ }, template: `
- - {{$ctrl.redemption.user.username}} redeemed {{$ctrl.redemption.reward.name}} + + {{$ctrl.redemption.user.displayName}}{{($ctrl.redemption.user.displayName.toLowerCase() !== $ctrl.redemption.user.username.toLowerCase() ? " (" + $ctrl.redemption.user.username + ")" : "")}} redeemed {{$ctrl.redemption.reward.name}}
`, controller: function() { diff --git a/src/gui/app/directives/chat/quick-actions/quick-actions.js b/src/gui/app/directives/chat/quick-actions/quick-actions.js index 1332dba6c..060825087 100644 --- a/src/gui/app/directives/chat/quick-actions/quick-actions.js +++ b/src/gui/app/directives/chat/quick-actions/quick-actions.js @@ -91,6 +91,16 @@ }); settingsService.setQuickActionSettings($ctrl.settings); + } else { + let highestPosition = Math.max(...Object.values($ctrl.settings).map(s => s.position)); + quickActionsService.quickActions.forEach((qa) => { + if ($ctrl.settings[qa.id] == null) { + $ctrl.settings[qa.id] = { + enabled: true, + position: ++highestPosition + }; + } + }); } }; diff --git a/src/gui/app/directives/controls/effectList.js b/src/gui/app/directives/controls/effectList.js index 09c1d533a..dc3729189 100644 --- a/src/gui/app/directives/controls/effectList.js +++ b/src/gui/app/directives/controls/effectList.js @@ -133,7 +133,7 @@
{ + ctrl.effectsData.list.forEach((e) => { if (e.active == null) { e.active = true; } @@ -365,7 +365,7 @@ function getSharedEffects(code) { return $http.get(`https://bytebin.lucko.me/${code}`) - .then(resp => { + .then((resp) => { if (resp.status === 200) { return JSON.parse(unescape(JSON.stringify(resp.data))); } @@ -383,7 +383,7 @@ saveText: "Add", inputPlaceholder: "Enter code", validationFn: (shareCode) => { - return new Promise(async resolve => { + return new Promise(async (resolve) => { if (shareCode == null || shareCode.trim().length < 1) { resolve(false); } @@ -414,7 +414,7 @@ effectDefinitions = await effectHelperService.getAllEffectDefinitions(); }; - ctrl.getEffectNameById = id => { + ctrl.getEffectNameById = (id) => { if (!effectDefinitions || effectDefinitions.length < 1) { return ""; } @@ -527,7 +527,7 @@ objectCopyHelper.copyEffects(ctrl.effectsData.list); }; - ctrl.openNewEffectModal = index => { + ctrl.openNewEffectModal = (index) => { utilityService.showModal({ component: "addNewEffectModal", backdrop: true, @@ -536,7 +536,7 @@ trigger: () => ctrl.trigger, triggerMeta: () => ctrl.triggerMeta }, - closeCallback: resp => { + closeCallback: (resp) => { if (resp == null) { return; } @@ -559,8 +559,141 @@ }); }; + function mergeArraysWithoutDuplicates(initialArray, arrayToAdd, keyToCheck) { + const nonDupes = arrayToAdd.filter((item) => { + return !initialArray.some((i) => { + return i[keyToCheck] === item[keyToCheck]; + }); + }); + return [...initialArray, ...nonDupes]; + } + + function stringCanBeShorthand(str) { + return /^([a-z][a-z\d._-]+)([\s\S]*)$/i.test(str); + } + + function checkEffectListForMagicVariables(effects, ignoreEffectId) { + const magicVariables = { + customVariables: [], + effectOutputs: [] + }; + + if (!effects || !Array.isArray(effects)) { + return; + } + + for (const effect of effects) { + if (effect == null || typeof (effect) !== "object" || effect.id === ignoreEffectId) { + continue; + } + + if (effect.type === "firebot:customvariable" || effect.name?.length) { + const canBeShorthand = stringCanBeShorthand(effect.name); + magicVariables.customVariables = mergeArraysWithoutDuplicates(magicVariables.customVariables, [{ + name: effect.name, + handle: canBeShorthand ? `$$${effect.name}` : `$customVariable[${effect.name}]`, + effectLabel: effect.effectLabel, + examples: [ + ...(canBeShorthand ? [ + { + handle: `$$${effect.name}["path", "to", "property"]`, + description: `Access a property of "${effect.name}"` + }, + { + handle: `$customVariable[${effect.name}]`, + description: `Long hand version of "${effect.name}"` + } + ] + : []), + { + handle: `$customVariable[${effect.name}, "path.to.property"]`, + description: `Access a property of "${effect.name}" using long hand` + } + ] + }], "name"); + continue; + } + + const effectDefinition = effectDefinitions.find(e => e.id === effect.type); + if (effectDefinition != null && effectDefinition.outputs?.length) { + const customOutputNames = effect.outputNames || {}; + + const outputs = effectDefinition.outputs.map((output) => { + const name = customOutputNames[output.defaultName] ?? output.defaultName; + const canBeShorthand = stringCanBeShorthand(name); + return { + name, + handle: canBeShorthand ? `$&${name}` : `$effectOutput[${name}]`, + label: output.label, + description: output.description, + effectLabel: `${effectDefinition.name}${effect.effectLabel ? ` (${effect.effectLabel})` : ""}`, + examples: [ + ...(canBeShorthand ? [ + { + handle: `$&${name}["path", "to", "property"]`, + description: `Access a property of "${name}"` + }, + { + handle: `$effectOutput[${name}]`, + description: `Long hand version of "${name}"` + } + ] + : []), + { + handle: `$effectOutput[${name}, "path.to.property"]`, + description: `Access a property of "${name}" using long hand` + } + ] + }; + }); + + magicVariables.effectOutputs = mergeArraysWithoutDuplicates(magicVariables.effectOutputs, outputs, "name"); + + } + + for (const value of Object.values(effect)) { + if (Array.isArray(value)) { + const result = checkEffectListForMagicVariables(value, ignoreEffectId); + magicVariables.customVariables = mergeArraysWithoutDuplicates(magicVariables.customVariables, result.customVariables, "name"); + magicVariables.effectOutputs = mergeArraysWithoutDuplicates(magicVariables.effectOutputs, result.effectOutputs, "name"); + } + } + } + + return magicVariables; + } + + + function determineMagicVariables(ignoreEffectId) { + const magicVariables = { + customVariables: [], + effectOutputs: [], + presetListArgs: ctrl.triggerMeta?.presetListArgs?.map((a) => { + const canBeShorthand = stringCanBeShorthand(a.name); + return { + name: a.name, + handle: canBeShorthand ? `$#${a.name}` : `$presetListArg[${a.name}]`, + examples: canBeShorthand ? [ + { + handle: `$presetListArg[${a.name}]`, + description: "Long hand version of the preset list argument" + } + ] : undefined + }; + }) || [] + }; + + const effectsToCheck = ctrl.triggerMeta?.rootEffects?.list || ctrl.effectsData.list; + const effectsResult = checkEffectListForMagicVariables(effectsToCheck, ignoreEffectId); + magicVariables.customVariables = mergeArraysWithoutDuplicates(magicVariables.customVariables, effectsResult.customVariables, "name"); + magicVariables.effectOutputs = mergeArraysWithoutDuplicates(magicVariables.effectOutputs, effectsResult.effectOutputs, "name"); + + return magicVariables; + } + ctrl.openEditEffectModal = (effect, index, trigger, isNew) => { - utilityService.showEditEffectModal(effect, index, trigger, response => { + const magicVariables = determineMagicVariables(effect.id); + utilityService.showEditEffectModal(effect, index, trigger, (response) => { if (response.action === "add") { ctrl.effectsData.list.splice(index + 1, 0, response.effect); } else if (response.action === "update") { @@ -569,7 +702,7 @@ ctrl.removeEffectAtIndex(response.index); } ctrl.effectsUpdate(); - }, ctrl.triggerMeta, isNew); + }, { ...(ctrl.triggerMeta ?? {}), magicVariables }, isNew); }; //effect queue @@ -627,14 +760,14 @@ ctrl.showAddEditEffectQueueModal = (queueId) => { effectQueuesService.showAddEditEffectQueueModal(queueId) - .then(id => { + .then((id) => { ctrl.effectsData.queue = id; }); }; ctrl.showDeleteEffectQueueModal = (queueId) => { effectQueuesService.showDeleteEffectQueueModal(queueId) - .then(confirmed => { + .then((confirmed) => { if (confirmed) { ctrl.effectsData.queue = undefined; } @@ -649,7 +782,7 @@ saveText: "Save", inputPlaceholder: "Enter secs", validationFn: (value) => { - return new Promise(resolve => { + return new Promise((resolve) => { if (value == null || value < 0) { return resolve(false); } diff --git a/src/gui/app/directives/controls/firebot-button.js b/src/gui/app/directives/controls/firebot-button.js index 2db7cecec..626d0c399 100644 --- a/src/gui/app/directives/controls/firebot-button.js +++ b/src/gui/app/directives/controls/firebot-button.js @@ -7,24 +7,57 @@ bindings: { text: "@", ngClick: "&?", - type: "{{$ctrl.text}} + tooltip-enable="$ctrl.tooltip" + uib-tooltip="{{$ctrl.tooltip}}" + tooltip-placement="{{$ctrl.tooltipPlacement || 'top'}}" + tooltip-append-to-body="true" + > + + + {{$ctrl.text}} `, controller: function() { const $ctrl = this; + $ctrl.iconClass = ""; + + const buttonSizes = { + extraSmall: "btn-xs", + small: "btn-sm", + large: "btn-lg" + }; + + $ctrl.sizeClass = ""; + $ctrl.$onInit = () => { if ($ctrl.type == null) { $ctrl.type = "default"; } + if ($ctrl.size != null) { + $ctrl.sizeClass = buttonSizes[$ctrl.size] ?? ""; + } + if ($ctrl.icon != null) { + const classes = $ctrl.icon.split(" "); + if (classes.length === 1) { + $ctrl.iconClass = `far ${classes[0]}`; + } else { + $ctrl.iconClass = classes.join(" "); + } + } }; } }); diff --git a/src/gui/app/directives/controls/firebot-checkbox.js b/src/gui/app/directives/controls/firebot-checkbox.js index 4d9a718c2..811e068cf 100644 --- a/src/gui/app/directives/controls/firebot-checkbox.js +++ b/src/gui/app/directives/controls/firebot-checkbox.js @@ -8,11 +8,12 @@ label: "@", tooltip: "@?", model: "=", - style: "@?" + style: "@?", + disabled: " {{$ctrl.label}} - +
` diff --git a/src/gui/app/directives/controls/firebot-radio.js b/src/gui/app/directives/controls/firebot-radio.js new file mode 100644 index 000000000..9a8932452 --- /dev/null +++ b/src/gui/app/directives/controls/firebot-radio.js @@ -0,0 +1,35 @@ +"use strict"; + +(function() { + angular + .module('firebotApp') + .component("firebotRadioContainer", { + bindings: { + inline: "
+ ` + }); + + angular + .module('firebotApp') + .component("firebotRadio", { + bindings: { + label: "@", + description: "@?", + tooltip: "@?", + value: "{{$ctrl.label}}
{{$ctrl.description}}
+ +
+ + ` + }); +}()); diff --git a/src/gui/app/directives/controls/firebot-radios.js b/src/gui/app/directives/controls/firebot-radios.js index 3823ad38c..1f72ac3e9 100644 --- a/src/gui/app/directives/controls/firebot-radios.js +++ b/src/gui/app/directives/controls/firebot-radios.js @@ -11,17 +11,21 @@ style: "@?" }, template: ` -
- -
+ + + `, controller: function() { const $ctrl = this; - $ctrl.labelIsObj = (label) => typeof label === "object"; + $ctrl.labelIsObj = label => typeof label === "object"; } }); }()); diff --git a/src/gui/app/directives/controls/sort-tag-dropdown.component.js b/src/gui/app/directives/controls/sort-tag-dropdown.component.js index b4bbe362f..05b1e5db0 100644 --- a/src/gui/app/directives/controls/sort-tag-dropdown.component.js +++ b/src/gui/app/directives/controls/sort-tag-dropdown.component.js @@ -26,7 +26,7 @@
+ +
+ +
+ +
`, - controller: function($scope, sortTagsService) { + controller: function($scope, $element, sortTagsService) { const $ctrl = this; + $scope.getSortTags = () => sortTagsService.getSortTagsForItem($ctrl.context, $ctrl.item.sortTags); + + $scope.getSortTagNames = () => $scope.getSortTags().map(t => t.name).join("
"); + + $scope.getOverflowTagCount = () => { + const allTags = $element.find(".sort-tags").children().toArray(); + return Math.max(allTags.reduce((acc, child) => { + const parent = child.parentNode; + if ((child.offsetLeft - parent.offsetLeft > parent.offsetWidth) || + (child.offsetTop - parent.offsetTop > parent.offsetHeight)) { + acc++; + } + return acc; + }, 0), 0); + }; + + $scope.hasOverflow = () => { + return $scope.getOverflowTagCount() > 0; + }; + $scope.sts = sortTagsService; $ctrl.removeSortTag = (tagId) => { @@ -47,20 +89,12 @@ } }; - $ctrl.getSortTagsContextMenu = () => { - if ($ctrl.item.sortTags == null) { - $ctrl.item.sortTags = []; + $ctrl.toggleSortTag = (sortTag) => { + if ($ctrl.item.sortTags.some(id => id === sortTag.id)) { + $ctrl.removeSortTag(sortTag.id); + } else { + $ctrl.addSortTag(sortTag); } - - const sortTags = sortTagsService.getSortTags($ctrl.context).filter(st => !$ctrl.item.sortTags.includes(st.id)); - return sortTags.map(st => { - return { - html: ` ${st.name}`, - click: () => { - $ctrl.addSortTag(st); - } - }; - }); }; $ctrl.$onInit = () => { diff --git a/src/gui/app/directives/controls/time-input.js b/src/gui/app/directives/controls/time-input.js index 2e7c4ce68..7c2258f7a 100644 --- a/src/gui/app/directives/controls/time-input.js +++ b/src/gui/app/directives/controls/time-input.js @@ -9,7 +9,8 @@ ngModel: "<", validationError: " { + if ($ctrl.maxTimeUnit != null && $ctrl.timeUnits.includes($ctrl.maxTimeUnit)) { + $ctrl.timeUnits.length = $ctrl.timeUnits.findIndex(unit => unit === $ctrl.maxTimeUnit) + 1; + } + if ($ctrl.ngModel != null) { $ctrl.selectedTimeUnit = determineTimeUnit($ctrl.ngModel); diff --git a/src/gui/app/directives/misc/ad-break-indicator.component.js b/src/gui/app/directives/misc/ad-break-indicator.component.js new file mode 100644 index 000000000..6360687ef --- /dev/null +++ b/src/gui/app/directives/misc/ad-break-indicator.component.js @@ -0,0 +1,70 @@ +"use strict"; + +(function() { + + const moment = require("moment"); + + angular.module("firebotApp") + .component("adBreakIndicator", { + bindings: {}, + template: ` +
+ + {{abs.adRunning ? 'REMAINING' : 'STARTS IN'}} + {{timeLeftDisplay}} + ({{abs.friendlyDuration}} break) +
+ `, + controller: function($scope, adBreakService, $interval) { + const $ctrl = this; + + $scope.abs = adBreakService; + + $scope.timeLeftDisplay = "0:00"; + + function updateTimeLeftDisplay() { + const endsAt = moment(adBreakService.adRunning + ? adBreakService.endsAt + : adBreakService.nextAdBreak + ); + const now = moment(); + + if (now.isAfter(endsAt)) { + $scope.timeLeftDisplay = adBreakService.adRunning + ? "ENDING" + : "SOON"; + return; + } + + const secondsLeft = Math.abs(now.diff(endsAt, "seconds")); + + const allSecs = Math.round(secondsLeft); + + const divisorForMinutes = allSecs % (60 * 60); + const minutes = Math.floor(divisorForMinutes / 60); + + const divisorForSeconds = divisorForMinutes % 60; + const seconds = Math.ceil(divisorForSeconds); + + const minDisplay = minutes.toString().padStart(1, "0"), + secDisplay = seconds.toString().padStart(2, "0"); + + $scope.timeLeftDisplay = `${minDisplay}:${secDisplay}`; + } + + $ctrl.$onInit = function() { + updateTimeLeftDisplay(); + }; + + $interval(() => { + updateTimeLeftDisplay(); + }, 1000); + } + }); +}()); diff --git a/src/gui/app/directives/misc/firebot-item-table/firebot-item-table.html b/src/gui/app/directives/misc/firebot-item-table/firebot-item-table.html index 706030c04..272816a28 100644 --- a/src/gui/app/directives/misc/firebot-item-table/firebot-item-table.html +++ b/src/gui/app/directives/misc/firebot-item-table/firebot-item-table.html @@ -55,20 +55,40 @@ -
- +
+ {{header.name}} + + +
- - TAGS + +
+ + + + + TAGS + + + + +
@@ -78,7 +98,7 @@ @@ -95,6 +115,7 @@
- - + + + +