Presence embeds

This commit is contained in:
Samuel Elliott 2023-12-27 16:40:09 +00:00
parent f122ef8c92
commit cbf6e257be
No known key found for this signature in database
GPG Key ID: 8420C7CDE43DC4D6
11 changed files with 1366 additions and 11 deletions

View File

@ -24,6 +24,7 @@ RUN npm ci --production
COPY bin /app/bin
COPY resources /app/resources
COPY resources/cli/fonts /usr/local/share/fonts
COPY --from=build /app/dist /app/dist
RUN ln -s /app/bin/nxapi.js /usr/local/bin/nxapi

763
package-lock.json generated
View File

@ -20,6 +20,7 @@
"node-notifier": "^10.0.1",
"node-persist": "^3.1.3",
"read": "^3.0.0",
"sharp": "^0.33.1",
"splatnet3-types": "^0.2.20231119210145",
"supports-color": "^8.1.1",
"tslib": "^2.6.2",
@ -2682,6 +2683,15 @@
"node": ">= 10.0.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "0.44.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.44.0.tgz",
"integrity": "sha512-ZX/etZEZw8DR7zAB1eVQT40lNo0jeqpb6dCgOvctB6FIQ5PoXfMuNY8+ayQfu8tNQbAB8gQWSSJupR8NxeiZXw==",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@fastify/busboy": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz",
@ -2707,6 +2717,437 @@
"@hapi/hoek": "^9.0.0"
}
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.1.tgz",
"integrity": "sha512-esr2BZ1x0bo+wl7Gx2hjssYhjrhUsD88VQulI0FrG8/otRQUOxLWHMBd1Y1qo2Gfg2KUvXNpT0ASnV9BzJCexw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"glibc": ">=2.26",
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.0.0"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.1.tgz",
"integrity": "sha512-YrnuB3bXuWdG+hJlXtq7C73lF8ampkhU3tMxg5Hh+E7ikxbUVOU9nlNtVTloDXz6pRHt2y2oKJq7DY/yt+UXYw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"glibc": ">=2.26",
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.0.0"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.0.tgz",
"integrity": "sha512-VzYd6OwnUR81sInf3alj1wiokY50DjsHz5bvfnsFpxs5tqQxESoHtJO6xyksDs3RIkyhMWq2FufXo6GNSU9BMw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"macos": ">=11",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.0.tgz",
"integrity": "sha512-dD9OznTlHD6aovRswaPNEy8dKtSAmNo4++tO7uuR4o5VxbVAOoEQ1uSmN4iFAdQneTHws1lkTZeiXPrcCkh6IA==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"macos": ">=10.13",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.0.tgz",
"integrity": "sha512-VwgD2eEikDJUk09Mn9Dzi1OW2OJFRQK+XlBTkUNmAWPrtj8Ly0yq05DFgu1VCMx2/DqCGQVi5A1dM9hTmxf3uw==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"glibc": ">=2.28",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.0.tgz",
"integrity": "sha512-xTYThiqEZEZc0PRU90yVtM3KE7lw1bKdnDQ9kCTHWbqWyHOe4NpPOtMGy27YnN51q0J5dqRrvicfPbALIOeAZA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"glibc": ">=2.26",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.0.tgz",
"integrity": "sha512-o9E46WWBC6JsBlwU4QyU9578G77HBDT1NInd+aERfxeOPbk0qBZHgoDsQmA2v9TbqJRWzoBPx1aLOhprBMgPjw==",
"cpu": [
"s390x"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"glibc": ">=2.28",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.0.tgz",
"integrity": "sha512-naldaJy4hSVhWBgEjfdBY85CAa4UO+W1nx6a1sWStHZ7EUfNiuBTTN2KUYT5dH1+p/xij1t2QSXfCiFJoC5S/Q==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"glibc": ">=2.26",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.0.tgz",
"integrity": "sha512-OdorplCyvmSAPsoJLldtLh3nLxRrkAAAOHsGWGDYfN0kh730gifK+UZb3dWORRa6EusNqCTjfXV4GxvgJ/nPDQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"musl": ">=1.2.2",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.0.tgz",
"integrity": "sha512-FW8iK6rJrg+X2jKD0Ajhjv6y74lToIBEvkZhl42nZt563FfxkCYacrXZtd+q/sRQDypQLzY5WdLkVTbJoPyqNg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"musl": ">=1.2.2",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.1.tgz",
"integrity": "sha512-Ii4X1vnzzI4j0+cucsrYA5ctrzU9ciXERfJR633S2r39CiD8npqH2GMj63uFZRCFt3E687IenAdbwIpQOJ5BNA==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"glibc": ">=2.28",
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.0.0"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.1.tgz",
"integrity": "sha512-59B5GRO2d5N3tIfeGHAbJps7cLpuWEQv/8ySd9109ohQ3kzyCACENkFVAnGPX00HwPTQcaBNF7HQYEfZyZUFfw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"glibc": ">=2.26",
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.0.0"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.1.tgz",
"integrity": "sha512-tRGrb2pHnFUXpOAj84orYNxHADBDIr0J7rrjwQrTNMQMWA4zy3StKmMvwsI7u3dEZcgwuMMooIIGWEWOjnmG8A==",
"cpu": [
"s390x"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"glibc": ">=2.28",
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.0.0"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.1.tgz",
"integrity": "sha512-4y8osC0cAc1TRpy02yn5omBeloZZwS62fPZ0WUAYQiLhSFSpWJfY/gMrzKzLcHB9ulUV6ExFiu2elMaixKDbeg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"glibc": ">=2.26",
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.0.0"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.1.tgz",
"integrity": "sha512-D3lV6clkqIKUizNS8K6pkuCKNGmWoKlBGh5p0sLO2jQERzbakhu4bVX1Gz+RS4vTZBprKlWaf+/Rdp3ni2jLfA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"musl": ">=1.2.2",
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.0.0"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.1.tgz",
"integrity": "sha512-LOGKNu5w8uu1evVqUAUKTix2sQu1XDRIYbsi5Q0c/SrXhvJ4QyOx+GaajxmOg5PZSsSnCYPSmhjHHsRBx06/wQ==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"musl": ">=1.2.2",
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.0.0"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.1.tgz",
"integrity": "sha512-vWI/sA+0p+92DLkpAMb5T6I8dg4z2vzCUnp8yvxHlwBpzN8CIcO3xlSXrLltSvK6iMsVMNswAv+ub77rsf25lA==",
"cpu": [
"wasm32"
],
"optional": true,
"dependencies": {
"@emnapi/runtime": "^0.44.0"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.1.tgz",
"integrity": "sha512-/xhYkylsKL05R+NXGJc9xr2Tuw6WIVl2lubFJaFYfW4/MQ4J+dgjIo/T4qjNRizrqs/szF/lC9a5+updmY9jaQ==",
"cpu": [
"ia32"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.1.tgz",
"integrity": "sha512-XaM69X0n6kTEsp9tVYYLhXdg7Qj32vYJlAKRutxUsm1UlgQNx6BOhHwZPwukCGXBU2+tH87ip2eV1I/E8MQnZg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@jest/create-cache-key-function": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz",
@ -5084,6 +5525,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
"dependencies": {
"color-convert": "^2.0.1",
"color-string": "^1.9.0"
},
"engines": {
"node": ">=12.5.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -5100,6 +5553,15 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/color-string": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"dependencies": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
"node_modules/colorette": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz",
@ -5655,6 +6117,14 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/detect-libc": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
"integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==",
"engines": {
"node": ">=8"
}
},
"node_modules/detect-node": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
@ -10108,6 +10578,59 @@
"node": ">=8"
}
},
"node_modules/sharp": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.1.tgz",
"integrity": "sha512-iAYUnOdTqqZDb3QjMneBKINTllCJDZ3em6WaWy7NPECM4aHncvqHRm0v0bN9nqJxMiwamv5KIdauJ6lUzKDpTQ==",
"hasInstallScript": true,
"dependencies": {
"color": "^4.2.3",
"detect-libc": "^2.0.2",
"semver": "^7.5.4"
},
"engines": {
"libvips": ">=8.15.0",
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "0.33.1",
"@img/sharp-darwin-x64": "0.33.1",
"@img/sharp-libvips-darwin-arm64": "1.0.0",
"@img/sharp-libvips-darwin-x64": "1.0.0",
"@img/sharp-libvips-linux-arm": "1.0.0",
"@img/sharp-libvips-linux-arm64": "1.0.0",
"@img/sharp-libvips-linux-s390x": "1.0.0",
"@img/sharp-libvips-linux-x64": "1.0.0",
"@img/sharp-libvips-linuxmusl-arm64": "1.0.0",
"@img/sharp-libvips-linuxmusl-x64": "1.0.0",
"@img/sharp-linux-arm": "0.33.1",
"@img/sharp-linux-arm64": "0.33.1",
"@img/sharp-linux-s390x": "0.33.1",
"@img/sharp-linux-x64": "0.33.1",
"@img/sharp-linuxmusl-arm64": "0.33.1",
"@img/sharp-linuxmusl-x64": "0.33.1",
"@img/sharp-wasm32": "0.33.1",
"@img/sharp-win32-ia32": "0.33.1",
"@img/sharp-win32-x64": "0.33.1"
}
},
"node_modules/sharp/node_modules/semver": {
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -10164,6 +10687,19 @@
"dev": true,
"peer": true
},
"node_modules/simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
"dependencies": {
"is-arrayish": "^0.3.1"
}
},
"node_modules/simple-swizzle/node_modules/is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
},
"node_modules/simple-update-notifier": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
@ -13131,6 +13667,15 @@
}
}
},
"@emnapi/runtime": {
"version": "0.44.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.44.0.tgz",
"integrity": "sha512-ZX/etZEZw8DR7zAB1eVQT40lNo0jeqpb6dCgOvctB6FIQ5PoXfMuNY8+ayQfu8tNQbAB8gQWSSJupR8NxeiZXw==",
"optional": true,
"requires": {
"tslib": "^2.4.0"
}
},
"@fastify/busboy": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz",
@ -13153,6 +13698,147 @@
"@hapi/hoek": "^9.0.0"
}
},
"@img/sharp-darwin-arm64": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.1.tgz",
"integrity": "sha512-esr2BZ1x0bo+wl7Gx2hjssYhjrhUsD88VQulI0FrG8/otRQUOxLWHMBd1Y1qo2Gfg2KUvXNpT0ASnV9BzJCexw==",
"optional": true,
"requires": {
"@img/sharp-libvips-darwin-arm64": "1.0.0"
}
},
"@img/sharp-darwin-x64": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.1.tgz",
"integrity": "sha512-YrnuB3bXuWdG+hJlXtq7C73lF8ampkhU3tMxg5Hh+E7ikxbUVOU9nlNtVTloDXz6pRHt2y2oKJq7DY/yt+UXYw==",
"optional": true,
"requires": {
"@img/sharp-libvips-darwin-x64": "1.0.0"
}
},
"@img/sharp-libvips-darwin-arm64": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.0.tgz",
"integrity": "sha512-VzYd6OwnUR81sInf3alj1wiokY50DjsHz5bvfnsFpxs5tqQxESoHtJO6xyksDs3RIkyhMWq2FufXo6GNSU9BMw==",
"optional": true
},
"@img/sharp-libvips-darwin-x64": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.0.tgz",
"integrity": "sha512-dD9OznTlHD6aovRswaPNEy8dKtSAmNo4++tO7uuR4o5VxbVAOoEQ1uSmN4iFAdQneTHws1lkTZeiXPrcCkh6IA==",
"optional": true
},
"@img/sharp-libvips-linux-arm": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.0.tgz",
"integrity": "sha512-VwgD2eEikDJUk09Mn9Dzi1OW2OJFRQK+XlBTkUNmAWPrtj8Ly0yq05DFgu1VCMx2/DqCGQVi5A1dM9hTmxf3uw==",
"optional": true
},
"@img/sharp-libvips-linux-arm64": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.0.tgz",
"integrity": "sha512-xTYThiqEZEZc0PRU90yVtM3KE7lw1bKdnDQ9kCTHWbqWyHOe4NpPOtMGy27YnN51q0J5dqRrvicfPbALIOeAZA==",
"optional": true
},
"@img/sharp-libvips-linux-s390x": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.0.tgz",
"integrity": "sha512-o9E46WWBC6JsBlwU4QyU9578G77HBDT1NInd+aERfxeOPbk0qBZHgoDsQmA2v9TbqJRWzoBPx1aLOhprBMgPjw==",
"optional": true
},
"@img/sharp-libvips-linux-x64": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.0.tgz",
"integrity": "sha512-naldaJy4hSVhWBgEjfdBY85CAa4UO+W1nx6a1sWStHZ7EUfNiuBTTN2KUYT5dH1+p/xij1t2QSXfCiFJoC5S/Q==",
"optional": true
},
"@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.0.tgz",
"integrity": "sha512-OdorplCyvmSAPsoJLldtLh3nLxRrkAAAOHsGWGDYfN0kh730gifK+UZb3dWORRa6EusNqCTjfXV4GxvgJ/nPDQ==",
"optional": true
},
"@img/sharp-libvips-linuxmusl-x64": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.0.tgz",
"integrity": "sha512-FW8iK6rJrg+X2jKD0Ajhjv6y74lToIBEvkZhl42nZt563FfxkCYacrXZtd+q/sRQDypQLzY5WdLkVTbJoPyqNg==",
"optional": true
},
"@img/sharp-linux-arm": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.1.tgz",
"integrity": "sha512-Ii4X1vnzzI4j0+cucsrYA5ctrzU9ciXERfJR633S2r39CiD8npqH2GMj63uFZRCFt3E687IenAdbwIpQOJ5BNA==",
"optional": true,
"requires": {
"@img/sharp-libvips-linux-arm": "1.0.0"
}
},
"@img/sharp-linux-arm64": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.1.tgz",
"integrity": "sha512-59B5GRO2d5N3tIfeGHAbJps7cLpuWEQv/8ySd9109ohQ3kzyCACENkFVAnGPX00HwPTQcaBNF7HQYEfZyZUFfw==",
"optional": true,
"requires": {
"@img/sharp-libvips-linux-arm64": "1.0.0"
}
},
"@img/sharp-linux-s390x": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.1.tgz",
"integrity": "sha512-tRGrb2pHnFUXpOAj84orYNxHADBDIr0J7rrjwQrTNMQMWA4zy3StKmMvwsI7u3dEZcgwuMMooIIGWEWOjnmG8A==",
"optional": true,
"requires": {
"@img/sharp-libvips-linux-s390x": "1.0.0"
}
},
"@img/sharp-linux-x64": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.1.tgz",
"integrity": "sha512-4y8osC0cAc1TRpy02yn5omBeloZZwS62fPZ0WUAYQiLhSFSpWJfY/gMrzKzLcHB9ulUV6ExFiu2elMaixKDbeg==",
"optional": true,
"requires": {
"@img/sharp-libvips-linux-x64": "1.0.0"
}
},
"@img/sharp-linuxmusl-arm64": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.1.tgz",
"integrity": "sha512-D3lV6clkqIKUizNS8K6pkuCKNGmWoKlBGh5p0sLO2jQERzbakhu4bVX1Gz+RS4vTZBprKlWaf+/Rdp3ni2jLfA==",
"optional": true,
"requires": {
"@img/sharp-libvips-linuxmusl-arm64": "1.0.0"
}
},
"@img/sharp-linuxmusl-x64": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.1.tgz",
"integrity": "sha512-LOGKNu5w8uu1evVqUAUKTix2sQu1XDRIYbsi5Q0c/SrXhvJ4QyOx+GaajxmOg5PZSsSnCYPSmhjHHsRBx06/wQ==",
"optional": true,
"requires": {
"@img/sharp-libvips-linuxmusl-x64": "1.0.0"
}
},
"@img/sharp-wasm32": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.1.tgz",
"integrity": "sha512-vWI/sA+0p+92DLkpAMb5T6I8dg4z2vzCUnp8yvxHlwBpzN8CIcO3xlSXrLltSvK6iMsVMNswAv+ub77rsf25lA==",
"optional": true,
"requires": {
"@emnapi/runtime": "^0.44.0"
}
},
"@img/sharp-win32-ia32": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.1.tgz",
"integrity": "sha512-/xhYkylsKL05R+NXGJc9xr2Tuw6WIVl2lubFJaFYfW4/MQ4J+dgjIo/T4qjNRizrqs/szF/lC9a5+updmY9jaQ==",
"optional": true
},
"@img/sharp-win32-x64": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.1.tgz",
"integrity": "sha512-XaM69X0n6kTEsp9tVYYLhXdg7Qj32vYJlAKRutxUsm1UlgQNx6BOhHwZPwukCGXBU2+tH87ip2eV1I/E8MQnZg==",
"optional": true
},
"@jest/create-cache-key-function": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz",
@ -15061,6 +15747,15 @@
"mimic-response": "^1.0.0"
}
},
"color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
"requires": {
"color-convert": "^2.0.1",
"color-string": "^1.9.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -15074,6 +15769,15 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"color-string": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"requires": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
"colorette": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz",
@ -15520,6 +16224,11 @@
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="
},
"detect-libc": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
"integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="
},
"detect-node": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
@ -19065,6 +19774,45 @@
"kind-of": "^6.0.2"
}
},
"sharp": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.1.tgz",
"integrity": "sha512-iAYUnOdTqqZDb3QjMneBKINTllCJDZ3em6WaWy7NPECM4aHncvqHRm0v0bN9nqJxMiwamv5KIdauJ6lUzKDpTQ==",
"requires": {
"@img/sharp-darwin-arm64": "0.33.1",
"@img/sharp-darwin-x64": "0.33.1",
"@img/sharp-libvips-darwin-arm64": "1.0.0",
"@img/sharp-libvips-darwin-x64": "1.0.0",
"@img/sharp-libvips-linux-arm": "1.0.0",
"@img/sharp-libvips-linux-arm64": "1.0.0",
"@img/sharp-libvips-linux-s390x": "1.0.0",
"@img/sharp-libvips-linux-x64": "1.0.0",
"@img/sharp-libvips-linuxmusl-arm64": "1.0.0",
"@img/sharp-libvips-linuxmusl-x64": "1.0.0",
"@img/sharp-linux-arm": "0.33.1",
"@img/sharp-linux-arm64": "0.33.1",
"@img/sharp-linux-s390x": "0.33.1",
"@img/sharp-linux-x64": "0.33.1",
"@img/sharp-linuxmusl-arm64": "0.33.1",
"@img/sharp-linuxmusl-x64": "0.33.1",
"@img/sharp-wasm32": "0.33.1",
"@img/sharp-win32-ia32": "0.33.1",
"@img/sharp-win32-x64": "0.33.1",
"color": "^4.2.3",
"detect-libc": "^2.0.2",
"semver": "^7.5.4"
},
"dependencies": {
"semver": {
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"requires": {
"lru-cache": "^6.0.0"
}
}
}
},
"shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -19109,6 +19857,21 @@
"dev": true,
"peer": true
},
"simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
"requires": {
"is-arrayish": "^0.3.1"
},
"dependencies": {
"is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
}
}
},
"simple-update-notifier": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",

