mirror of
https://github.com/misenhower/splatoon2.ink.git
synced 2026-10-01 05:37:06 -05:00
Support explicit Puppeteer and Cloudflare screenshot providers
This commit is contained in:
376
package-lock.json
generated
376
package-lock.json
generated
@@ -23,6 +23,8 @@
|
||||
"lodash": "^4.18.1",
|
||||
"mkdirp": "^3.0.1",
|
||||
"moment-timezone": "^0.6.3",
|
||||
"puppeteer": "^25.10.0",
|
||||
"serve-handler": "^6.1.7",
|
||||
"sharp": "^0.35.4",
|
||||
"v-click-outside": "^3.0.1",
|
||||
"vue": "^2.7.16",
|
||||
@@ -3877,6 +3879,186 @@
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@puppeteer/browsers": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.2.tgz",
|
||||
"integrity": "sha512-q2BU4YfO9h/Wt7IcWPcggpOOqLk2Tbs1hDwolvKZrweRjy751OJBKMN9zO5bfD0pzU7X/tvKw/exQds4pM/LOg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"modern-tar": "^0.8.4",
|
||||
"yargs": "^18.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"browsers": "lib/main-cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"proxy-agent": ">=8.0.1",
|
||||
"yauzl": "^2.10.0 || ^3.4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"proxy-agent": {
|
||||
"optional": true
|
||||
},
|
||||
"yauzl": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@puppeteer/browsers/node_modules/ansi-regex": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz",
|
||||
"integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@puppeteer/browsers/node_modules/ansi-styles": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
||||
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@puppeteer/browsers/node_modules/cliui": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
|
||||
"integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^7.2.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"wrap-ansi": "^9.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@puppeteer/browsers/node_modules/cliui/node_modules/string-width": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
|
||||
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^10.3.0",
|
||||
"get-east-asian-width": "^1.0.0",
|
||||
"strip-ansi": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@puppeteer/browsers/node_modules/emoji-regex": {
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
|
||||
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@puppeteer/browsers/node_modules/string-width": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz",
|
||||
"integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-east-asian-width": "^1.5.0",
|
||||
"strip-ansi": "^7.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@puppeteer/browsers/node_modules/strip-ansi": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
|
||||
"integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@puppeteer/browsers/node_modules/wrap-ansi": {
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
|
||||
"integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.2.1",
|
||||
"string-width": "^7.0.0",
|
||||
"strip-ansi": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@puppeteer/browsers/node_modules/wrap-ansi/node_modules/string-width": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
|
||||
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^10.3.0",
|
||||
"get-east-asian-width": "^1.0.0",
|
||||
"strip-ansi": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@puppeteer/browsers/node_modules/yargs": {
|
||||
"version": "18.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-18.1.0.tgz",
|
||||
"integrity": "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^9.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"string-width": "^8.2.1",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^22.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=23"
|
||||
}
|
||||
},
|
||||
"node_modules/@puppeteer/browsers/node_modules/yargs-parser": {
|
||||
"version": "22.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
|
||||
"integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=23"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm-eabi": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.7.tgz",
|
||||
@@ -5961,7 +6143,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
@@ -6118,7 +6299,6 @@
|
||||
"version": "1.1.18",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
||||
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
@@ -6420,6 +6600,22 @@
|
||||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/chromium-bidi": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-17.0.2.tgz",
|
||||
"integrity": "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"mitt": "^3.0.1",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.19.0 <22.0.0 || >=22.12.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"devtools-protocol": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/cjs-module-lexer": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.1.tgz",
|
||||
@@ -6676,7 +6872,6 @@
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/connect-history-api-fallback": {
|
||||
@@ -7510,6 +7705,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/devtools-protocol": {
|
||||
"version": "0.0.1666840",
|
||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1666840.tgz",
|
||||
"integrity": "sha512-gCcO42XCHKEs7Ag0S7aGYsnJ7hlgrO3qderYqeiY0Eqk+0GFfuvT13IA0hHreJTa2KCdDVyGMeOhdMNmrrTjVg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/dir-glob": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||
@@ -7854,7 +8055,6 @@
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -8767,12 +8967,23 @@
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-east-asian-width": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz",
|
||||
"integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
@@ -10323,6 +10534,18 @@
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lilconfig": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
||||
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antonk52"
|
||||
}
|
||||
},
|
||||
"node_modules/lines-and-columns": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||
@@ -10806,6 +11029,27 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.18",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz",
|
||||
"integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "~1.33.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types/node_modules/mime-db": {
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
|
||||
"integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mimic-fn": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
|
||||
@@ -11520,7 +11764,6 @@
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
@@ -11677,6 +11920,12 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/mitt": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
||||
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
|
||||
@@ -11692,6 +11941,15 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/modern-tar": {
|
||||
"version": "0.8.5",
|
||||
"resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.5.tgz",
|
||||
"integrity": "sha512-snEhs+6G5Tjd4I7tLCDOaoln2RgE0bD19RzEKgvgK2hZ5VKy3MpLhLTZ2fWpXSTg4K2cyPwp+VHATFJhxfnOeA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/module-alias": {
|
||||
"version": "2.3.4",
|
||||
"resolved": "https://registry.npmjs.org/module-alias/-/module-alias-2.3.4.tgz",
|
||||
@@ -12416,6 +12674,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-is-inside": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
|
||||
"integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==",
|
||||
"license": "(WTFPL OR MIT)"
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
@@ -13355,6 +13619,44 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/puppeteer": {
|
||||
"version": "25.10.0",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.10.0.tgz",
|
||||
"integrity": "sha512-9ZfkiaZDQWpGPJp9XTS+Bkn/D78hPvYmtjPfIBeybn05oeY6Jj7aiSbYdfcSQD2UMvC0vE7Yi9PSDo179euRzw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@puppeteer/browsers": "3.2.2",
|
||||
"chromium-bidi": "17.0.2",
|
||||
"devtools-protocol": "0.0.1666840",
|
||||
"lilconfig": "^3.1.3",
|
||||
"puppeteer-core": "25.10.0",
|
||||
"typed-query-selector": "^2.12.2"
|
||||
},
|
||||
"bin": {
|
||||
"puppeteer": "lib/puppeteer/node/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/puppeteer-core": {
|
||||
"version": "25.10.0",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.10.0.tgz",
|
||||
"integrity": "sha512-Hy5eMQshOEMil4JUUx03h5pw1HYkYCso1RG/gcpPlFSd4cYPOcopxcXEAxpLPOkOPJb9LIJtwxuj66bSdvknFg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@puppeteer/browsers": "3.2.2",
|
||||
"chromium-bidi": "17.0.2",
|
||||
"devtools-protocol": "0.0.1666840",
|
||||
"typed-query-selector": "^2.12.2",
|
||||
"webdriver-bidi-protocol": "0.4.3",
|
||||
"ws": "^8.21.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/purgecss": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/purgecss/-/purgecss-8.0.0.tgz",
|
||||
@@ -14137,6 +14439,54 @@
|
||||
"randombytes": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-handler": {
|
||||
"version": "6.1.7",
|
||||
"resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.7.tgz",
|
||||
"integrity": "sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "3.0.0",
|
||||
"content-disposition": "0.5.2",
|
||||
"mime-types": "2.1.18",
|
||||
"minimatch": "3.1.5",
|
||||
"path-is-inside": "1.0.2",
|
||||
"path-to-regexp": "3.3.0",
|
||||
"range-parser": "1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-handler/node_modules/bytes": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
|
||||
"integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-handler/node_modules/content-disposition": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
|
||||
"integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-handler/node_modules/path-to-regexp": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz",
|
||||
"integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/serve-handler/node_modules/range-parser": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
|
||||
"integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-index": {
|
||||
"version": "1.9.2",
|
||||
"resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.2.tgz",
|
||||
@@ -15256,6 +15606,12 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/typed-query-selector": {
|
||||
"version": "2.12.2",
|
||||
"resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz",
|
||||
"integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz",
|
||||
@@ -15907,6 +16263,12 @@
|
||||
"defaults": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/webdriver-bidi-protocol": {
|
||||
"version": "0.4.3",
|
||||
"resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.3.tgz",
|
||||
"integrity": "sha512-uuN0goWfxP22B7J/uAgBpOYNPttC+XVseYE+rSY5+rQ+YBeVz/VORw8WbmLVcqW78zNg5A4qnjNXYUWR3il2ig==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
@@ -16591,7 +16953,6 @@
|
||||
"version": "8.21.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
|
||||
"integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
@@ -16623,7 +16984,6 @@
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
|
||||
@@ -39,7 +39,8 @@
|
||||
"updater:tail": "wrangler tail --config workers/updater/wrangler.jsonc",
|
||||
"social": "node src/app/index.js social",
|
||||
"social:test": "node src/app/index.js socialTest",
|
||||
"admin:preview": "node workers/updater/preview/server.mjs"
|
||||
"admin:preview": "node workers/updater/preview/server.mjs",
|
||||
"screenshot": "node src/app/screenshots/cli.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.20.42",
|
||||
@@ -59,6 +60,8 @@
|
||||
"lodash": "^4.18.1",
|
||||
"mkdirp": "^3.0.1",
|
||||
"moment-timezone": "^0.6.3",
|
||||
"puppeteer": "^25.10.0",
|
||||
"serve-handler": "^6.1.7",
|
||||
"sharp": "^0.35.4",
|
||||
"v-click-outside": "^3.0.1",
|
||||
"vue": "^2.7.16",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import FilesystemStorage from '../common/storage/FilesystemStorage.js';
|
||||
import * as updater from './updater/index.js';
|
||||
import * as social from './social/index.js';
|
||||
import { withScreenshots } from './screenshots/node.js';
|
||||
|
||||
export function filesystemStorage() {
|
||||
return {
|
||||
@@ -20,12 +21,13 @@ export async function updateAll() {
|
||||
}
|
||||
|
||||
export async function sendStatuses() {
|
||||
let result = await social.sendStatuses(filesystemStorage());
|
||||
let clients = social.createClients();
|
||||
let result = await withScreenshots(screenshots => social.sendStatuses(filesystemStorage(), clients, screenshots));
|
||||
if (!result.ok)
|
||||
throw new Error('One or more social posts failed.');
|
||||
return result;
|
||||
}
|
||||
|
||||
export function testScreenshots() {
|
||||
return social.testScreenshots(filesystemStorage());
|
||||
return withScreenshots(screenshots => social.testScreenshots(filesystemStorage(), [], screenshots));
|
||||
}
|
||||
|
||||
79
src/app/screenshots/BrowserRunClient.js
Normal file
79
src/app/screenshots/BrowserRunClient.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import { logMessage } from '../log.js';
|
||||
import { fetchWithTimeout } from '../../common/fetch.js';
|
||||
|
||||
async function errorMessage(response) {
|
||||
let body = await response.text();
|
||||
|
||||
try {
|
||||
let messages = JSON.parse(body).errors?.map(error => error.message).filter(Boolean);
|
||||
if (messages?.length)
|
||||
return messages.join('; ');
|
||||
} catch {
|
||||
// Not JSON; use the body as-is
|
||||
}
|
||||
|
||||
return body || response.statusText || 'Unknown error';
|
||||
}
|
||||
|
||||
// Browser Run's REST client works in both Node and Workers.
|
||||
export default class BrowserRunClient {
|
||||
constructor({ accountId, apiToken }) {
|
||||
this.accountId = accountId;
|
||||
this.apiToken = apiToken;
|
||||
}
|
||||
|
||||
async capture({ url, viewport, readySelector }) {
|
||||
let missing = [];
|
||||
if (!this.accountId)
|
||||
missing.push('CLOUDFLARE_ACCOUNT_ID');
|
||||
if (!this.apiToken)
|
||||
missing.push('CLOUDFLARE_BROWSER_RUN_API_TOKEN');
|
||||
if (missing.length)
|
||||
throw new Error(`Missing screenshot configuration: ${missing.join(', ')}`);
|
||||
|
||||
let endpoint = new URL(`/client/v4/accounts/${this.accountId}/browser-rendering/screenshot`, 'https://api.cloudflare.com');
|
||||
endpoint.searchParams.set('cacheTTL', '0');
|
||||
|
||||
let request = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.apiToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url: url.toString(),
|
||||
viewport,
|
||||
gotoOptions: { waitUntil: 'domcontentloaded', timeout: 10_000 },
|
||||
waitForSelector: { selector: readySelector, timeout: 10_000 },
|
||||
actionTimeout: 10_000,
|
||||
setExtraHTTPHeaders: { 'Cache-Control': 'no-cache' },
|
||||
screenshotOptions: { type: 'png' },
|
||||
}),
|
||||
};
|
||||
let image;
|
||||
for (let attempt = 0; attempt <= 3; attempt++) {
|
||||
try {
|
||||
// Three 10s browser phases plus transport overhead; covers response body too.
|
||||
let response = await fetchWithTimeout(endpoint, request, 40_000);
|
||||
if (!response.ok) {
|
||||
let message = await errorMessage(response);
|
||||
let error = new Error(`Browser Rendering screenshot failed (${response.status}): ${message}`);
|
||||
// Browser Run reports navigation/selector/action timeouts as 422 errors.
|
||||
error.retryable = response.status === 408 || response.status >= 500
|
||||
|| (response.status === 422 && /timeout|timed out/i.test(message));
|
||||
throw error;
|
||||
}
|
||||
image = new Uint8Array(await response.arrayBuffer());
|
||||
break;
|
||||
} catch (error) {
|
||||
let retryable = error.retryable ?? ['TimeoutError', 'TypeError'].includes(error.name);
|
||||
if (!retryable || attempt === 3)
|
||||
throw error;
|
||||
let delayMs = 500 * 2 ** attempt;
|
||||
logMessage('warn', 'Retrying Browser Run screenshot', { attempt: attempt + 1, delayMs, error: error.message });
|
||||
await new Promise(resolve => setTimeout(resolve, delayMs));
|
||||
}
|
||||
}
|
||||
return image;
|
||||
}
|
||||
}
|
||||
21
src/app/screenshots/PuppeteerRenderer.js
Normal file
21
src/app/screenshots/PuppeteerRenderer.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
// This module is imported only by Node entry points, never by the Worker.
|
||||
export default class PuppeteerRenderer {
|
||||
async capture({ url, viewport, readySelector }) {
|
||||
let browser = await puppeteer.launch({ timeout: 10_000, protocolTimeout: 10_000 });
|
||||
try {
|
||||
let page = await browser.newPage();
|
||||
page.setDefaultTimeout(10_000);
|
||||
await page.setViewport(viewport);
|
||||
await page.setCacheEnabled(false);
|
||||
let response = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 10_000 });
|
||||
if (!response?.ok())
|
||||
throw new Error(`Screenshot page failed (${response?.status() ?? 'no response'}): ${url}`);
|
||||
await page.waitForSelector(readySelector);
|
||||
return new Uint8Array(await page.screenshot({ type: 'png' }));
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
84
src/app/screenshots/ScreenshotGenerator.js
Normal file
84
src/app/screenshots/ScreenshotGenerator.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import { screenshotReadySelector } from '../../common/screenshot.js';
|
||||
|
||||
export const defaultViewport = {
|
||||
// Use a 16:9 ratio for the public social image.
|
||||
// 1216 was chosen as the width because of Bulma's "widescreen" breakpoint
|
||||
width: 1216,
|
||||
height: 684,
|
||||
deviceScaleFactor: 2,
|
||||
};
|
||||
|
||||
// Routes and image dimensions are shared regardless of where the browser runs.
|
||||
export default class ScreenshotGenerator {
|
||||
constructor(renderer, siteUrl) {
|
||||
this.renderer = renderer;
|
||||
this.siteUrl = siteUrl;
|
||||
}
|
||||
|
||||
async capture({ hash, url, viewport = {} }) {
|
||||
if (!url) {
|
||||
if (!this.siteUrl)
|
||||
throw new Error('SITE_URL is required to capture a screenshot route.');
|
||||
url = new URL('/screenshots.html', this.siteUrl);
|
||||
url.hash = hash;
|
||||
}
|
||||
let captureViewport = { ...defaultViewport, ...viewport };
|
||||
let image = await this.renderer.capture({
|
||||
url: new URL(url).toString(),
|
||||
viewport: captureViewport,
|
||||
readySelector: screenshotReadySelector,
|
||||
});
|
||||
return {
|
||||
image,
|
||||
type: 'image/png',
|
||||
width: captureViewport.width * captureViewport.deviceScaleFactor,
|
||||
height: captureViewport.height * captureViewport.deviceScaleFactor,
|
||||
};
|
||||
}
|
||||
|
||||
captureScheduleScreenshot(now) {
|
||||
let hash = `/schedules/${now}`;
|
||||
|
||||
return this.capture({ hash });
|
||||
}
|
||||
|
||||
captureGearScreenshot(now) {
|
||||
let hash = `/splatNetGear/${now}`;
|
||||
|
||||
return this.capture({ hash });
|
||||
}
|
||||
|
||||
captureSalmonRunScreenshot(now, mode) {
|
||||
let hash = `/salmonRun/${now}?mode=${mode}`;
|
||||
|
||||
return this.capture({ hash });
|
||||
}
|
||||
|
||||
captureSalmonRunGearScreenshot(now) {
|
||||
let hash = `/salmonRunGear/${now}`;
|
||||
|
||||
return this.capture({ hash });
|
||||
}
|
||||
|
||||
captureNewWeaponScreenshot(now, weaponCount) {
|
||||
let hash = `/newWeapon/${now}`;
|
||||
|
||||
// There are a max of 4 weapons per row
|
||||
const rows = Math.ceil(weaponCount / 4);
|
||||
// Determine the image height based on the number of rows
|
||||
let height = rows * 320;
|
||||
// Add some extra height for the bottom banner
|
||||
height += 60;
|
||||
// Set a minimum overall image height
|
||||
height = Math.max(height, 700);
|
||||
|
||||
return this.capture({ hash, viewport: { height } });
|
||||
}
|
||||
|
||||
captureSplatfestScreenshot(region, now, regions) {
|
||||
regions = regions.join(',');
|
||||
let hash = `/splatfest/${region}/${now}?regions=${regions}`;
|
||||
|
||||
return this.capture({ hash });
|
||||
}
|
||||
}
|
||||
33
src/app/screenshots/cli.js
Normal file
33
src/app/screenshots/cli.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { existsSync } from 'node:fs';
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import { dirname } from 'node:path';
|
||||
import { parseArgs } from 'node:util';
|
||||
import { withScreenshots } from './node.js';
|
||||
|
||||
// Capture-only: no social clients, checkpoints, or production-data comparisons.
|
||||
if (existsSync('.env'))
|
||||
process.loadEnvFile();
|
||||
|
||||
try {
|
||||
let { values } = parseArgs({ options: {
|
||||
provider: { type: 'string' },
|
||||
url: { type: 'string' },
|
||||
hash: { type: 'string' },
|
||||
output: { type: 'string', default: 'dist/test-screenshots/capture.png' },
|
||||
help: { type: 'boolean' },
|
||||
} });
|
||||
if (values.help) {
|
||||
console.log('Usage: npm run screenshot -- [--provider puppeteer|cloudflare] (--url <page URL> | --hash <screenshot route>) [--output <file.png>]');
|
||||
console.log('Uses SCREENSHOT_PROVIDER and SITE_URL from the environment or .env. Without SITE_URL, Puppeteer serves dist/ temporarily.');
|
||||
} else {
|
||||
if (!!values.url === !!values.hash)
|
||||
throw new Error('Specify exactly one of --url or --hash. Use --help for examples of the options.');
|
||||
let result = await withScreenshots(screenshots => screenshots.capture(values), values);
|
||||
await mkdir(dirname(values.output), { recursive: true });
|
||||
await writeFile(values.output, result.image);
|
||||
console.log(`Saved ${result.width}×${result.height} PNG to ${values.output}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './screenshots.js';
|
||||
54
src/app/screenshots/node.js
Normal file
54
src/app/screenshots/node.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import { createServer } from 'node:http';
|
||||
import { access } from 'node:fs/promises';
|
||||
import handler from 'serve-handler';
|
||||
import BrowserRunClient from './BrowserRunClient.js';
|
||||
import PuppeteerRenderer from './PuppeteerRenderer.js';
|
||||
import ScreenshotGenerator from './ScreenshotGenerator.js';
|
||||
|
||||
// Provider selection and the temporary file server belong to the Node command.
|
||||
// The Worker constructs its BrowserRunClient directly.
|
||||
export async function withScreenshots(callback, {
|
||||
provider = process.env.SCREENSHOT_PROVIDER,
|
||||
siteUrl = process.env.SITE_URL,
|
||||
url,
|
||||
} = {}) {
|
||||
let renderer;
|
||||
if (provider === 'cloudflare') {
|
||||
renderer = new BrowserRunClient({
|
||||
accountId: process.env.CLOUDFLARE_ACCOUNT_ID,
|
||||
apiToken: process.env.CLOUDFLARE_BROWSER_RUN_API_TOKEN,
|
||||
});
|
||||
if (!siteUrl && !url)
|
||||
throw new Error('SITE_URL or --url is required for Cloudflare screenshots.');
|
||||
} else if (provider === 'puppeteer') {
|
||||
renderer = new PuppeteerRenderer;
|
||||
} else {
|
||||
throw new Error('SCREENSHOT_PROVIDER must be "puppeteer" or "cloudflare" (or pass --provider).');
|
||||
}
|
||||
|
||||
let server;
|
||||
try {
|
||||
if (!siteUrl && !url) {
|
||||
await access('dist/screenshots.html').catch(() => {
|
||||
throw new Error('Missing dist/screenshots.html. Run npm run build, or set SITE_URL to your dev server.');
|
||||
});
|
||||
server = createServer((request, response) => {
|
||||
handler(request, response, { public: 'dist', cleanUrls: false, directoryListing: false }).catch(error => {
|
||||
console.error(error);
|
||||
response.destroy();
|
||||
});
|
||||
});
|
||||
await new Promise((resolve, reject) => {
|
||||
server.once('error', reject);
|
||||
server.listen(0, '127.0.0.1', resolve);
|
||||
});
|
||||
siteUrl = `http://127.0.0.1:${server.address().port}`;
|
||||
}
|
||||
return await callback(new ScreenshotGenerator(renderer, siteUrl));
|
||||
} finally {
|
||||
if (server) {
|
||||
server.closeAllConnections();
|
||||
await new Promise(resolve => server.close(resolve));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
import { logMessage } from '../log.js';
|
||||
import { fetchWithTimeout } from '../../common/fetch.js';
|
||||
import { screenshotReadySelector } from '../../common/screenshot.js';
|
||||
|
||||
// Screenshots of the site's screenshot page, rendered by Cloudflare Browser Rendering's REST
|
||||
// API. It is plain fetch, so the same code runs under Node and in a Worker. The page is the
|
||||
// deployed one (SITE_URL), which reads the published data.
|
||||
//
|
||||
// Configuration (environment variables / Worker secrets):
|
||||
// SITE_URL e.g. https://splatoon2.ink
|
||||
// CLOUDFLARE_ACCOUNT_ID
|
||||
// CLOUDFLARE_BROWSER_RUN_API_TOKEN an API token with Browser Rendering permission
|
||||
|
||||
const viewport = {
|
||||
// Use a 16:9 ratio for the public social image.
|
||||
// 1216 was chosen as the width because of Bulma's "widescreen" breakpoint
|
||||
width: 1216,
|
||||
height: 684,
|
||||
deviceScaleFactor: 2,
|
||||
};
|
||||
|
||||
function config() {
|
||||
let names = ['SITE_URL', 'CLOUDFLARE_ACCOUNT_ID', 'CLOUDFLARE_BROWSER_RUN_API_TOKEN'];
|
||||
let missing = names.filter(name => !process.env[name]);
|
||||
if (missing.length)
|
||||
throw new Error(`Missing screenshot configuration: ${missing.join(', ')}`);
|
||||
|
||||
return {
|
||||
siteUrl: process.env.SITE_URL,
|
||||
accountId: process.env.CLOUDFLARE_ACCOUNT_ID,
|
||||
apiToken: process.env.CLOUDFLARE_BROWSER_RUN_API_TOKEN,
|
||||
};
|
||||
}
|
||||
|
||||
async function errorMessage(response) {
|
||||
let body = await response.text();
|
||||
|
||||
try {
|
||||
let messages = JSON.parse(body).errors?.map(error => error.message).filter(Boolean);
|
||||
if (messages?.length)
|
||||
return messages.join('; ');
|
||||
} catch {
|
||||
// Not JSON; use the body as-is
|
||||
}
|
||||
|
||||
return body || response.statusText || 'Unknown error';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ hash: string, viewport?: object }} options
|
||||
* @returns {Promise<{ image: Uint8Array, type: string, width: number, height: number }>}
|
||||
*/
|
||||
export async function captureScreenshot({ hash, viewport: viewportOverrides }) {
|
||||
let { siteUrl, accountId, apiToken } = config();
|
||||
let thisViewport = Object.assign({}, viewport, viewportOverrides);
|
||||
|
||||
let url = new URL('/screenshots.html', siteUrl);
|
||||
url.hash = hash;
|
||||
|
||||
let endpoint = new URL(`/client/v4/accounts/${accountId}/browser-rendering/screenshot`, 'https://api.cloudflare.com');
|
||||
endpoint.searchParams.set('cacheTTL', '0');
|
||||
|
||||
let request = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url: url.toString(),
|
||||
viewport: thisViewport,
|
||||
gotoOptions: { waitUntil: 'domcontentloaded', timeout: 10_000 },
|
||||
waitForSelector: { selector: screenshotReadySelector, timeout: 10_000 },
|
||||
actionTimeout: 10_000,
|
||||
setExtraHTTPHeaders: { 'Cache-Control': 'no-cache' },
|
||||
screenshotOptions: { type: 'png' },
|
||||
}),
|
||||
};
|
||||
let image;
|
||||
for (let attempt = 0; attempt <= 3; attempt++) {
|
||||
try {
|
||||
// Three 10s browser phases plus transport overhead; covers response body too.
|
||||
let response = await fetchWithTimeout(endpoint, request, 40_000);
|
||||
if (!response.ok) {
|
||||
let message = await errorMessage(response);
|
||||
let error = new Error(`Browser Rendering screenshot failed (${response.status}): ${message}`);
|
||||
// Browser Run reports navigation/selector/action timeouts as 422 errors.
|
||||
error.retryable = response.status === 408 || response.status >= 500
|
||||
|| (response.status === 422 && /timeout|timed out/i.test(message));
|
||||
throw error;
|
||||
}
|
||||
image = new Uint8Array(await response.arrayBuffer());
|
||||
break;
|
||||
} catch (error) {
|
||||
let retryable = error.retryable ?? ['TimeoutError', 'TypeError'].includes(error.name);
|
||||
if (!retryable || attempt === 3)
|
||||
throw error;
|
||||
let delayMs = 500 * 2 ** attempt;
|
||||
logMessage('warn', 'Retrying Browser Run screenshot', { attempt: attempt + 1, delayMs, error: error.message });
|
||||
await new Promise(resolve => setTimeout(resolve, delayMs));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
image,
|
||||
type: 'image/png',
|
||||
width: thisViewport.width * thisViewport.deviceScaleFactor,
|
||||
height: thisViewport.height * thisViewport.deviceScaleFactor,
|
||||
};
|
||||
}
|
||||
|
||||
export function captureScheduleScreenshot(now) {
|
||||
let hash = `/schedules/${now}`;
|
||||
|
||||
return captureScreenshot({ hash });
|
||||
}
|
||||
|
||||
export function captureGearScreenshot(now) {
|
||||
let hash = `/splatNetGear/${now}`;
|
||||
|
||||
return captureScreenshot({ hash });
|
||||
}
|
||||
|
||||
export function captureSalmonRunScreenshot(now, mode) {
|
||||
let hash = `/salmonRun/${now}?mode=${mode}`;
|
||||
|
||||
return captureScreenshot({ hash });
|
||||
}
|
||||
|
||||
export function captureSalmonRunGearScreenshot(now) {
|
||||
let hash = `/salmonRunGear/${now}`;
|
||||
|
||||
return captureScreenshot({ hash });
|
||||
}
|
||||
|
||||
export function captureNewWeaponScreenshot(now, weaponCount) {
|
||||
let hash = `/newWeapon/${now}`;
|
||||
|
||||
// There are a max of 4 weapons per row
|
||||
const rows = Math.ceil(weaponCount / 4);
|
||||
// Determine the image height based on the number of rows
|
||||
let height = rows * 320;
|
||||
// Add some extra height for the bottom banner
|
||||
height += 60;
|
||||
// Set a minimum overall image height
|
||||
height = Math.max(height, 700);
|
||||
|
||||
return captureScreenshot({ hash, viewport: { height } });
|
||||
}
|
||||
|
||||
export function captureSplatfestScreenshot(region, now, regions) {
|
||||
regions = regions.join(',');
|
||||
let hash = `/splatfest/${region}/${now}?regions=${regions}`;
|
||||
|
||||
return captureScreenshot({ hash });
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import SocialPostBase from './SocialPostBase.js';
|
||||
import { captureGearScreenshot } from '../../screenshots/index.js';
|
||||
import { getTopOfCurrentHour } from '../../../common/time.js';
|
||||
|
||||
export default class GearPost extends SocialPostBase {
|
||||
@@ -29,7 +28,7 @@ export default class GearPost extends SocialPostBase {
|
||||
|
||||
getImage(data) {
|
||||
let now = getTopOfCurrentHour();
|
||||
return captureGearScreenshot(now);
|
||||
return this.screenshots.captureGearScreenshot(now);
|
||||
}
|
||||
|
||||
getPublicImageFilename() {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import SocialPostBase from './SocialPostBase.js';
|
||||
import { captureNewWeaponScreenshot } from '../../screenshots/index.js';
|
||||
|
||||
export default class NewWeaponPost extends SocialPostBase {
|
||||
getKey() { return 'weapon'; }
|
||||
@@ -31,7 +30,7 @@ export default class NewWeaponPost extends SocialPostBase {
|
||||
}
|
||||
|
||||
getImage(data) {
|
||||
return captureNewWeaponScreenshot(data[0].release_time, data.length);
|
||||
return this.screenshots.captureNewWeaponScreenshot(data[0].release_time, data.length);
|
||||
}
|
||||
|
||||
getText(data) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import SocialPostBase from './SocialPostBase.js';
|
||||
import { captureSalmonRunGearScreenshot } from '../../screenshots/index.js';
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
export default class SalmonRunGearPost extends SocialPostBase {
|
||||
@@ -22,7 +21,7 @@ export default class SalmonRunGearPost extends SocialPostBase {
|
||||
}
|
||||
|
||||
getImage(data) {
|
||||
return captureSalmonRunGearScreenshot(data.available_time);
|
||||
return this.screenshots.captureSalmonRunGearScreenshot(data.available_time);
|
||||
}
|
||||
|
||||
getText(data) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import SocialPostBase from './SocialPostBase.js';
|
||||
import { captureSalmonRunScreenshot } from '../../screenshots/index.js';
|
||||
|
||||
const PREVIOUS_SCHEDULE_KEY = 'salmonrun-previousSchedule.json';
|
||||
|
||||
@@ -83,7 +82,7 @@ export default class SalmonRunPost extends SocialPostBase {
|
||||
|
||||
async getImage(data) {
|
||||
let mode = (data.current) ? 'current' : 'upcoming';
|
||||
return captureSalmonRunScreenshot(await this.getDataTime(), mode);
|
||||
return this.screenshots.captureSalmonRunScreenshot(await this.getDataTime(), mode);
|
||||
}
|
||||
|
||||
async getText(data) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import SocialPostBase from './SocialPostBase.js';
|
||||
import { captureScheduleScreenshot } from '../../screenshots/index.js';
|
||||
import finalFest from '../../../common/data/finalFest.json' with { type: 'json' };
|
||||
import shiftyStations from '../../../common/data/shiftyStations.json' with { type: 'json' };
|
||||
|
||||
@@ -37,7 +36,7 @@ export default class SchedulePost extends SocialPostBase {
|
||||
}
|
||||
|
||||
getImage(data) {
|
||||
return captureScheduleScreenshot(data.regular.start_time);
|
||||
return this.screenshots.captureScheduleScreenshot(data.regular.start_time);
|
||||
}
|
||||
|
||||
getPublicImageFilename() {
|
||||
|
||||
@@ -9,11 +9,13 @@ export default class SocialPostBase {
|
||||
* publicStorage holds the site data this post reads and the public copy of its image;
|
||||
* privateStorage holds the last-posted times and other state.
|
||||
* @param {object[]} clients the social clients to post through (see ../clients/)
|
||||
* @param {import('../../screenshots/ScreenshotGenerator.js').default} screenshots
|
||||
*/
|
||||
constructor(storage = {}, clients = []) {
|
||||
constructor(storage = {}, clients = [], screenshots) {
|
||||
this.publicStorage = storage.publicStorage;
|
||||
this.privateStorage = storage.privateStorage;
|
||||
this.clients = clients;
|
||||
this.screenshots = screenshots;
|
||||
}
|
||||
|
||||
async maybePost() {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import SocialPostBase from './SocialPostBase.js';
|
||||
import { captureSplatfestScreenshot } from '../../screenshots/index.js';
|
||||
import { splatoonRegions } from '../../../common/regions.js';
|
||||
|
||||
export default class SplatfestPost extends SocialPostBase {
|
||||
constructor(region, storage, clients) {
|
||||
super(storage, clients);
|
||||
constructor(region, storage, clients, screenshots) {
|
||||
super(storage, clients, screenshots);
|
||||
|
||||
this.region = region;
|
||||
this.regionInfo = this.getRegionInfo();
|
||||
@@ -109,7 +108,7 @@ export default class SplatfestPost extends SocialPostBase {
|
||||
}
|
||||
|
||||
async getImage(data) {
|
||||
return captureSplatfestScreenshot(this.region, await this.getDataTime(), await this.regions(data));
|
||||
return this.screenshots.captureSplatfestScreenshot(this.region, await this.getDataTime(), await this.regions(data));
|
||||
}
|
||||
|
||||
async getText(data) {
|
||||
|
||||
@@ -9,17 +9,18 @@ import SplatfestPost from './SplatfestPost.js';
|
||||
/**
|
||||
* @param {{ publicStorage: object, privateStorage: object }} storage
|
||||
* @param {object[]} clients
|
||||
* @param {import('../../screenshots/ScreenshotGenerator.js').default} screenshots
|
||||
*/
|
||||
export function createPosts(storage, clients) {
|
||||
export function createPosts(storage, clients, screenshots) {
|
||||
return [
|
||||
new SchedulePost(storage, clients),
|
||||
new GearPost(storage, clients),
|
||||
new SalmonRunPost(storage, clients),
|
||||
// new SalmonRunGearPost(storage, clients),
|
||||
new NewWeaponPost(storage, clients),
|
||||
new NewStagePost(storage, clients),
|
||||
new SplatfestPost('na', storage, clients),
|
||||
new SplatfestPost('eu', storage, clients),
|
||||
new SplatfestPost('jp', storage, clients),
|
||||
new SchedulePost(storage, clients, screenshots),
|
||||
new GearPost(storage, clients, screenshots),
|
||||
new SalmonRunPost(storage, clients, screenshots),
|
||||
// new SalmonRunGearPost(storage, clients, screenshots),
|
||||
new NewWeaponPost(storage, clients, screenshots),
|
||||
new NewStagePost(storage, clients, screenshots),
|
||||
new SplatfestPost('na', storage, clients, screenshots),
|
||||
new SplatfestPost('eu', storage, clients, screenshots),
|
||||
new SplatfestPost('jp', storage, clients, screenshots),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -9,17 +9,18 @@ export function createClients() {
|
||||
* Post whatever is due for the current hour.
|
||||
* @param {{ publicStorage: object, privateStorage: object }} storage
|
||||
* @param {object[]} [clients]
|
||||
* @param {import('../screenshots/ScreenshotGenerator.js').default} screenshots
|
||||
*/
|
||||
export async function sendStatuses(storage, clients = createClients()) {
|
||||
export async function sendStatuses(storage, clients = createClients(), screenshots) {
|
||||
let posts = [];
|
||||
for (let post of createPosts(storage, clients)) {
|
||||
for (let post of createPosts(storage, clients, screenshots)) {
|
||||
let result = await post.maybePost();
|
||||
posts.push({ key: post.getKey(), ...(result || { ok: true, skipped: true }) });
|
||||
}
|
||||
return { ok: posts.every(post => post.ok), posts };
|
||||
}
|
||||
|
||||
export async function testScreenshots(storage, clients = createClients()) {
|
||||
for (let post of createPosts(storage, clients))
|
||||
export async function testScreenshots(storage, clients = createClients(), screenshots) {
|
||||
for (let post of createPosts(storage, clients, screenshots))
|
||||
await post.saveTestScreenshot();
|
||||
}
|
||||
|
||||
96
test/screenshots/node.test.js
Normal file
96
test/screenshots/node.test.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import { test, mock, afterEach } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import puppeteer from 'puppeteer';
|
||||
import PuppeteerRenderer from '../../src/app/screenshots/PuppeteerRenderer.js';
|
||||
import { withScreenshots } from '../../src/app/screenshots/node.js';
|
||||
|
||||
const PNG = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
|
||||
afterEach(() => mock.restoreAll());
|
||||
|
||||
test('local browser uses page readiness and closes on success or failure', async () => {
|
||||
let closed = 0;
|
||||
let fail = false;
|
||||
let ready;
|
||||
let page = {
|
||||
setDefaultTimeout(timeout) { assert.equal(timeout, 10_000); },
|
||||
async setViewport() {},
|
||||
async setCacheEnabled() {},
|
||||
async goto(url, options) {
|
||||
assert.equal(url, 'http://localhost:8080/screenshots.html');
|
||||
assert.equal(options.waitUntil, 'domcontentloaded');
|
||||
return { ok: () => true };
|
||||
},
|
||||
async waitForSelector(selector) {
|
||||
ready = selector;
|
||||
if (fail)
|
||||
throw new Error('Page not ready');
|
||||
},
|
||||
async screenshot(options) {
|
||||
assert.equal(options.type, 'png');
|
||||
return PNG;
|
||||
},
|
||||
};
|
||||
mock.method(puppeteer, 'launch', async () => ({
|
||||
newPage: async () => page,
|
||||
close: async () => { closed++; },
|
||||
}));
|
||||
let renderer = new PuppeteerRenderer;
|
||||
let options = { url: 'http://localhost:8080/screenshots.html', viewport: {}, readySelector: '[data-screenshot-ready="true"]' };
|
||||
assert.deepEqual(await renderer.capture(options), PNG);
|
||||
assert.equal(ready, options.readySelector);
|
||||
fail = true;
|
||||
await assert.rejects(renderer.capture(options), /Page not ready/);
|
||||
assert.equal(closed, 2);
|
||||
});
|
||||
|
||||
test('Node can select Cloudflare for a direct URL without SITE_URL or social data', async () => {
|
||||
let requests = [];
|
||||
mock.method(globalThis, 'fetch', async (url, init) => {
|
||||
requests.push(JSON.parse(init.body));
|
||||
return new Response(PNG);
|
||||
});
|
||||
let previous = { ...process.env };
|
||||
try {
|
||||
process.env.CLOUDFLARE_ACCOUNT_ID = 'account';
|
||||
process.env.CLOUDFLARE_BROWSER_RUN_API_TOKEN = 'token';
|
||||
let url = 'https://dev.example.test/screenshots.html#/schedules/3600';
|
||||
let result = await withScreenshots(screenshots => screenshots.capture({ url }), {
|
||||
provider: 'cloudflare', siteUrl: '', url,
|
||||
});
|
||||
assert.deepEqual(result.image, PNG);
|
||||
assert.equal(requests[0].url, url);
|
||||
} finally {
|
||||
process.env = previous;
|
||||
}
|
||||
});
|
||||
|
||||
test('provider selection is explicit and Cloudflare requires a reachable target', async () => {
|
||||
await assert.rejects(withScreenshots(() => {}, { provider: 'unknown' }), /SCREENSHOT_PROVIDER/);
|
||||
await assert.rejects(withScreenshots(() => {}, { provider: 'cloudflare', siteUrl: '' }), /SITE_URL or --url/);
|
||||
});
|
||||
|
||||
test('temporary dist server is loopback-only and closes when capture fails', async () => {
|
||||
let { mkdtemp, mkdir, writeFile, rm } = await import('node:fs/promises');
|
||||
let { tmpdir } = await import('node:os');
|
||||
let { join } = await import('node:path');
|
||||
let directory = await mkdtemp(join(tmpdir(), 'screenshot-server-'));
|
||||
let originalDirectory = process.cwd();
|
||||
let siteUrl;
|
||||
try {
|
||||
await mkdir(join(directory, 'dist'));
|
||||
await writeFile(join(directory, 'dist/screenshots.html'), '<html>local fixture</html>');
|
||||
process.chdir(directory);
|
||||
mock.method(PuppeteerRenderer.prototype, 'capture', async () => { throw new Error('Capture failed'); });
|
||||
await assert.rejects(withScreenshots(async screenshots => {
|
||||
siteUrl = screenshots.siteUrl;
|
||||
assert.equal(new URL(siteUrl).hostname, '127.0.0.1');
|
||||
let response = await fetch(new URL('/screenshots.html', siteUrl));
|
||||
assert.match(await response.text(), /local fixture/);
|
||||
return screenshots.capture({ hash: '/schedules/3600' });
|
||||
}, { provider: 'puppeteer', siteUrl: '' }), /Capture failed/);
|
||||
await assert.rejects(fetch(siteUrl));
|
||||
} finally {
|
||||
process.chdir(originalDirectory);
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -1,6 +1,14 @@
|
||||
import { test, beforeEach, afterEach, mock } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { captureScreenshot, captureNewWeaponScreenshot, captureSplatfestScreenshot } from '../../src/app/screenshots/screenshots.js';
|
||||
import BrowserRunClient from '../../src/app/screenshots/BrowserRunClient.js';
|
||||
import ScreenshotGenerator from '../../src/app/screenshots/ScreenshotGenerator.js';
|
||||
|
||||
function screenshots() {
|
||||
return new ScreenshotGenerator(new BrowserRunClient({
|
||||
accountId: process.env.CLOUDFLARE_ACCOUNT_ID,
|
||||
apiToken: process.env.CLOUDFLARE_BROWSER_RUN_API_TOKEN,
|
||||
}), process.env.SITE_URL);
|
||||
}
|
||||
|
||||
const PNG = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
|
||||
|
||||
@@ -22,7 +30,7 @@ afterEach(() => mock.restoreAll());
|
||||
|
||||
test('asks Browser Rendering for the deployed screenshot page at the default viewport', async () => {
|
||||
const requests = fakeBrowserRendering();
|
||||
const result = await captureScreenshot({ hash: '/schedules/3600' });
|
||||
const result = await screenshots().capture({ hash: '/schedules/3600' });
|
||||
|
||||
assert.equal(requests.length, 1);
|
||||
const [{ url, headers, body }] = requests;
|
||||
@@ -40,25 +48,25 @@ test('asks Browser Rendering for the deployed screenshot page at the default vie
|
||||
|
||||
test('the new-weapon screenshot grows with the number of weapons, and splatfest passes its regions', async () => {
|
||||
const requests = fakeBrowserRendering();
|
||||
const tall = await captureNewWeaponScreenshot(3600, 9);
|
||||
const tall = await screenshots().captureNewWeaponScreenshot(3600, 9);
|
||||
assert.equal(requests[0].body.viewport.height, 3 * 320 + 60);
|
||||
assert.equal(tall.height, (3 * 320 + 60) * 2);
|
||||
const short = await captureNewWeaponScreenshot(3600, 1);
|
||||
const short = await screenshots().captureNewWeaponScreenshot(3600, 1);
|
||||
assert.equal(requests[1].body.viewport.height, 700);
|
||||
assert.equal(short.width, 2432);
|
||||
|
||||
await captureSplatfestScreenshot('na', 3600, ['na', 'eu']);
|
||||
await screenshots().captureSplatfestScreenshot('na', 3600, ['na', 'eu']);
|
||||
assert.equal(requests[2].body.url, 'https://example.test/screenshots.html#/splatfest/na/3600?regions=na,eu');
|
||||
});
|
||||
|
||||
test('reports API errors with Cloudflare\'s message', async () => {
|
||||
fakeBrowserRendering(() => Response.json({ errors: [{ message: 'Invalid token' }] }, { status: 401 }));
|
||||
await assert.rejects(captureScreenshot({ hash: '/x' }), /Browser Rendering screenshot failed \(401\): Invalid token/);
|
||||
await assert.rejects(screenshots().capture({ hash: '/x' }), /Browser Rendering screenshot failed \(401\): Invalid token/);
|
||||
});
|
||||
|
||||
test('fails clearly when configuration is missing', async () => {
|
||||
delete process.env.CLOUDFLARE_BROWSER_RUN_API_TOKEN;
|
||||
await assert.rejects(captureScreenshot({ hash: '/x' }), /Missing screenshot configuration: CLOUDFLARE_BROWSER_RUN_API_TOKEN/);
|
||||
await assert.rejects(screenshots().capture({ hash: '/x' }), /Missing screenshot configuration: CLOUDFLARE_BROWSER_RUN_API_TOKEN/);
|
||||
});
|
||||
|
||||
function immediateBackoff() {
|
||||
@@ -76,7 +84,7 @@ test('retries timeout responses and succeeds with the same screenshot request',
|
||||
const requests = fakeBrowserRendering(() => ++attempt < 3
|
||||
? Response.json({ errors: [{ message: 'Navigation timeout of 10000 ms exceeded' }] }, { status: 422 })
|
||||
: new Response(PNG));
|
||||
const result = await captureScreenshot({ hash: '/schedules/3600' });
|
||||
const result = await screenshots().capture({ hash: '/schedules/3600' });
|
||||
assert.deepEqual(result.image, PNG);
|
||||
assert.equal(requests.length, 3);
|
||||
assert.deepEqual(requests[0].body, requests[2].body);
|
||||
@@ -86,7 +94,7 @@ test('retries timeout responses and succeeds with the same screenshot request',
|
||||
test('stops after three retries and preserves the final error', async () => {
|
||||
const delays = immediateBackoff();
|
||||
const requests = fakeBrowserRendering(() => new Response('upstream unavailable', { status: 503 }));
|
||||
await assert.rejects(captureScreenshot({ hash: '/x' }), /503.*upstream unavailable/);
|
||||
await assert.rejects(screenshots().capture({ hash: '/x' }), /503.*upstream unavailable/);
|
||||
assert.equal(requests.length, 4);
|
||||
assert.deepEqual(delays, [500, 1000, 2000]);
|
||||
});
|
||||
@@ -96,7 +104,7 @@ test('does not retry authentication, rate limits or non-timeout validation error
|
||||
for (const status of [401, 403, 429, 422]) {
|
||||
let count = 0;
|
||||
const requests = fakeBrowserRendering(() => { count++; return new Response('invalid request', { status }); });
|
||||
await assert.rejects(captureScreenshot({ hash: '/x' }), new RegExp(String(status)));
|
||||
await assert.rejects(screenshots().capture({ hash: '/x' }), new RegExp(String(status)));
|
||||
assert.equal(count, 1);
|
||||
assert.equal(requests.length, 1);
|
||||
}
|
||||
@@ -113,6 +121,6 @@ test('retries network and client deadline failures, including while reading the
|
||||
if (attempt === 3) return new Response(new ReadableStream({ start(controller) { controller.error(new TypeError('connection reset')); } }));
|
||||
return new Response(PNG);
|
||||
});
|
||||
assert.deepEqual((await captureScreenshot({ hash: '/x' })).image, PNG);
|
||||
assert.deepEqual((await screenshots().capture({ hash: '/x' })).image, PNG);
|
||||
assert.equal(requests.length, 4);
|
||||
});
|
||||
|
||||
@@ -25,9 +25,10 @@ function pinTime(post, time) {
|
||||
}
|
||||
|
||||
test('createPosts builds every post against the same storage and clients', () => {
|
||||
const posts = createPosts(s, [client]);
|
||||
const screenshots = {};
|
||||
const posts = createPosts(s, [client], screenshots);
|
||||
assert.deepEqual(posts.map(t => t.getKey()), ['schedule', 'gear', 'salmonrun', 'weapon', 'newstage', 'splatfest-na', 'splatfest-eu', 'splatfest-jp']);
|
||||
assert.ok(posts.every(t => t.publicStorage === s.publicStorage && t.clients[0] === client));
|
||||
assert.ok(posts.every(t => t.publicStorage === s.publicStorage && t.clients[0] === client && t.screenshots === screenshots));
|
||||
});
|
||||
|
||||
test('schedule: posts the rotation for the current hour, with new-stage wording when the stage is new', async () => {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import BrowserRunClient from '../../../src/app/screenshots/BrowserRunClient.js';
|
||||
import ScreenshotGenerator from '../../../src/app/screenshots/ScreenshotGenerator.js';
|
||||
import stringify from 'json-stable-stringify';
|
||||
import { sendStatuses, createClients } from '../../../src/app/social/index.js';
|
||||
import { fetchWithTimeout } from '../../../src/common/fetch.js';
|
||||
@@ -32,7 +34,11 @@ export async function runPosters(env) {
|
||||
for (let client of clients)
|
||||
if (await client.canSend())
|
||||
enabled.push(client.key);
|
||||
let result = await sendStatuses(storage, clients);
|
||||
let screenshots = new ScreenshotGenerator(new BrowserRunClient({
|
||||
accountId: process.env.CLOUDFLARE_ACCOUNT_ID,
|
||||
apiToken: process.env.CLOUDFLARE_BROWSER_RUN_API_TOKEN,
|
||||
}), process.env.SITE_URL);
|
||||
let result = await sendStatuses(storage, clients, screenshots);
|
||||
let summary = { ...result, ms: Date.now() - started, clients: enabled };
|
||||
createLogger('social')[summary.ok ? 'info' : 'error']('Social run finished', summary);
|
||||
return summary;
|
||||
|
||||
Reference in New Issue
Block a user