diff --git a/package-lock.json b/package-lock.json
index 3905c89d..82168457 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
"date-fns": "^2.30.0",
"express": "^4.18.2",
"got": "^12.6.0",
+ "jsdom": "^24.1.1",
"klona": "^2.0.6",
"moment": "^2.29.4",
"obs-websocket-js": "^5.0.2",
@@ -30,6 +31,7 @@
"@mui/icons-material": "^5.11.16",
"@mui/material": "^5.12.3",
"@types/app-root-path": "^1.2.5",
+ "@types/jsdom": "^21.1.7",
"@types/node": "^18.16.4",
"@types/react": "^18.2.5",
"@types/react-dom": "^18.2.3",
@@ -1415,6 +1417,17 @@
"version": "4.0.1",
"license": "MIT"
},
+ "node_modules/@types/jsdom": {
+ "version": "21.1.7",
+ "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz",
+ "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*",
+ "@types/tough-cookie": "*",
+ "parse5": "^7.0.0"
+ }
+ },
"node_modules/@types/json-schema": {
"version": "7.0.11",
"dev": true,
@@ -1639,6 +1652,12 @@
"csstype": "^3.0.2"
}
},
+ "node_modules/@types/tough-cookie": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
+ "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
+ "dev": true
+ },
"node_modules/@types/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.1.tgz",
@@ -2726,6 +2745,22 @@
"postcss-value-parser": "^4.0.2"
}
},
+ "node_modules/cssstyle": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.0.1.tgz",
+ "integrity": "sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==",
+ "dependencies": {
+ "rrweb-cssom": "^0.6.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/cssstyle/node_modules/rrweb-cssom": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz",
+ "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw=="
+ },
"node_modules/csstype": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
@@ -2740,6 +2775,49 @@
"type": "^1.0.1"
}
},
+ "node_modules/data-urls": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
+ "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
+ "dependencies": {
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/data-urls/node_modules/tr46": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz",
+ "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/data-urls/node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/data-urls/node_modules/whatwg-url": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz",
+ "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==",
+ "dependencies": {
+ "tr46": "^5.0.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/date-fns": {
"version": "2.30.0",
"license": "MIT",
@@ -2815,6 +2893,11 @@
"node": ">=0.10.0"
}
},
+ "node_modules/decimal.js": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz",
+ "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA=="
+ },
"node_modules/decompress-response": {
"version": "6.0.0",
"license": "MIT",
@@ -3122,6 +3205,17 @@
"node": ">=8.6"
}
},
+ "node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/error-ex": {
"version": "1.3.2",
"dev": true,
@@ -3949,6 +4043,19 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/form-data-encoder": {
"version": "2.1.4",
"license": "MIT",
@@ -4408,6 +4515,17 @@
"node": ">=10"
}
},
+ "node_modules/html-encoding-sniffer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
+ "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
+ "dependencies": {
+ "whatwg-encoding": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/http-cache-semantics": {
"version": "4.1.1",
"license": "BSD-2-Clause"
@@ -4426,6 +4544,29 @@
"node": ">= 0.8"
}
},
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/http-proxy-agent/node_modules/agent-base": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
+ "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==",
+ "dependencies": {
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/http2-wrapper": {
"version": "2.2.0",
"license": "MIT",
@@ -4806,6 +4947,11 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="
+ },
"node_modules/is-promise": {
"version": "2.2.2",
"dev": true,
@@ -4961,6 +5107,99 @@
"dev": true,
"license": "Python-2.0"
},
+ "node_modules/jsdom": {
+ "version": "24.1.1",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.1.tgz",
+ "integrity": "sha512-5O1wWV99Jhq4DV7rCLIoZ/UIhyQeDR7wHVyZAHAshbrvZsLs+Xzz7gtwnlJTJDjleiTKh54F4dXrX70vJQTyJQ==",
+ "dependencies": {
+ "cssstyle": "^4.0.1",
+ "data-urls": "^5.0.0",
+ "decimal.js": "^10.4.3",
+ "form-data": "^4.0.0",
+ "html-encoding-sniffer": "^4.0.0",
+ "http-proxy-agent": "^7.0.2",
+ "https-proxy-agent": "^7.0.5",
+ "is-potential-custom-element-name": "^1.0.1",
+ "nwsapi": "^2.2.12",
+ "parse5": "^7.1.2",
+ "rrweb-cssom": "^0.7.1",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^4.1.4",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^7.0.0",
+ "whatwg-encoding": "^3.1.1",
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.0.0",
+ "ws": "^8.18.0",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "canvas": "^2.11.2"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jsdom/node_modules/agent-base": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
+ "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==",
+ "dependencies": {
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/jsdom/node_modules/https-proxy-agent": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz",
+ "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==",
+ "dependencies": {
+ "agent-base": "^7.0.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/jsdom/node_modules/tr46": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz",
+ "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/jsdom/node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/jsdom/node_modules/whatwg-url": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz",
+ "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==",
+ "dependencies": {
+ "tr46": "^5.0.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/jsesc": {
"version": "2.5.2",
"dev": true,
@@ -6091,6 +6330,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/nwsapi": {
+ "version": "2.2.12",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.12.tgz",
+ "integrity": "sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w=="
+ },
"node_modules/object-assign": {
"version": "4.1.1",
"license": "MIT",
@@ -6326,6 +6570,17 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/parse5": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
+ "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==",
+ "dependencies": {
+ "entities": "^4.4.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
"node_modules/parseurl": {
"version": "1.3.3",
"license": "MIT",
@@ -6907,10 +7162,15 @@
"node": ">= 0.10"
}
},
+ "node_modules/psl": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
+ "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag=="
+ },
"node_modules/punycode": {
- "version": "2.3.0",
- "dev": true,
- "license": "MIT",
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"engines": {
"node": ">=6"
}
@@ -6935,6 +7195,11 @@
"node": ">=0.4.x"
}
},
+ "node_modules/querystringify": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
+ "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
+ },
"node_modules/queue-microtask": {
"version": "1.2.3",
"dev": true,
@@ -7236,6 +7501,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/requires-port": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
+ },
"node_modules/resolve": {
"version": "1.20.0",
"dev": true,
@@ -7361,6 +7631,11 @@
"rollup": "^2.60.0 || ^3.0.0"
}
},
+ "node_modules/rrweb-cssom": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz",
+ "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg=="
+ },
"node_modules/run-async": {
"version": "2.4.1",
"dev": true,
@@ -7415,6 +7690,17 @@
"version": "2.1.2",
"license": "MIT"
},
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
"node_modules/scheduler": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
@@ -7856,6 +8142,11 @@
"node": ">=4"
}
},
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="
+ },
"node_modules/tar": {
"version": "6.1.13",
"dev": true,
@@ -7973,6 +8264,28 @@
"node": ">=0.6"
}
},
+ "node_modules/tough-cookie": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
+ "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
+ "dependencies": {
+ "psl": "^1.1.33",
+ "punycode": "^2.1.1",
+ "universalify": "^0.2.0",
+ "url-parse": "^1.5.3"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tough-cookie/node_modules/universalify": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
+ "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
"node_modules/tr46": {
"version": "0.0.3",
"license": "MIT"
@@ -8159,6 +8472,15 @@
"querystring": "0.2.0"
}
},
+ "node_modules/url-parse": {
+ "version": "1.5.10",
+ "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
+ "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
+ "dependencies": {
+ "querystringify": "^2.1.1",
+ "requires-port": "^1.0.0"
+ }
+ },
"node_modules/url-template": {
"version": "2.0.8",
"license": "BSD"
@@ -8255,10 +8577,51 @@
}
}
},
+ "node_modules/w3c-xmlserializer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+ "dependencies": {
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/webidl-conversions": {
"version": "3.0.1",
"license": "BSD-2-Clause"
},
+ "node_modules/whatwg-encoding": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-encoding/node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/whatwg-url": {
"version": "5.0.0",
"license": "MIT",
@@ -8355,8 +8718,9 @@
"license": "ISC"
},
"node_modules/ws": {
- "version": "8.13.0",
- "license": "MIT",
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
+ "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"engines": {
"node": ">=10.0.0"
},
@@ -8373,6 +8737,19 @@
}
}
},
+ "node_modules/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="
+ },
"node_modules/yallist": {
"version": "4.0.0",
"license": "ISC"
diff --git a/package.json b/package.json
index 6cb6ec36..cfb6d2c4 100644
--- a/package.json
+++ b/package.json
@@ -36,6 +36,7 @@
"date-fns": "^2.30.0",
"express": "^4.18.2",
"got": "^12.6.0",
+ "jsdom": "^24.1.1",
"klona": "^2.0.6",
"moment": "^2.29.4",
"obs-websocket-js": "^5.0.2",
@@ -51,6 +52,7 @@
"@mui/icons-material": "^5.11.16",
"@mui/material": "^5.12.3",
"@types/app-root-path": "^1.2.5",
+ "@types/jsdom": "^21.1.7",
"@types/node": "^18.16.4",
"@types/react": "^18.2.5",
"@types/react-dom": "^18.2.3",
diff --git a/schemas/pokemon-control.json b/schemas/pokemon-control.json
new file mode 100644
index 00000000..16742c6f
--- /dev/null
+++ b/schemas/pokemon-control.json
@@ -0,0 +1,10 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "url": {"type": "string", "default": ""},
+ "enableScraping": {"type": "boolean", "default": false}
+ },
+ "required": ["url", "enableScraping"]
+}
diff --git a/src/browser/dashboard/views/poke.tsx b/src/browser/dashboard/views/poke.tsx
index afd86e95..80543473 100644
--- a/src/browser/dashboard/views/poke.tsx
+++ b/src/browser/dashboard/views/poke.tsx
@@ -4,13 +4,31 @@ import CssBaseline from "@mui/material/CssBaseline";
import {useReplicant} from "../../use-replicant";
import {useState, useEffect} from "react";
import {render} from "../../render";
+import {FormControlLabel, Switch} from "@mui/material";
const pokeRep = nodecg.Replicant("poke");
+const pokeControlRep = nodecg.Replicant("pokemon-control");
+
+const updateUrl = (url: string) => {
+ if (!pokeControlRep.value) {
+ return;
+ }
+ pokeControlRep.value.url = url;
+};
+
+const updateEnableScraping = (enable: boolean) => {
+ if (!pokeControlRep.value) {
+ return;
+ }
+ pokeControlRep.value.enableScraping = enable;
+};
const App = () => {
const poke = useReplicant("poke");
const [pokeState, setPoke] = useState(0);
+ const pokeControl = useReplicant("pokemon-control");
+
useEffect(() => {
if (poke) {
setPoke(poke);
@@ -23,6 +41,29 @@ const App = () => {
padding: "8px",
}}
>
+ {pokeControl && (
+ <>
+ {
+ updateUrl(e.currentTarget.value);
+ }}
+ placeholder='URL'
+ />
+ {
+ updateEnableScraping(e.target.checked);
+ }}
+ />
+ }
+ label='ページから自動取得する'
+ />
+ >
+ )}
{
onChange={(e) => {
setPoke(Number(e.target.value));
}}
+ disabled={pokeControl?.enableScraping}
/>
diff --git a/src/extension/index.ts b/src/extension/index.ts
index 94af1153..0b16c2d2 100644
--- a/src/extension/index.ts
+++ b/src/extension/index.ts
@@ -6,6 +6,7 @@ import {twitch} from "./twitch";
import {setupObs} from "./obs";
import {tracker} from "./tracker";
import {music} from "./music";
+import {pokemon} from "./pokemon";
export default (nodecg: NodeCG) => {
checklist(nodecg);
@@ -30,4 +31,6 @@ export default (nodecg: NodeCG) => {
cameraStateRep.value = "hidden";
}
});
+
+ pokemon(nodecg);
};
diff --git a/src/extension/pokemon.ts b/src/extension/pokemon.ts
new file mode 100644
index 00000000..b5920bef
--- /dev/null
+++ b/src/extension/pokemon.ts
@@ -0,0 +1,66 @@
+import {JSDOM} from "jsdom";
+import {NodeCG} from "./nodecg";
+
+const GET_COUNT_INTERVAL_SECONDS = 10;
+
+export const pokemon = async (nodecg: NodeCG) => {
+ const {default: got} = await import("got");
+
+ const logger = new nodecg.Logger("pokemon");
+ const pokeRep = nodecg.Replicant("poke", {defaultValue: 0});
+ const pokemonControlRep = nodecg.Replicant("pokemon-control", {
+ defaultValue: {
+ url: "",
+ enableScraping: false,
+ },
+ });
+
+ const parseCurrentCount = (text: string): number => {
+ const [current] = text.split("/");
+ return parseInt(current?.trim() || "");
+ };
+
+ const fetchCurrentCountFromUrl = async (): Promise => {
+ if (!pokemonControlRep.value.url) {
+ logger.warn("Request URL for getting pokemon progress is not set.");
+ return null;
+ }
+ try {
+ const response = await got.get(pokemonControlRep.value.url);
+ if (response.statusCode !== 200) {
+ logger.error(`Failed to request by status code ${response.statusCode}`);
+ return null;
+ }
+ const dom = new JSDOM(response.body);
+ const [, summaryDivDom] = dom.window.document.querySelectorAll(
+ ".widget_summary > div",
+ );
+ const summaryText = summaryDivDom?.textContent;
+ if (!summaryText) {
+ logger.error("Success to request but summary is not found.");
+ return null;
+ }
+ const currentCount = parseCurrentCount(summaryText);
+ if (isNaN(currentCount)) {
+ logger.error("Failed to parse summary text.");
+ return null;
+ }
+ return currentCount;
+ } catch (e) {
+ logger.error("Failed to request.");
+ logger.error(e);
+ return null;
+ }
+ };
+
+ setInterval(async () => {
+ if (!pokemonControlRep.value.enableScraping) {
+ return;
+ }
+ const current = await fetchCurrentCountFromUrl();
+ if (!current) {
+ return;
+ }
+ pokeRep.value = current;
+ }, GET_COUNT_INTERVAL_SECONDS * 1000);
+};
diff --git a/src/nodecg/replicants.d.ts b/src/nodecg/replicants.d.ts
index 54d4501d..6f17fd22 100644
--- a/src/nodecg/replicants.d.ts
+++ b/src/nodecg/replicants.d.ts
@@ -25,6 +25,7 @@ import {DonationQueue} from "./generated/donation-queue";
import {VideoControl} from "./generated/video-control";
import {Announcements} from "./generated/announcements";
import {PlayingMusic} from "./generated/playing-music";
+import {PokemonControl} from "./generated/pokemon-control";
type Run = NonNullable;
type Runner = Run["runners"][number];
@@ -77,6 +78,7 @@ type ReplicantMap = {
announcements: Announcements;
"playing-music": PlayingMusic;
poke: Poke;
+ "pokemon-control": PokemonControl;
};
export type {
@@ -108,4 +110,5 @@ export type {
Announcements,
PlayingMusic,
Poke,
+ PokemonControl,
};