View File

@ -47,6 +47,7 @@
"node-notifier": "^10.0.1",
"node-persist": "^3.1.3",
"read": "^3.0.0",
"sharp": "^0.33.1",
"splatnet3-types": "^0.2.20231119210145",
"supports-color": "^8.1.1",
"tslib": "^2.6.2",

Binary file not shown.

Binary file not shown.

View File

@ -2,12 +2,13 @@ import * as net from 'node:net';
import * as os from 'node:os';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import { createHash } from 'node:crypto';
import express, { Request, Response } from 'express';
import { fetch } from 'undici';
import * as persist from 'node-persist';
import { BankaraMatchSetting_schedule, CoopRule, CoopSetting_schedule, DetailFestRecordDetailResult, DetailVotingStatusResult, FestMatchSetting_schedule, FestRecordResult, FestState, FestTeam_schedule, FestTeam_votingStatus, FestVoteState, Fest_schedule, FriendListResult, FriendOnlineState, Friend_friendList, GraphQLSuccessResponse, KnownRequestId, LeagueMatchSetting_schedule, RegularMatchSetting_schedule, StageScheduleResult, XMatchSetting_schedule } from 'splatnet3-types/splatnet3';
import type { Arguments as ParentArguments } from '../cli.js';
import { product, version } from '../util/product.js';
import { git, product, version } from '../util/product.js';
import Users, { CoralUser } from '../common/users.js';
import { Friend } from '../api/coral-types.js';
import SplatNet3Api, { PersistedQueryResult, RequestIdSymbol } from '../api/splatnet3.js';
@ -22,6 +23,7 @@ import { ArgumentsCamelCase, Argv, YargsArguments } from '../util/yargs.js';
import { getTitleIdFromEcUrl } from '../util/misc.js';
import { getSettingForCoopRule, getSettingForVsMode } from '../discord/monitor/splatoon3.js';
import { CoralApiInterface } from '../api/coral.js';
import { PresenceEmbedFormat, getUserEmbedOptionsFromRequest, renderUserEmbedImage, renderUserEmbedSvg } from './util/presence-embed.js';
const debug = createDebug('cli:presence-server');
const debugSplatnet3Proxy = createDebug('cli:presence-server:splatnet3-proxy');
@ -31,7 +33,7 @@ interface AllUsersResult extends Friend {
splatoon3?: Friend_friendList | null;
splatoon3_fest_team?: (FestTeam_schedule & FestTeam_votingStatus) | null;
}
interface PresenceResponse {
export interface PresenceResponse {
friend: Friend;
title: TitleResult | null;
splatoon3?: Friend_friendList | null;
@ -529,7 +531,8 @@ class Server extends HttpServer {
app: express.Express;
titles = new Map</** NSA ID */ string, [TitleResult | null, /** updated */ number]>();
readonly promise_image = new Map<string, Promise<string>>();
readonly promise_image = new Map<string, Promise<string |
readonly [name: string, data: Uint8Array, type: string]>>();
constructor(
readonly storage: persist.LocalStorage,
@ -565,6 +568,18 @@ class Server extends HttpServer {
app.get('/api/presence/:user/events', this.createApiRequestHandler((req, res) =>
this.handlePresenceStreamRequest(req, res, req.params.user)));
app.get('/api/presence/:user/image', this.createApiRequestHandler((req, res) =>
this.handleUserImageRequest(req, res, req.params.user)));
app.get('/api/presence/:user/embed', this.createApiRequestHandler((req, res) =>
this.handlePresenceEmbedRequest(req, res, req.params.user, PresenceEmbedFormat.SVG)));
app.get('/api/presence/:user/embed.png', this.createApiRequestHandler((req, res) =>
this.handlePresenceEmbedRequest(req, res, req.params.user, PresenceEmbedFormat.PNG)));
app.get('/api/presence/:user/embed.jpeg', this.createApiRequestHandler((req, res) =>
this.handlePresenceEmbedRequest(req, res, req.params.user, PresenceEmbedFormat.JPEG)));
app.get('/api/presence/:user/embed.webp', this.createApiRequestHandler((req, res) =>
this.handlePresenceEmbedRequest(req, res, req.params.user, PresenceEmbedFormat.WEBP)));
if (image_proxy_path?.baas) {
this.image_proxy_path_baas = image_proxy_path.baas;
app.use('/api/presence/resources/baas', express.static(this.image_proxy_path_baas, {redirect: false}));
@ -1097,6 +1112,55 @@ class Server extends HttpServer {
}
}
async handleUserImageRequest(req: Request, res: Response, presence_user_nsaid: string) {
res.setHeader('Access-Control-Allow-Origin', '*');
const result = await this.handlePresenceRequest(req, null, presence_user_nsaid);
const url_map = await this.downloadImages({
url: result.friend.imageUri,
}, this.getResourceBaseUrls(req));
const image_url = url_map[result.friend.imageUri];
res.statusCode = 303;
res.setHeader('Location', image_url);
res.setHeader('Content-Type', 'text/plain');
res.write('Redirecting to ' + image_url + '\n');
res.end();
}
async handlePresenceEmbedRequest(req: Request, res: Response, presence_user_nsaid: string, format = PresenceEmbedFormat.SVG) {
res.setHeader('Access-Control-Allow-Origin', '*');
const result = await this.handlePresenceRequest(req, null, presence_user_nsaid);
const {theme, friend_code, transparent} = getUserEmbedOptionsFromRequest(req);
const etag = createHash('sha256').update(JSON.stringify({
result,
theme,
friend_code,
transparent,
v: version + '-' + git?.revision,
})).digest('base64url');
if (req.headers['if-none-match'] === '"' + etag + '"' || req.headers['if-none-match'] === 'W/"' + etag + '"') {
res.statusCode = 304;
res.end();
return;
}
const url_map = await this.downloadImages(result, this.getResourceBaseUrls(req));
const svg = renderUserEmbedSvg(result, url_map, theme, friend_code);
const [image, type] = await renderUserEmbedImage(svg, format);
res.setHeader('Content-Type', type);
res.setHeader('Etag', '"' + etag + '"');
res.end(image);
}
async handleSplatNet3ProxyFriends(req: Request, res: Response) {
if (!this.enable_splatnet3_proxy) throw new ResponseError(403, 'forbidden');
@ -1154,6 +1218,37 @@ class Server extends HttpServer {
atum: string | null;
splatnet3: string | null;
}): Promise<Record<string, string>> {
const image_urls = this.getImageUrls(data, base_url);
const url_map: Record<string, string> = {};
await Promise.all(image_urls.map(async ([url, dir, base_url]) => {
url_map[url] = new URL(await this.downloadImage(url, dir), base_url).toString();
}));
return url_map;
}
async getImages(data: unknown, base_url: {
baas: string | null;
atum: string | null;
splatnet3: string | null;
}): Promise<Record<string, readonly [name: string, data: Uint8Array, type: string]>> {
const image_urls = this.getImageUrls(data, base_url);
const url_map: Record<string, readonly [name: string, data: Uint8Array, type: string]> = {};
await Promise.all(image_urls.map(async ([url, dir, base_url]) => {
const [name, data, type] = await this.downloadImage(url, dir, true);
url_map[url] = [new URL(name, base_url).toString(), data, type];
}));
return url_map;
}
getImageUrls(data: unknown, base_url: {
baas: string | null;
atum: string | null;
splatnet3: string | null;
}) {
const image_urls: [url: string, dir: string, base_url: string][] = [];
// Use JSON.stringify to iterate over everything in the response
@ -1185,13 +1280,7 @@ class Server extends HttpServer {
return value;
});
const url_map: Record<string, string> = {};
await Promise.all(image_urls.map(async ([url, dir, base_url]) => {
url_map[url] = new URL(await this.downloadImage(url, dir), base_url).toString();
}));
return url_map;
return image_urls;
}
getResourceBaseUrls(req: Request) {
@ -1206,7 +1295,10 @@ class Server extends HttpServer {
};
}
downloadImage(url: string, dir: string) {
downloadImage(url: string, dir: string, return_image_data: true): Promise<readonly [name: string, data: Uint8Array, type: string]>
downloadImage(url: string, dir: string, return_image_data?: false): Promise<string>
downloadImage(url: string, dir: string, return_image_data?: boolean): Promise<string | readonly [name: string, data: Uint8Array, type: string]>
downloadImage(url: string, dir: string, return_image_data?: boolean) {
const pathname = new URL(url).pathname;
const name = pathname.substr(1).toLowerCase()
.replace(/^resources\//g, '')
@ -1215,6 +1307,11 @@ class Server extends HttpServer {
const promise = this.promise_image.get(dir + '/' + name) ?? Promise.resolve().then(async () => {
try {
if (return_image_data) {
const data = await fs.readFile(path.join(dir, name));
return [name, data, 'image/jpeg'] as const;
}
await fs.stat(path.join(dir, name));
return name;
} catch (err) {}
@ -1230,6 +1327,10 @@ class Server extends HttpServer {
debug('Downloaded image %s', name);
if (return_image_data) {
return [name, data, 'image/jpeg'] as const;
}
return name;
}).then(result => {
this.promise_image.delete(dir + '/' + name);

View File

@ -5,3 +5,5 @@ export * as discordActivity from './discord-activity.js';
export * as discordRpc from './discord-rpc.js';
export * as remoteConfig from './remote-config.js';
export * as storage from './storage.js';
export * as presenceEmbedRender from './presence-embed-render.js';
export * as presenceEmbedServer from './presence-embed-server.js';

View File

@ -0,0 +1,78 @@
import type { Arguments as ParentArguments } from '../util.js';
import createDebug from '../../util/debug.js';
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
import { getPresenceFromUrl } from '../../api/znc-proxy.js';
import { PresenceResponse } from '../presence-server.js';
import { PresenceEmbedFormat, PresenceEmbedTheme, renderUserEmbedImage, renderUserEmbedSvg } from './presence-embed.js';
const debug = createDebug('cli:util:render-presence-embed');
export const command = 'render-presence-embed <url>';
export const desc = 'Render presence embed';
export function builder(yargs: Argv<ParentArguments>) {
return yargs.positional('url', {
describe: 'Presence URL',
type: 'string',
demandOption: true,
}).option('output', {
describe: 'Output (svg, png, jpeg or webp)',
type: 'string',
default: 'svg',
}).option('theme', {
describe: 'Theme (light or dark)',
type: 'string',
default: 'light',
}).option('friend-code', {
describe: 'Friend code',
type: 'string',
}).option('scale', {
describe: 'Image scale',
type: 'number',
default: 1,
}).option('transparent', {
describe: 'Remove border and use transparent background',
type: 'boolean',
default: false,
});
}
type Arguments = YargsArguments<ReturnType<typeof builder>>;
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
const theme = argv.theme === 'dark' ? PresenceEmbedTheme.DARK : PresenceEmbedTheme.LIGHT;
const format =
argv.output === 'png' ? PresenceEmbedFormat.PNG :
argv.output === 'jpeg' ? PresenceEmbedFormat.JPEG :
argv.output === 'webp' ? PresenceEmbedFormat.WEBP :
PresenceEmbedFormat.SVG;
if (argv.friendCode && !argv.friendCode.match(/^\d{4}-\d{4}-\d{4}$/)) {
throw new TypeError('Invalid friend code');
}
const [presence, user, data] = await getPresenceFromUrl(argv.url);
const result = data as PresenceResponse;
const image_urls = [result.friend.imageUri];
if ('imageUri' in result.friend.presence.game) image_urls.push(result.friend.presence.game.imageUri);
const url_map: Record<string, readonly [name: string, data: Uint8Array, type: string]> = {};
debug('images', image_urls);
await Promise.all(image_urls.map(async (url) => {
debug('Fetching image %s', url);
const response = await fetch(url);
const data = new Uint8Array(await response.arrayBuffer());
url_map[url] = [url, data, 'image/jpeg'];
}));
const svg = renderUserEmbedSvg(result, url_map, theme, argv.friendCode, argv.scale, argv.transparent);
const [image, type] = await renderUserEmbedImage(svg, format);
console.warn('output type', type);
process.stdout.write(image);
}

View File

@ -0,0 +1,178 @@
import * as os from 'node:os';
import * as net from 'node:net';
import express, { Request, Response } from 'express';
import { createHash } from 'node:crypto';
import type { Arguments as ParentArguments } from '../util.js';
import createDebug from '../../util/debug.js';
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
import { getPresenceFromUrl } from '../../api/znc-proxy.js';
import { PresenceResponse } from '../presence-server.js';
import { addCliFeatureUserAgent } from '../../util/useragent.js';
import { HttpServer, ResponseError } from './http-server.js';
import { git, product, version } from '../../util/product.js';
import { parseListenAddress } from '../../util/net.js';
import { RawValueSymbol, htmlentities } from '../../util/misc.js';
import { PresenceEmbedFormat, PresenceEmbedTheme, getUserEmbedOptionsFromRequest, renderUserEmbedImage, renderUserEmbedSvg } from './presence-embed.js';
const debug = createDebug('cli:util:presence-embed-server');
export const command = 'presence-embed-server <url>';
export const desc = 'Presence embed test server';
export function builder(yargs: Argv<ParentArguments>) {
return yargs.positional('url', {
describe: 'Presence URL',
type: 'string',
demandOption: true,
}).option('listen', {
describe: 'Server address and port',
type: 'array',
default: ['[::]:0'],
});
}
type Arguments = YargsArguments<ReturnType<typeof builder>>;
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
addCliFeatureUserAgent('presence-embed-test-server');
const server = new Server(argv.url);
const app = server.app;
for (const address of argv.listen) {
const [host, port] = parseListenAddress(address);
const server = app.listen(port, host ?? '::');
server.on('listening', () => {
const address = server.address() as net.AddressInfo;
console.log('Listening on %s, port %d', address.address, address.port);
});
}
}
class Server extends HttpServer {
app: express.Express;
constructor(
readonly base_url: string,
) {
super();
const app = this.app = express();
app.use('/api/presence', (req, res, next) => {
console.log('[%s] %s %s HTTP/%s from %s, port %d%s, %s',
new Date(), req.method, req.url, req.httpVersion,
req.socket.remoteAddress, req.socket.remotePort,
req.headers['x-forwarded-for'] ? ' (' + req.headers['x-forwarded-for'] + ')' : '',
req.headers['user-agent']);
res.setHeader('Server', product + ' presence-embed-test-server');
res.setHeader('X-Server', product + ' presence-embed-test-server');
res.setHeader('X-Served-By', os.hostname());
next();
});
app.get('/api/presence/:user/embed', this.createApiRequestHandler((req, res) =>
this.handlePresenceEmbedRequest(req, res, req.params.user, PresenceEmbedFormat.SVG)));
app.get('/api/presence/:user/embed.png', this.createApiRequestHandler((req, res) =>
this.handlePresenceEmbedRequest(req, res, req.params.user, PresenceEmbedFormat.PNG)));
app.get('/api/presence/:user/embed.jpeg', this.createApiRequestHandler((req, res) =>
this.handlePresenceEmbedRequest(req, res, req.params.user, PresenceEmbedFormat.JPEG)));
app.get('/api/presence/:user/embed.webp', this.createApiRequestHandler((req, res) =>
this.handlePresenceEmbedRequest(req, res, req.params.user, PresenceEmbedFormat.WEBP)));
app.get('/api/presence/:user/embed.html', this.createApiRequestHandler((req, res) =>
this.handlePresenceEmbedHtmlRequest(req, res, req.params.user)));
}
async handlePresenceEmbedRequest(req: Request, res: Response, presence_user_nsaid: string, format = PresenceEmbedFormat.SVG) {
if (!presence_user_nsaid.match(/^[0-9a-f]{16}$/)) throw new ResponseError(404, 'not_found');
res.setHeader('Access-Control-Allow-Origin', '*');
const url = new URL(req.url, 'https://localhost');
url.searchParams.delete('theme');
url.searchParams.delete('friend-code');
url.searchParams.delete('transparent');
const qs = url.searchParams.size ? '?' + url.searchParams.toString() : '';
const [presence, user, data] = await getPresenceFromUrl(this.base_url + '/' + presence_user_nsaid + qs);
const result = data as PresenceResponse;
const {theme, friend_code, transparent} = getUserEmbedOptionsFromRequest(req);
const etag = createHash('sha256').update(JSON.stringify({
data,
format,
theme,
friend_code,
transparent,
v: version + '-' + git?.revision,
})).digest('base64url');
if (req.headers['if-none-match'] === '"' + etag + '"' || req.headers['if-none-match'] === 'W/"' + etag + '"') {
res.statusCode = 304;
res.end();
return;
}
const image_urls = [result.friend.imageUri];
if ('imageUri' in result.friend.presence.game) image_urls.push(result.friend.presence.game.imageUri);
const url_map: Record<string, readonly [name: string, data: Uint8Array, type: string]> = {};
await Promise.all(image_urls.map(async (url) => {
debug('Fetching image %s', url);
const response = await fetch(url);
const data = new Uint8Array(await response.arrayBuffer());
url_map[url] = [url, data, 'image/jpeg'];
}));
const svg = renderUserEmbedSvg(result, url_map, theme, friend_code, 1, transparent);
const [image, type] = await renderUserEmbedImage(svg, format);
res.setHeader('Content-Type', type);
res.setHeader('Etag', '"' + etag + '"');
res.end(image);
}
async handlePresenceEmbedHtmlRequest(req: Request, res: Response, presence_user_nsaid: string) {
if (!presence_user_nsaid.match(/^[0-9a-f]{16}$/)) throw new ResponseError(404, 'not_found');
const url = new URL(req.url, 'https://localhost');
url.searchParams.delete('theme');
const qs = url.searchParams.size ? '&' + url.searchParams.toString() : '';
const url_2 = new URL(req.url, 'https://localhost');
url_2.searchParams.delete('theme');
url_2.searchParams.delete('friend-code');
const qs_2 = url_2.searchParams.size ? '?' + url_2.searchParams.toString() : '';
const [presence, user, data] = await getPresenceFromUrl(this.base_url + '/' + presence_user_nsaid + qs_2);
const result = data as PresenceResponse;
const image_urls = [result.friend.imageUri];
if ('imageUri' in result.friend.presence.game) image_urls.push(result.friend.presence.game.imageUri);
const url_map: Record<string, readonly [name: string, data: Uint8Array, type: string]> = {};
await Promise.all(image_urls.map(async (url) => {
debug('Fetching image %s', url);
const response = await fetch(url);
const data = new Uint8Array(await response.arrayBuffer());
url_map[url] = [url, data, 'image/jpeg'];
}));
const svg = renderUserEmbedSvg(result, url_map, PresenceEmbedTheme.LIGHT, undefined, 1, true);
res.setHeader('Content-Type', 'text/html');
res.write(`<!DOCTYPE html><html><head><meta name="viewport" content="width=device-width,initial-scale=1"/><style>body{margin:0;min-height:100vh;width:100vw;min-width:fit-content;display:flex;align-items:center;justify-content:center}</style></head>`);
res.write(htmlentities`<body><picture><source srcset="embed?theme=dark${qs}" media="(prefers-color-scheme:dark)"/><img src="embed?theme=light${qs}" alt="Nintendo Switch presence"/></picture><p>${{[RawValueSymbol]: svg}}</p></body></html>\n`);
res.end();
}
}

View File

@ -0,0 +1,220 @@
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import { Request } from 'express';
import sharp from 'sharp';
import { FriendOnlineState } from 'splatnet3-types/splatnet3';
import { dir } from '../../util/product.js';
import createDebug from '../../util/debug.js';
import { Game, PresenceState } from '../../api/coral-types.js';
import { RawValueSymbol, htmlentities } from '../../util/misc.js';
import { PresenceResponse } from '../presence-server.js';
const debug = createDebug('cli:util:presence-embed');
export enum PresenceEmbedFormat {
SVG,
PNG,
JPEG,
WEBP,
}
export enum PresenceEmbedTheme {
LIGHT,
DARK,
}
interface PresenceEmbedThemeColours {
background: string;
separator: string;
text: string;
online: string;
online_border: string;
}
const embed_themes: Record<PresenceEmbedTheme, PresenceEmbedThemeColours> = {
[PresenceEmbedTheme.LIGHT]: {
background: '#ebebeb',
separator: '#7b7b7b',
text: '#000000',
online: '#2db742',
online_border: '#0eb728',
},
[PresenceEmbedTheme.DARK]: {
background: '#2d2d2d',
separator: '#7e7e7e',
text: '#ffffff',
online: '#47e85f',
online_border: '#19e838',
},
};
const embed_titles: Partial<Record<string, (
result: PresenceResponse,
url_map: Record<string, string | readonly [url: string, data: Uint8Array, type: string]>,
image: (url: string) => string | undefined,
theme?: PresenceEmbedTheme,
) => readonly [svg: string, height: number]>> = {
'0100c2500fc20000': renderUserSplatoon3EmbedPartialSvg,
};
export function getUserEmbedOptionsFromRequest(req: Request) {
const url = new URL(req.url, 'https://localhost');
const theme = url.searchParams.get('theme') === 'dark' ? PresenceEmbedTheme.DARK : PresenceEmbedTheme.LIGHT;
const friend_code = url.searchParams.getAll('friend-code').find(c => c.match(/^\d{4}-\d{4}-\d{4}$/));
const transparent = url.searchParams.get('transparent') === '1';
return {theme, friend_code, transparent};
}
export async function renderUserEmbedImage(
svg: string,
format: PresenceEmbedFormat,
): Promise<[data: Buffer, type: string]> {
if (format === PresenceEmbedFormat.SVG) {
return [Buffer.from(svg), 'image/svg+xml'];
}
const start = Date.now();
debug('generating image, format %s', PresenceEmbedFormat[format]);
let image = sharp(Buffer.from(svg)).withMetadata({
density: 72 * 2,
});
if (format === PresenceEmbedFormat.PNG) image = image.png();
if (format === PresenceEmbedFormat.JPEG) image = image.jpeg();
if (format === PresenceEmbedFormat.WEBP) image = image.webp();
const data = await image.toBuffer();
debug('generated %s in %d ms', PresenceEmbedFormat[format], Date.now() - start);
if (format === PresenceEmbedFormat.PNG) return [data, 'image/png'];
if (format === PresenceEmbedFormat.JPEG) return [data, 'image/jpeg'];
if (format === PresenceEmbedFormat.WEBP) return [data, 'image/webp'];
throw new TypeError('Invalid format');
}
export function renderUserEmbedSvg(
result: PresenceResponse,
url_map: Record<string, string | readonly [url: string, data: Uint8Array, type: string]>,
theme = PresenceEmbedTheme.LIGHT,
friend_code?: string,
scale = 1,
transparent = false,
) {
let width = 500;
let height = 180;
if (friend_code) height += 40;
const colours = embed_themes[theme];
const font_family = '\'Open Sans\', -apple-system, BlinkMacSystemFont, Arial, sans-serif';
const state = result.friend.presence.state;
const game = 'name' in result.friend.presence.game ? result.friend.presence.game : null;
const image = (url: string) =>
url_map[url] instanceof Array ?
'data:' + url_map[url][2] + ';base64,' +
Buffer.from(url_map[url][1]).toString('base64') :
url_map[url] as string | undefined;
const title_extra = result.title ? embed_titles[result.title.id]?.call(null, result, url_map, image, theme) : null;
if (title_extra) height += title_extra[1];
return htmlentities`<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- ${JSON.stringify(result, null, 4)} -->
<svg
width="${(width + (transparent ? -60 : 0)) * scale}"
height="${(height + (transparent ? -60 : 0) + (title_extra?.[1] ?? 0)) * scale}"
viewBox="${transparent ? '30 30' : '0 0'} ${width + (transparent ? -60 : 0)} ${height + (transparent ? -60 : 0) + (title_extra?.[1] ?? 0)}"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
>
<style>${embed_style}</style>
<defs>
<linearGradient id="gradient-out">
<stop offset="0%" stop-opacity="1" stop-color="#ffffff"></stop>
<stop offset="100%" stop-opacity="0" stop-color="#ffffff"></stop>
</linearGradient>
<mask id="mask-out">
<rect x="0" y="0" width="${width - 50}" height="${height}" fill="#ffffff"></rect>
<rect x="${width - 50}" y="0" width="20" height="${height}" fill="url(#gradient-out)"></rect>
</mask>
</defs>
${{[RawValueSymbol]: transparent ? '' : htmlentities`
<rect x="0" y="0" width="${width}" height="${height}" fill="${colours.background}" />
`}}
<image x="30" y="30" width="120" height="120"
href="${image(result.friend.imageUri) ?? result.friend.imageUri}" />
<text x="180" y="57" fill="${colours.text}" font-size="26" font-family="${font_family}" font-weight="500">${result.friend.name}</text>
<line x1="180" y1="73" x2="470" y2="73" stroke="${colours.separator}" />
${{[RawValueSymbol]: game && (state === PresenceState.ONLINE || state === PresenceState.PLAYING) ? htmlentities`
<image x="180" y="87" width="60" height="60"
href="${image(game.imageUri) ?? game.imageUri}" />
${{[RawValueSymbol]: renderUserTitleEmbedPartialSvg(game, colours, font_family)}}
` : htmlentities`
<text x="180" y="97" fill="${colours.text}" font-size="14" font-family="${font_family}" font-weight="400">Offline</text>
`}}
${{[RawValueSymbol]: friend_code ? htmlentities`
<text x="30" y="186" fill="${colours.text}" font-size="14" font-family="${font_family}" font-weight="400">Friend code: SW-${friend_code}</text>
` : ''}}
${{[RawValueSymbol]: title_extra?.[0] ?? ''}}
</svg>
`;
}
function renderUserTitleEmbedPartialSvg(game: Game, colours: PresenceEmbedThemeColours, font_family: string) {
const playing_text_offset = game.sysDescription ? 92 : 97;
const title_name_text_offset = game.sysDescription ? 122 : 133;
return htmlentities`
<rect x="255" y="${playing_text_offset}" width="10" height="10" fill="${colours.online}"
stroke="${colours.online_border}" stroke-width="1" rx="1" ry="1" stroke-linejoin="round"
/>
<text x="272" y="${playing_text_offset + 10}" fill="${colours.online}" font-size="14" font-family="${font_family}" font-weight="400" mask="url(#mask-out)">Playing</text>
<text x="255" y="${title_name_text_offset}" fill="${colours.text}" font-size="14" font-family="${font_family}" font-weight="400" mask="url(#mask-out)">${game.name}</text>
<text x="255" y="142" fill="${colours.text}" font-size="14" font-family="${font_family}" font-weight="300" mask="url(#mask-out)">${game.sysDescription}</text>
`;
}
function renderUserSplatoon3EmbedPartialSvg(
result: PresenceResponse,
url_map: Record<string, string | readonly [url: string, data: Uint8Array, type: string]>,
image: (url: string) => string | undefined,
theme = PresenceEmbedTheme.LIGHT,
) {
return ['', 0] as const;
}
const embed_fonts: [name: string, style: string, weight: string, files: [format: string, type: string, path: string][]][] = [
['Open Sans', 'normal', '400', [['opentype', 'font/ttf', 'opensans-normal-400.ttf']]],
['Open Sans', 'normal', '500', [['opentype', 'font/ttf', 'opensans-normal-500.ttf']]],
];
const embed_style = `
text {
-webkit-user-select: none;
user-select: none;
}
` + (await Promise.all(embed_fonts.map(async ([name, style, weight, files]) => `@font-face {
font-family: '${name}';
font-style: ${style};
font-weight: ${weight};
src: ${(await Promise.all(files.map(async ([format, type, file]) => `url('data:${type};base64,${
(await fs.readFile(path.join(dir, 'resources', 'cli', 'fonts', file))).toString('base64')
}') format('${format}')`))).join(',')};
}`))).join('\n');

View File

@ -40,3 +40,14 @@ export function timeoutSignal(ms = 60 * 1000) {
return [controller.signal, () => clearTimeout(timeout), controller] as const;
}
export const RawValueSymbol = Symbol('RawValue');
export type RawValue = {[RawValueSymbol]: string};
export function htmlentities(strings: TemplateStringsArray, ...args: (string | number | RawValue)[]): string {
return strings.map((s, i) => s + (args[i] ? (
typeof args[i] === 'object' && RawValueSymbol in (args[i] as object) ?
(args[i] as RawValue)[RawValueSymbol] :
args[i].toString().replace(/[\u00A0-\u9999<>\&]/gim, c => '&#' + c.charCodeAt(0) + ';')
) : '')).join('');
}